diff --git a/CMakeLists.txt b/CMakeLists.txt
index 97a94a4eff3e7b956a219ca9399b5221157d6500..114ceecc5d49685a513708dced3dcffea83bff29 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -530,6 +530,7 @@ target_link_libraries(
   PugsAlgebra
   PugsScheme
   PugsUtils
+  PugsOutput
   PugsLanguageUtils
   kokkos
   ${PETSC_LIBRARIES}
@@ -552,7 +553,8 @@ if(${CMAKE_VERSION} VERSION_LESS "3.13.0")
     LIBRARY DESTINATION lib
     ARCHIVE DESTINATION lib)
 else()
-  install(TARGETS pugs PugsMesh PugsUtils PugsLanguage
+  install(TARGETS pugs  PugsAlgebra PugsUtils PugsLanguage PugsLanguageAST PugsLanguageModules PugsLanguageAlgorithms  PugsLanguageUtils PugsMesh PugsScheme PugsOutput
+
     RUNTIME DESTINATION bin
     LIBRARY DESTINATION lib
     ARCHIVE DESTINATION lib)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e9c207b2fc2f256c6e17a35afbafcb82521dea75..16904a7be0cddc9aa54206638ab4b5c759cd0de8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -19,4 +19,4 @@ add_subdirectory(mesh)
 add_subdirectory(scheme)
 
 # Pugs output
-#add_subdirectory(output)
+add_subdirectory(output)
diff --git a/src/language/algorithms/AcousticSolverAlgorithm.cpp b/src/language/algorithms/AcousticSolverAlgorithm.cpp
index 9811c90f0b0f2c52d5f72369efea4a4398f007e3..a2a456f0ebc91476b2dbb541e9fdc6eecc77c83c 100644
--- a/src/language/algorithms/AcousticSolverAlgorithm.cpp
+++ b/src/language/algorithms/AcousticSolverAlgorithm.cpp
@@ -7,6 +7,8 @@
 #include <scheme/IBoundaryDescriptor.hpp>
 #include <scheme/SymmetryBoundaryConditionDescriptor.hpp>
 
+#include <iomanip>
+
 template <size_t Dimension>
 AcousticSolverAlgorithm<Dimension>::AcousticSolverAlgorithm(
   const AcousticSolverType& solver_type,
diff --git a/src/language/modules/SchemeModule.cpp b/src/language/modules/SchemeModule.cpp
index e691e248adb885410d7a50d8994734517d4fc921..c23d3794e4c26a95301599b6933fe0d643391010 100644
--- a/src/language/modules/SchemeModule.cpp
+++ b/src/language/modules/SchemeModule.cpp
@@ -5,6 +5,7 @@
 #include <language/utils/TypeDescriptor.hpp>
 #include <mesh/Mesh.hpp>
 #include <scheme/DirichletBoundaryConditionDescriptor.hpp>
+#include <scheme/DiscreteFunctionDescriptorP0.hpp>
 #include <scheme/DiscreteFunctionInterpoler.hpp>
 #include <scheme/FourierBoundaryConditionDescriptor.hpp>
 #include <scheme/FreeBoundaryConditionDescriptor.hpp>
diff --git a/src/language/modules/VTKModule.cpp b/src/language/modules/VTKModule.cpp
index 3144460fb5729ba29db2f20f864d6a5552e74a9b..9c6b1838f641cb2bab7ac36886b01da8ee398643 100644
--- a/src/language/modules/VTKModule.cpp
+++ b/src/language/modules/VTKModule.cpp
@@ -5,118 +5,164 @@
 #include <mesh/Connectivity.hpp>
 #include <mesh/GmshReader.hpp>
 #include <mesh/Mesh.hpp>
+#include <output/IWriter.hpp>
+#include <output/NamedDiscreteFunction.hpp>
 #include <output/VTKWriter.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
 #include <scheme/IDiscreteFunction.hpp>
+#include <scheme/IDiscreteFunctionDescriptor.hpp>
 
 VTKModule::VTKModule()
 {
-  this->_addBuiltinFunction("writeMeshVTK",
-                            std::make_shared<
-                              BuiltinFunctionEmbedder<void(std::shared_ptr<const IMesh>, const std::string&)>>(
+  this->_addTypeDescriptor(ast_node_data_type_from<std::shared_ptr<const NamedDiscreteFunction>>);
 
-                              [](std::shared_ptr<const IMesh> p_mesh, const std::string& filename) -> void {
-                                VTKWriter writer(filename, 0.1);
+  this->_addTypeDescriptor(ast_node_data_type_from<std::shared_ptr<const IWriter>>);
 
-                                static double time = 0;
+  this->_addBuiltinFunction("vtk_writer",
+                            std::make_shared<BuiltinFunctionEmbedder<std::shared_ptr<const IWriter>(const std::string&,
+                                                                                                    const double&)>>(
 
-                                switch (p_mesh->dimension()) {
-                                case 1: {
-                                  using MeshType = Mesh<Connectivity<1>>;
-                                  const std::shared_ptr<const MeshType> mesh =
-                                    std::dynamic_pointer_cast<const MeshType>(p_mesh);
-
-                                  writer.write(mesh,
-                                               OutputNamedItemValueSet{
-                                                 NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
-                                               time, true);
-                                  break;
-                                }
-                                case 2: {
-                                  using MeshType = Mesh<Connectivity<2>>;
-                                  const std::shared_ptr<const MeshType> mesh =
-                                    std::dynamic_pointer_cast<const MeshType>(p_mesh);
-
-                                  writer.write(mesh,
-                                               OutputNamedItemValueSet{
-                                                 NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
-                                               time, true);
-                                  break;
-                                }
-                                case 3: {
-                                  using MeshType = Mesh<Connectivity<3>>;
-                                  const std::shared_ptr<const MeshType> mesh =
-                                    std::dynamic_pointer_cast<const MeshType>(p_mesh);
-
-                                  writer.write(mesh,
-                                               OutputNamedItemValueSet{
-                                                 NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
-                                               time, true);
-                                  break;
-                                }
-                                }
-
-                                time++;
+                              [](const std::string& filename, const double& period) {
+                                return std::make_shared<VTKWriter>(filename, period);
                               }
 
                               ));
 
-  this->_addBuiltinFunction("writeVTK",
+  this->_addBuiltinFunction("name_output",
                             std::make_shared<BuiltinFunctionEmbedder<
-                              void(const std::vector<std::shared_ptr<const IDiscreteFunction>>&, const std::string&)>>(
+                              std::shared_ptr<const NamedDiscreteFunction>(std::shared_ptr<const IDiscreteFunction>,
+                                                                           const std::string&)>>(
 
-                              [](const std::vector<std::shared_ptr<const IDiscreteFunction>>& discrete_function_list,
-                                 const std::string& filename) -> void {
-                                Assert(discrete_function_list.size() > 0);
+                              [](std::shared_ptr<const IDiscreteFunction> discrete_function,
+                                 const std::string& name) -> std::shared_ptr<const NamedDiscreteFunction> {
+                                return std::make_shared<const NamedDiscreteFunction>(discrete_function, name);
+                              }
 
-                                VTKWriter writer(filename, 0.1);
+                              ));
 
-                                static double time = 0;
+  this->_addBuiltinFunction("write_mesh",
+                            std::make_shared<BuiltinFunctionEmbedder<void(std::shared_ptr<const IWriter>,
+                                                                          std::shared_ptr<const IMesh>)>>(
 
-                                std::shared_ptr p_mesh = discrete_function_list[0]->mesh();
-                                for (size_t i = 1; i < discrete_function_list.size(); ++i) {
-                                  if (p_mesh != discrete_function_list[i]->mesh()) {
-                                    throw NormalError("discrete functions must be defined on the same mesh!");
-                                  }
-                                }
+                              [](std::shared_ptr<const IWriter> writer, std::shared_ptr<const IMesh> p_mesh) -> void {
+                                static double number = 0;
 
                                 switch (p_mesh->dimension()) {
                                 case 1: {
-                                  using MeshType = Mesh<Connectivity<1>>;
-                                  const std::shared_ptr<const MeshType> mesh =
-                                    std::dynamic_pointer_cast<const MeshType>(p_mesh);
-
-                                  writer.write(mesh,
-                                               OutputNamedItemValueSet{
-                                                 NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
-                                               time, true);
+                                  const std::shared_ptr mesh =
+                                    std::dynamic_pointer_cast<const Mesh<Connectivity<1>>>(p_mesh);
+
+                                  writer->write(mesh,
+                                                OutputNamedItemValueSet{
+                                                  NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
+                                                number, true);
                                   break;
                                 }
                                 case 2: {
-                                  using MeshType = Mesh<Connectivity<2>>;
-                                  const std::shared_ptr<const MeshType> mesh =
-                                    std::dynamic_pointer_cast<const MeshType>(p_mesh);
-
-                                  writer.write(mesh,
-                                               OutputNamedItemValueSet{
-                                                 NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
-                                               time, true);
+                                  const std::shared_ptr mesh =
+                                    std::dynamic_pointer_cast<const Mesh<Connectivity<2>>>(p_mesh);
+
+                                  writer->write(mesh,
+                                                OutputNamedItemValueSet{
+                                                  NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
+                                                number, true);
                                   break;
                                 }
                                 case 3: {
-                                  using MeshType = Mesh<Connectivity<3>>;
-                                  const std::shared_ptr<const MeshType> mesh =
-                                    std::dynamic_pointer_cast<const MeshType>(p_mesh);
-
-                                  writer.write(mesh,
-                                               OutputNamedItemValueSet{
-                                                 NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
-                                               time, true);
+                                  const std::shared_ptr mesh =
+                                    std::dynamic_pointer_cast<const Mesh<Connectivity<3>>>(p_mesh);
+
+                                  writer->write(mesh,
+                                                OutputNamedItemValueSet{
+                                                  NamedItemValue{"cell_number", mesh->connectivity().cellNumber()}},
+                                                number, true);
                                   break;
                                 }
                                 }
 
-                                time++;
+                                number++;
                               }
 
                               ));
+
+  this->_addBuiltinFunction(
+    "writeVTK",
+    std::make_shared<
+      BuiltinFunctionEmbedder<void(std::shared_ptr<const IWriter>,
+                                   const std::vector<std::shared_ptr<const NamedDiscreteFunction>>&, const double&)>>(
+
+      [](std::shared_ptr<const IWriter> writer,
+         const std::vector<std::shared_ptr<const NamedDiscreteFunction>>& named_discrete_function_list,
+         const double& time) -> void {
+        Assert(named_discrete_function_list.size() > 0);
+
+        std::shared_ptr p_mesh = named_discrete_function_list[0]->discreteFunction()->mesh();
+        for (size_t i = 1; i < named_discrete_function_list.size(); ++i) {
+          if (p_mesh != named_discrete_function_list[i]->discreteFunction()->mesh()) {
+            std::ostringstream error_msg;
+            error_msg << "discrete functions must be defined on the same mesh!\n"
+                      << rang::fgB::yellow << "note:" << rang::style::reset << " cannot write " << rang::fgB::blue
+                      << named_discrete_function_list[0]->name() << rang::style::reset << " and " << rang::fgB::blue
+                      << named_discrete_function_list[i]->name() << rang::style::reset << " in the same file.";
+            throw NormalError(error_msg.str());
+          }
+        }
+
+        OutputNamedItemValueSet named_item_value_set;
+
+        for (auto& named_discrete_function : named_discrete_function_list) {
+          const IDiscreteFunction& i_discrete_function = *named_discrete_function->discreteFunction();
+
+          switch (i_discrete_function.descriptor().type()) {
+          case DiscreteFunctionType::P0: {
+            const DiscreteFunctionP0<2, double>& discrete_function =
+              dynamic_cast<const DiscreteFunctionP0<2, double>&>(i_discrete_function);
+
+            named_item_value_set.add(NamedItemValue{named_discrete_function->name(), discrete_function.cellValues()});
+            break;
+          }
+          default: {
+            std::ostringstream error_msg;
+            error_msg << "the type of discrete function of " << rang::fgB::blue << named_discrete_function->name()
+                      << rang::style::reset << " is not supported";
+            throw NormalError(error_msg.str());
+          }
+          }
+        }
+
+        switch (p_mesh->dimension()) {
+        case 1: {
+          using MeshType                             = Mesh<Connectivity<1>>;
+          const std::shared_ptr<const MeshType> mesh = std::dynamic_pointer_cast<const MeshType>(p_mesh);
+
+          named_item_value_set.add(NamedItemValue{"cell_number", mesh->connectivity().cellNumber()});
+          named_item_value_set.add(NamedItemValue{"node_number", mesh->connectivity().nodeNumber()});
+
+          writer->write(mesh, named_item_value_set, time, true);
+          break;
+        }
+        case 2: {
+          using MeshType                             = Mesh<Connectivity<2>>;
+          const std::shared_ptr<const MeshType> mesh = std::dynamic_pointer_cast<const MeshType>(p_mesh);
+
+          named_item_value_set.add(NamedItemValue{"cell_number", mesh->connectivity().cellNumber()});
+          named_item_value_set.add(NamedItemValue{"node_number", mesh->connectivity().nodeNumber()});
+
+          writer->write(mesh, named_item_value_set, time, true);
+          break;
+        }
+        case 3: {
+          using MeshType                             = Mesh<Connectivity<3>>;
+          const std::shared_ptr<const MeshType> mesh = std::dynamic_pointer_cast<const MeshType>(p_mesh);
+
+          named_item_value_set.add(NamedItemValue{"cell_number", mesh->connectivity().cellNumber()});
+          named_item_value_set.add(NamedItemValue{"node_number", mesh->connectivity().nodeNumber()});
+
+          writer->write(mesh, named_item_value_set, time, true);
+          break;
+        }
+        }
+      }
+
+      ));
 }
diff --git a/src/language/modules/VTKModule.hpp b/src/language/modules/VTKModule.hpp
index 1578fad811b7dae1181053317e891dbb865ad035..55d413e888c0edef1ba138d09ad265df2dd7f2bf 100644
--- a/src/language/modules/VTKModule.hpp
+++ b/src/language/modules/VTKModule.hpp
@@ -2,8 +2,19 @@
 #define VTK_MODULE_HPP
 
 #include <language/modules/BuiltinModule.hpp>
+#include <language/utils/ASTNodeDataTypeTraits.hpp>
 #include <utils/PugsMacros.hpp>
 
+class NamedDiscreteFunction;
+template <>
+inline ASTNodeDataType ast_node_data_type_from<std::shared_ptr<const NamedDiscreteFunction>> =
+  ASTNodeDataType::build<ASTNodeDataType::type_id_t>("output");
+
+class IWriter;
+template <>
+inline ASTNodeDataType ast_node_data_type_from<std::shared_ptr<const IWriter>> =
+  ASTNodeDataType::build<ASTNodeDataType::type_id_t>("writer");
+
 class VTKModule : public BuiltinModule
 {
  public:
diff --git a/src/output/CMakeLists.txt b/src/output/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e5b860bc0bf2781a16d7453537e6657600d55250
--- /dev/null
+++ b/src/output/CMakeLists.txt
@@ -0,0 +1,14 @@
+# ------------------- Source files --------------------
+
+add_library(
+  PugsOutput
+  VTKWriter.cpp)
+
+# ------------------- Installation --------------------
+# temporary version workaround
+if(${CMAKE_VERSION} VERSION_LESS "3.13.0")
+  install(TARGETS PugsMesh
+    RUNTIME DESTINATION bin
+    LIBRARY DESTINATION lib
+    ARCHIVE DESTINATION lib)
+endif()
diff --git a/src/output/IWriter.hpp b/src/output/IWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..98df0858ee121db43119ba085e783ae757c3022d
--- /dev/null
+++ b/src/output/IWriter.hpp
@@ -0,0 +1,19 @@
+#ifndef I_WRITER_HPP
+#define I_WRITER_HPP
+
+#include <output/OutputNamedItemValueSet.hpp>
+class IMesh;
+
+class IWriter
+{
+ public:
+  virtual void write(const std::shared_ptr<const IMesh>& mesh,
+                     const OutputNamedItemValueSet& output_named_item_value_set,
+                     double time,
+                     bool forced_output = false) const = 0;
+
+  IWriter()          = default;
+  virtual ~IWriter() = default;
+};
+
+#endif   // I_WRITER_HPP
diff --git a/src/output/NamedDiscreteFunction.hpp b/src/output/NamedDiscreteFunction.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1b0850040a1fd944c5e3d3ba361f7d764bece3dd
--- /dev/null
+++ b/src/output/NamedDiscreteFunction.hpp
@@ -0,0 +1,39 @@
+#ifndef NAMED_DISCRETE_FUNCTION_HPP
+#define NAMED_DISCRETE_FUNCTION_HPP
+
+#include <memory>
+#include <string>
+
+class IDiscreteFunction;
+
+class NamedDiscreteFunction
+{
+ private:
+  std::shared_ptr<const IDiscreteFunction> m_discrete_function;
+  std::string m_name;
+
+ public:
+  const std::string&
+  name() const
+  {
+    return m_name;
+  }
+
+  const std::shared_ptr<const IDiscreteFunction>
+  discreteFunction() const
+  {
+    return m_discrete_function;
+  }
+
+  NamedDiscreteFunction(const std::shared_ptr<const IDiscreteFunction>& discrete_function, const std::string& name)
+    : m_discrete_function{discrete_function}, m_name{name}
+  {}
+
+  NamedDiscreteFunction(const NamedDiscreteFunction&) = default;
+  NamedDiscreteFunction(NamedDiscreteFunction&&)      = default;
+
+  NamedDiscreteFunction()  = default;
+  ~NamedDiscreteFunction() = default;
+};
+
+#endif   // NAMED_DISCRETE_FUNCTION_HPP
diff --git a/src/output/OutputNamedItemValueSet.hpp b/src/output/OutputNamedItemValueSet.hpp
index 7dcc23e3d0aadaa95cb3a4333e38b73d1edad9a5..c474419eeca269644fa99f4b6c45fa596439c5e0 100644
--- a/src/output/OutputNamedItemValueSet.hpp
+++ b/src/output/OutputNamedItemValueSet.hpp
@@ -103,6 +103,13 @@ class OutputNamedItemValueSet
     return m_name_itemvariant_map.end();
   }
 
+  template <typename DataType, ItemType item_type>
+  void
+  add(const NamedItemValue<DataType, item_type>& named_itemvalue)
+  {
+    _doInsert(named_itemvalue);
+  }
+
   template <typename... DataType, ItemType... item_type>
   OutputNamedItemValueSet(NamedItemValue<DataType, item_type>... named_itemvalue)
   {
diff --git a/src/output/VTKWriter.cpp b/src/output/VTKWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..11a5d4bb5774076429bae9cbedce05a7b90b7e91
--- /dev/null
+++ b/src/output/VTKWriter.cpp
@@ -0,0 +1,468 @@
+#include <output/VTKWriter.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <utils/Messenger.hpp>
+
+#include <fstream>
+#include <iomanip>
+#include <sstream>
+#include <unordered_map>
+
+std::string
+VTKWriter::_getFilenamePVTU() const
+{
+  std::ostringstream sout;
+  sout << m_base_filename;
+  sout << '.' << std::setfill('0') << std::setw(4) << m_file_number << ".pvtu";
+  return sout.str();
+}
+
+std::string
+VTKWriter::_getFilenameVTU(int rank_number) const
+{
+  std::ostringstream sout;
+  sout << m_base_filename;
+  if (parallel::size() > 1) {
+    sout << '-' << std::setfill('0') << std::setw(4) << rank_number;
+  }
+  sout << '.' << std::setfill('0') << std::setw(4) << m_file_number << ".vtu";
+  return sout.str();
+}
+
+template <typename DataType>
+void
+VTKWriter::_write_node_pvtu(std::ofstream& os, const std::string& name, const NodeValue<const DataType>&) const
+{
+  os << "<PDataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\"/>\n";
+}
+
+template <size_t N, typename DataType>
+void
+VTKWriter::_write_node_pvtu(std::ofstream& os,
+                            const std::string& name,
+                            const NodeValue<const TinyVector<N, DataType>>&) const
+{
+  os << "<PDataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
+     << "\"/>\n";
+}
+
+template <typename DataType>
+void
+VTKWriter::_write_node_pvtu(std::ofstream&, const std::string&, const CellValue<const DataType>&) const
+{}
+
+template <typename DataType>
+void
+VTKWriter::_write_cell_pvtu(std::ofstream& os, const std::string& name, const CellValue<const DataType>&) const
+{
+  os << "<PDataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\"/>\n";
+}
+
+template <size_t N, typename DataType>
+void
+VTKWriter::_write_cell_pvtu(std::ofstream& os,
+                            const std::string& name,
+                            const CellValue<const TinyVector<N, DataType>>&) const
+{
+  os << "<PDataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
+     << "\"/>\n";
+}
+
+template <typename DataType>
+void
+VTKWriter::_write_cell_pvtu(std::ofstream&, const std::string&, const NodeValue<const DataType>&) const
+{}
+
+template <typename DataType>
+struct VTKWriter::VTKType
+{
+  inline const static std::string name = [] {
+    static_assert(std::is_arithmetic_v<DataType>, "invalid data type");
+
+    if constexpr (std::is_integral_v<DataType>) {
+      if constexpr (std::is_unsigned_v<DataType>) {
+        return "UInt" + std::to_string(sizeof(DataType) * 8);
+      } else {
+        return "UInt" + std::to_string(sizeof(DataType) * 8);
+      }
+    } else if constexpr (std::is_floating_point_v<DataType>) {
+      return "Float" + std::to_string(sizeof(DataType) * 8);
+    }
+  }();
+};
+
+template <typename DataType>
+void
+VTKWriter::_write_array(std::ofstream& os, const std::string& name, const Array<DataType>& item_value) const
+{
+  os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\">\n";
+  for (typename Array<DataType>::index_type i = 0; i < item_value.size(); ++i) {
+    // The following '+' enforces integer output for char types
+    os << +item_value[i] << ' ';
+  }
+  os << "\n</DataArray>\n";
+}
+
+template <size_t N, typename DataType>
+void
+VTKWriter::_write_array(std::ofstream& os,
+                        const std::string& name,
+                        const Array<TinyVector<N, DataType>>& item_value) const
+{
+  os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
+     << "\">\n";
+  for (typename Array<DataType>::index_type i = 0; i < item_value.size(); ++i) {
+    for (size_t j = 0; j < N; ++j) {
+      // The following '+' enforces integer output for char types
+      os << +item_value[i][j] << ' ';
+    }
+  }
+  os << "\n</DataArray>\n";
+}
+
+template <typename DataType>
+void
+VTKWriter::_write_node_value(std::ofstream& os,
+                             const std::string& name,
+                             const NodeValue<const DataType>& item_value) const
+{
+  os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\">\n";
+  for (NodeId i = 0; i < item_value.size(); ++i) {
+    // The following '+' enforces integer output for char types
+    os << +item_value[i] << ' ';
+  }
+  os << "\n</DataArray>\n";
+}
+
+template <size_t N, typename DataType>
+void
+VTKWriter::_write_node_value(std::ofstream& os,
+                             const std::string& name,
+                             const NodeValue<const TinyVector<N, DataType>>& item_value) const
+{
+  os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
+     << "\">\n";
+  for (NodeId i = 0; i < item_value.size(); ++i) {
+    for (size_t j = 0; j < N; ++j) {
+      // The following '+' enforces integer output for char types
+      os << +item_value[i][j] << ' ';
+    }
+  }
+  os << "\n</DataArray>\n";
+}
+
+template <typename DataType>
+void
+VTKWriter::_write_node_value(std::ofstream&, const std::string&, const CellValue<const DataType>&) const
+{}
+
+template <typename DataType>
+void
+VTKWriter::_write_cell_value(std::ofstream& os,
+                             const std::string& name,
+                             const CellValue<const DataType>& item_value) const
+{
+  os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\">\n";
+  for (CellId i = 0; i < item_value.size(); ++i) {
+    // The following '+' enforces integer output for char types
+    os << +item_value[i] << ' ';
+  }
+  os << "\n</DataArray>\n";
+}
+
+template <size_t N, typename DataType>
+void
+VTKWriter::_write_cell_value(std::ofstream& os,
+                             const std::string& name,
+                             const CellValue<const TinyVector<N, DataType>>& item_value) const
+{
+  os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
+     << "\">\n";
+  for (CellId i = 0; i < item_value.size(); ++i) {
+    for (size_t j = 0; j < N; ++j) {
+      // The following '+' enforces integer output for char types
+      os << +item_value[i][j] << ' ';
+    }
+  }
+  os << "\n</DataArray>\n";
+}
+
+template <typename DataType>
+void
+VTKWriter::_write_cell_value(std::ofstream&, const std::string&, const NodeValue<const DataType>&) const
+{}
+
+template <typename MeshType>
+void
+VTKWriter::_write(const std::shared_ptr<const MeshType>& mesh,
+                  const OutputNamedItemValueSet& output_named_item_value_set,
+                  double time,
+                  bool forced_output) const
+{
+  if (time == m_last_time)
+    return;   // output already performed
+  if ((time - m_last_time >= m_time_period) or forced_output) {
+    m_last_time = time;
+  } else {
+    return;
+  }
+
+  if (parallel::rank() == 0) {   // write PVTK file
+    std::ofstream fout(_getFilenamePVTU());
+    fout << "<?xml version=\"1.0\"?>\n";
+    fout << "<VTKFile type=\"PUnstructuredGrid\">\n";
+    fout << "<PUnstructuredGrid GhostLevel=\"0\">\n";
+
+    fout << "<PPoints>\n";
+    fout << "<PDataArray Name=\"Positions\" NumberOfComponents=\"3\" "
+            "type=\"Float64\"/>\n";
+    fout << "</PPoints>\n";
+
+    fout << "<PCells>\n";
+    fout << "<PDataArray type=\"Int32\" Name=\"connectivity\" "
+            "NumberOfComponents=\"1\"/>\n";
+    fout << "<PDataArray type=\"UInt32\" Name=\"offsets\" "
+            "NumberOfComponents=\"1\"/>\n";
+    fout << "<PDataArray type=\"Int8\" Name=\"types\" "
+            "NumberOfComponents=\"1\"/>\n";
+    for (const auto& [name, item_value_variant] : output_named_item_value_set) {
+      std::visit([&, name = name](auto&& item_value) { return this->_write_cell_pvtu(fout, name, item_value); },
+                 item_value_variant);
+    }
+    if constexpr (MeshType::Dimension == 3) {
+      fout << "<PDataArray type=\"Int64\" IdType=\"1\" Name=\"faces\"/>\n";
+      fout << "<PDataArray type=\"Int64\" IdType=\"1\" Name=\"faceoffsets\"/>\n";
+    }
+    fout << "</PCells>\n";
+
+    fout << "<PPointData>\n";
+    for (const auto& [name, item_value_variant] : output_named_item_value_set) {
+      std::visit([&, name = name](auto&& item_value) { return this->_write_node_pvtu(fout, name, item_value); },
+                 item_value_variant);
+    }
+    fout << "</PPointData>\n";
+
+    fout << "<PCellData>\n";
+    for (const auto& [name, item_value_variant] : output_named_item_value_set) {
+      std::visit([&, name = name](auto&& item_value) { return this->_write_cell_pvtu(fout, name, item_value); },
+                 item_value_variant);
+    }
+    fout << "</PCellData>\n";
+
+    for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
+      fout << "<Piece Source=\"" << _getFilenameVTU(i_rank) << "\"/>\n";
+    }
+    fout << "</PUnstructuredGrid>\n";
+    fout << "</VTKFile>\n";
+  }
+
+  {   // write VTK files
+    std::ofstream fout(_getFilenameVTU(parallel::rank()));
+    fout << "<?xml version=\"1.0\"?>\n";
+    fout << "<VTKFile type=\"UnstructuredGrid\">\n";
+    fout << "<UnstructuredGrid>\n";
+    fout << "<Piece NumberOfPoints=\"" << mesh->numberOfNodes() << "\" NumberOfCells=\"" << mesh->numberOfCells()
+         << "\">\n";
+    fout << "<CellData>\n";
+    for (const auto& [name, item_value_variant] : output_named_item_value_set) {
+      std::visit([&, name = name](auto&& item_value) { return this->_write_cell_value(fout, name, item_value); },
+                 item_value_variant);
+    }
+    fout << "</CellData>\n";
+    fout << "<PointData>\n";
+    for (const auto& [name, item_value_variant] : output_named_item_value_set) {
+      std::visit([&, name = name](auto&& item_value) { return this->_write_node_value(fout, name, item_value); },
+                 item_value_variant);
+    }
+    fout << "</PointData>\n";
+    fout << "<Points>\n";
+    {
+      using Rd                      = TinyVector<MeshType::Dimension>;
+      const NodeValue<const Rd>& xr = mesh->xr();
+      Array<TinyVector<3>> positions(mesh->numberOfNodes());
+      parallel_for(
+        mesh->numberOfNodes(), PUGS_LAMBDA(NodeId r) {
+          for (unsigned short i = 0; i < MeshType::Dimension; ++i) {
+            positions[r][i] = xr[r][i];
+          }
+          for (unsigned short i = MeshType::Dimension; i < 3; ++i) {
+            positions[r][i] = 0;
+          }
+        });
+      _write_array(fout, "Positions", positions);
+    }
+    fout << "</Points>\n";
+
+    fout << "<Cells>\n";
+    {
+      const auto& cell_to_node_matrix = mesh->connectivity().cellToNodeMatrix();
+
+      _write_array(fout, "connectivity", cell_to_node_matrix.entries());
+    }
+
+    {
+      const auto& cell_to_node_matrix = mesh->connectivity().cellToNodeMatrix();
+      Array<unsigned int> offsets(mesh->numberOfCells());
+      unsigned int offset = 0;
+      for (CellId j = 0; j < mesh->numberOfCells(); ++j) {
+        const auto& cell_nodes = cell_to_node_matrix[j];
+        offset += cell_nodes.size();
+        offsets[j] = offset;
+      }
+      _write_array(fout, "offsets", offsets);
+    }
+
+    {
+      Array<int8_t> types(mesh->numberOfCells());
+      const auto& cell_type           = mesh->connectivity().cellType();
+      const auto& cell_to_node_matrix = mesh->connectivity().cellToNodeMatrix();
+
+      parallel_for(
+        mesh->numberOfCells(), PUGS_LAMBDA(CellId j) {
+          switch (cell_type[j]) {
+          case CellType::Line: {
+            types[j] = 3;
+            break;
+          }
+          case CellType::Triangle: {
+            types[j] = 5;
+            break;
+          }
+          case CellType::Quadrangle: {
+            types[j] = 9;
+            break;
+          }
+          case CellType::Tetrahedron: {
+            types[j] = 10;
+            break;
+          }
+          case CellType::Pyramid: {
+            if (cell_to_node_matrix[j].size() == 5) {
+              types[j] = 14;   // quadrangle basis
+            } else {
+              types[j] = 41;   // polygonal basis
+            }
+            break;
+          }
+          case CellType::Prism: {
+            types[j] = 13;
+            break;
+          }
+          case CellType::Hexahedron: {
+            types[j] = 12;
+            break;
+          }
+          case CellType::Diamond: {
+            types[j] = 41;
+            break;
+          }
+          default: {
+            std::ostringstream os;
+            os << __FILE__ << ':' << __LINE__ << ": unknown cell type";
+            throw UnexpectedError(os.str());
+          }
+          }
+        });
+      _write_array(fout, "types", types);
+      if constexpr (MeshType::Dimension == 3) {
+        const bool has_general_polyhedron = [&] {
+          for (size_t i = 0; i < types.size(); ++i) {
+            if (types[i] == 41)
+              return true;
+          }
+          return false;
+        }();
+        if (has_general_polyhedron) {
+          const auto& cell_to_face_matrix   = mesh->connectivity().cellToFaceMatrix();
+          const auto& face_to_node_matrix   = mesh->connectivity().faceToNodeMatrix();
+          const auto& cell_face_is_reversed = mesh->connectivity().cellFaceIsReversed();
+
+          Array<size_t> faces_offsets(mesh->numberOfCells());
+          size_t next_offset = 0;
+          fout << "<DataArray type=\"Int64\" IdType=\"1\" Name=\"faces\">\n";
+          for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
+            const auto& cell_nodes = cell_to_node_matrix[cell_id];
+            std::unordered_map<size_t, size_t> node_id_to_local_number_in_cell;
+            for (size_t i_cell_node = 0; i_cell_node < cell_nodes.size(); ++i_cell_node) {
+              node_id_to_local_number_in_cell[cell_nodes[i_cell_node]] = i_cell_node;
+            }
+
+            const auto& cell_faces = cell_to_face_matrix[cell_id];
+            fout << cell_faces.size() << '\n';
+            next_offset++;
+            for (size_t i_cell_face = 0; i_cell_face < cell_faces.size(); ++i_cell_face) {
+              const FaceId& face_id  = cell_faces[i_cell_face];
+              const auto& face_nodes = face_to_node_matrix[face_id];
+              fout << face_nodes.size();
+              next_offset++;
+              Array<size_t> face_node_in_cell(face_nodes.size());
+
+              for (size_t i_face_node = 0; i_face_node < face_nodes.size(); ++i_face_node) {
+                const NodeId& node_id                  = face_nodes[i_face_node];
+                auto i_node_id_to_local_number_in_cell = node_id_to_local_number_in_cell.find(node_id);
+                Assert(i_node_id_to_local_number_in_cell != node_id_to_local_number_in_cell.end());
+                face_node_in_cell[i_face_node] = i_node_id_to_local_number_in_cell->second;
+              }
+
+              if (cell_face_is_reversed(cell_id, i_cell_face)) {
+                for (size_t i = 0; i < face_node_in_cell.size(); ++i) {
+                  fout << ' ' << face_node_in_cell[face_node_in_cell.size() - 1 - i];
+                }
+              } else {
+                for (size_t i = 0; i < face_node_in_cell.size(); ++i) {
+                  fout << ' ' << face_node_in_cell[i];
+                }
+              }
+
+              next_offset += face_nodes.size();
+
+              fout << '\n';
+            }
+            faces_offsets[cell_id] = next_offset;
+          }
+          fout << "</DataArray>\n";
+          fout << "<DataArray type=\"Int64\" IdType=\"1\" Name=\"faceoffsets\">\n";
+          for (size_t i_face_offsets = 0; i_face_offsets < faces_offsets.size(); ++i_face_offsets) {
+            fout << faces_offsets[i_face_offsets] << '\n';
+          }
+          fout << "</DataArray>\n";
+        }
+      }
+    }
+
+    fout << "</Cells>\n";
+    fout << "</Piece>\n";
+    fout << "</UnstructuredGrid>\n";
+    fout << "</VTKFile>\n";
+  }
+  m_file_number++;
+}
+
+void
+VTKWriter::write(const std::shared_ptr<const IMesh>& mesh,
+                 const OutputNamedItemValueSet& output_named_item_value_set,
+                 double time,
+                 bool forced_output) const
+{
+  switch (mesh->dimension()) {
+  case 1: {
+    this->_write(std::dynamic_pointer_cast<const Mesh<Connectivity<1>>>(mesh), output_named_item_value_set, time,
+                 forced_output);
+    break;
+  }
+  case 2: {
+    this->_write(std::dynamic_pointer_cast<const Mesh<Connectivity<2>>>(mesh), output_named_item_value_set, time,
+                 forced_output);
+    break;
+  }
+  case 3: {
+    this->_write(std::dynamic_pointer_cast<const Mesh<Connectivity<3>>>(mesh), output_named_item_value_set, time,
+                 forced_output);
+    break;
+  }
+  default: {
+    throw UnexpectedError("invalid mesh dimension");
+  }
+  }
+}
diff --git a/src/output/VTKWriter.hpp b/src/output/VTKWriter.hpp
index f3c202ffc51633d291d36150d662fe950023ef58..7661d0a27257a5ae1dbad96726d3e0263f0eaa21 100644
--- a/src/output/VTKWriter.hpp
+++ b/src/output/VTKWriter.hpp
@@ -1,449 +1,91 @@
 #ifndef VTK_WRITER_HPP
 #define VTK_WRITER_HPP
 
+#include <output/IWriter.hpp>
+
 #include <algebra/TinyVector.hpp>
-#include <mesh/CellType.hpp>
-#include <mesh/IConnectivity.hpp>
-#include <mesh/ItemValue.hpp>
 #include <output/OutputNamedItemValueSet.hpp>
-#include <utils/Exceptions.hpp>
-#include <utils/Messenger.hpp>
-#include <utils/PugsAssert.hpp>
 
-#include <fstream>
-#include <iomanip>
-#include <iostream>
-#include <sstream>
+class IMesh;
+
 #include <string>
-#include <unordered_map>
 
-class VTKWriter
+class VTKWriter : public IWriter
 {
  private:
   const std::string m_base_filename;
-  unsigned int m_file_number;
-  double m_last_time;
+  mutable unsigned int m_file_number;
+  mutable double m_last_time;
   const double m_time_period;
 
-  std::string
-  _getFilenamePVTU()
-  {
-    std::ostringstream sout;
-    sout << m_base_filename;
-    sout << '.' << std::setfill('0') << std::setw(4) << m_file_number << ".pvtu";
-    return sout.str();
-  }
-  std::string
-  _getFilenameVTU(int rank_number) const
-  {
-    std::ostringstream sout;
-    sout << m_base_filename;
-    if (parallel::size() > 1) {
-      sout << '-' << std::setfill('0') << std::setw(4) << rank_number;
-    }
-    sout << '.' << std::setfill('0') << std::setw(4) << m_file_number << ".vtu";
-    return sout.str();
-  }
+  std::string _getFilenamePVTU() const;
+
+  std::string _getFilenameVTU(int rank_number) const;
 
   template <typename DataType>
-  void
-  _write_node_pvtu(std::ofstream& os, const std::string& name, const NodeValue<const DataType>&)
-  {
-    os << "<PDataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\"/>\n";
-  }
+  void _write_node_pvtu(std::ofstream& os, const std::string& name, const NodeValue<const DataType>&) const;
 
   template <size_t N, typename DataType>
-  void
-  _write_node_pvtu(std::ofstream& os, const std::string& name, const NodeValue<const TinyVector<N, DataType>>&)
-  {
-    os << "<PDataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
-       << "\"/>\n";
-  }
+  void _write_node_pvtu(std::ofstream& os,
+                        const std::string& name,
+                        const NodeValue<const TinyVector<N, DataType>>&) const;
 
   template <typename DataType>
-  void
-  _write_node_pvtu(std::ofstream&, const std::string&, const CellValue<const DataType>&)
-  {}
+  void _write_node_pvtu(std::ofstream&, const std::string&, const CellValue<const DataType>&) const;
 
   template <typename DataType>
-  void
-  _write_cell_pvtu(std::ofstream& os, const std::string& name, const CellValue<const DataType>&)
-  {
-    os << "<PDataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\"/>\n";
-  }
+  void _write_cell_pvtu(std::ofstream& os, const std::string& name, const CellValue<const DataType>&) const;
 
   template <size_t N, typename DataType>
-  void
-  _write_cell_pvtu(std::ofstream& os, const std::string& name, const CellValue<const TinyVector<N, DataType>>&)
-  {
-    os << "<PDataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
-       << "\"/>\n";
-  }
+  void _write_cell_pvtu(std::ofstream& os,
+                        const std::string& name,
+                        const CellValue<const TinyVector<N, DataType>>&) const;
 
   template <typename DataType>
-  void
-  _write_cell_pvtu(std::ofstream&, const std::string&, const NodeValue<const DataType>&)
-  {}
+  void _write_cell_pvtu(std::ofstream&, const std::string&, const NodeValue<const DataType>&) const;
 
   template <typename DataType>
-  struct VTKType
-  {
-    inline const static std::string name = [] {
-      static_assert(std::is_arithmetic_v<DataType>, "invalid data type");
-
-      if constexpr (std::is_integral_v<DataType>) {
-        if constexpr (std::is_unsigned_v<DataType>) {
-          return "UInt" + std::to_string(sizeof(DataType) * 8);
-        } else {
-          return "UInt" + std::to_string(sizeof(DataType) * 8);
-        }
-      } else if constexpr (std::is_floating_point_v<DataType>) {
-        return "Float" + std::to_string(sizeof(DataType) * 8);
-      }
-    }();
-  };
+  struct VTKType;
 
   template <typename DataType>
-  void
-  _write_array(std::ofstream& os, const std::string& name, const Array<DataType>& item_value)
-  {
-    os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\">\n";
-    for (typename Array<DataType>::index_type i = 0; i < item_value.size(); ++i) {
-      // The following '+' enforces integer output for char types
-      os << +item_value[i] << ' ';
-    }
-    os << "\n</DataArray>\n";
-  }
+  void _write_array(std::ofstream& os, const std::string& name, const Array<DataType>& item_value) const;
 
   template <size_t N, typename DataType>
-  void
-  _write_array(std::ofstream& os, const std::string& name, const Array<TinyVector<N, DataType>>& item_value)
-  {
-    os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
-       << "\">\n";
-    for (typename Array<DataType>::index_type i = 0; i < item_value.size(); ++i) {
-      for (size_t j = 0; j < N; ++j) {
-        // The following '+' enforces integer output for char types
-        os << +item_value[i][j] << ' ';
-      }
-    }
-    os << "\n</DataArray>\n";
-  }
+  void _write_array(std::ofstream& os, const std::string& name, const Array<TinyVector<N, DataType>>& item_value) const;
 
   template <typename DataType>
-  void
-  _write_node_value(std::ofstream& os, const std::string& name, const NodeValue<const DataType>& item_value)
-  {
-    os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\">\n";
-    for (NodeId i = 0; i < item_value.size(); ++i) {
-      // The following '+' enforces integer output for char types
-      os << +item_value[i] << ' ';
-    }
-    os << "\n</DataArray>\n";
-  }
+  void _write_node_value(std::ofstream& os, const std::string& name, const NodeValue<const DataType>& item_value) const;
 
   template <size_t N, typename DataType>
-  void
-  _write_node_value(std::ofstream& os,
-                    const std::string& name,
-                    const NodeValue<const TinyVector<N, DataType>>& item_value)
-  {
-    os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
-       << "\">\n";
-    for (NodeId i = 0; i < item_value.size(); ++i) {
-      for (size_t j = 0; j < N; ++j) {
-        // The following '+' enforces integer output for char types
-        os << +item_value[i][j] << ' ';
-      }
-    }
-    os << "\n</DataArray>\n";
-  }
+  void _write_node_value(std::ofstream& os,
+                         const std::string& name,
+                         const NodeValue<const TinyVector<N, DataType>>& item_value) const;
 
   template <typename DataType>
-  void
-  _write_node_value(std::ofstream&, const std::string&, const CellValue<const DataType>&)
-  {}
+  void _write_node_value(std::ofstream&, const std::string&, const CellValue<const DataType>&) const;
 
   template <typename DataType>
-  void
-  _write_cell_value(std::ofstream& os, const std::string& name, const CellValue<const DataType>& item_value)
-  {
-    os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\">\n";
-    for (CellId i = 0; i < item_value.size(); ++i) {
-      // The following '+' enforces integer output for char types
-      os << +item_value[i] << ' ';
-    }
-    os << "\n</DataArray>\n";
-  }
+  void _write_cell_value(std::ofstream& os, const std::string& name, const CellValue<const DataType>& item_value) const;
 
   template <size_t N, typename DataType>
-  void
-  _write_cell_value(std::ofstream& os,
-                    const std::string& name,
-                    const CellValue<const TinyVector<N, DataType>>& item_value)
-  {
-    os << "<DataArray type=\"" << VTKType<DataType>::name << "\" Name=\"" << name << "\" NumberOfComponents=\"" << N
-       << "\">\n";
-    for (CellId i = 0; i < item_value.size(); ++i) {
-      for (size_t j = 0; j < N; ++j) {
-        // The following '+' enforces integer output for char types
-        os << +item_value[i][j] << ' ';
-      }
-    }
-    os << "\n</DataArray>\n";
-  }
+  void _write_cell_value(std::ofstream& os,
+                         const std::string& name,
+                         const CellValue<const TinyVector<N, DataType>>& item_value) const;
 
   template <typename DataType>
-  void
-  _write_cell_value(std::ofstream&, const std::string&, const NodeValue<const DataType>&)
-  {}
+  void _write_cell_value(std::ofstream&, const std::string&, const NodeValue<const DataType>&) const;
 
- public:
   template <typename MeshType>
-  void
-  write(const std::shared_ptr<const MeshType>& mesh,
-        const OutputNamedItemValueSet& output_named_item_value_set,
-        double time,
-        bool forced_output = false)
-  {
-    if (time == m_last_time)
-      return;   // output already performed
-    if ((time - m_last_time >= m_time_period) or forced_output) {
-      m_last_time = time;
-    } else {
-      return;
-    }
-
-    if (parallel::rank() == 0) {   // write PVTK file
-      std::ofstream fout(_getFilenamePVTU());
-      fout << "<?xml version=\"1.0\"?>\n";
-      fout << "<VTKFile type=\"PUnstructuredGrid\">\n";
-      fout << "<PUnstructuredGrid GhostLevel=\"0\">\n";
-
-      fout << "<PPoints>\n";
-      fout << "<PDataArray Name=\"Positions\" NumberOfComponents=\"3\" "
-              "type=\"Float64\"/>\n";
-      fout << "</PPoints>\n";
-
-      fout << "<PCells>\n";
-      fout << "<PDataArray type=\"Int32\" Name=\"connectivity\" "
-              "NumberOfComponents=\"1\"/>\n";
-      fout << "<PDataArray type=\"UInt32\" Name=\"offsets\" "
-              "NumberOfComponents=\"1\"/>\n";
-      fout << "<PDataArray type=\"Int8\" Name=\"types\" "
-              "NumberOfComponents=\"1\"/>\n";
-      for (const auto& [name, item_value_variant] : output_named_item_value_set) {
-        std::visit([&, name = name](auto&& item_value) { return this->_write_cell_pvtu(fout, name, item_value); },
-                   item_value_variant);
-      }
-      if constexpr (MeshType::Dimension == 3) {
-        fout << "<PDataArray type=\"Int64\" IdType=\"1\" Name=\"faces\"/>\n";
-        fout << "<PDataArray type=\"Int64\" IdType=\"1\" Name=\"faceoffsets\"/>\n";
-      }
-      fout << "</PCells>\n";
-
-      fout << "<PPointData>\n";
-      for (const auto& [name, item_value_variant] : output_named_item_value_set) {
-        std::visit([&, name = name](auto&& item_value) { return this->_write_node_pvtu(fout, name, item_value); },
-                   item_value_variant);
-      }
-      fout << "</PPointData>\n";
-
-      fout << "<PCellData>\n";
-      for (const auto& [name, item_value_variant] : output_named_item_value_set) {
-        std::visit([&, name = name](auto&& item_value) { return this->_write_cell_pvtu(fout, name, item_value); },
-                   item_value_variant);
-      }
-      fout << "</PCellData>\n";
+  void _write(const std::shared_ptr<const MeshType>& mesh,
+              const OutputNamedItemValueSet& output_named_item_value_set,
+              double time,
+              bool forced_output = false) const;
 
-      for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
-        fout << "<Piece Source=\"" << _getFilenameVTU(i_rank) << "\"/>\n";
-      }
-      fout << "</PUnstructuredGrid>\n";
-      fout << "</VTKFile>\n";
-    }
-
-    {   // write VTK files
-      std::ofstream fout(_getFilenameVTU(parallel::rank()));
-      fout << "<?xml version=\"1.0\"?>\n";
-      fout << "<VTKFile type=\"UnstructuredGrid\">\n";
-      fout << "<UnstructuredGrid>\n";
-      fout << "<Piece NumberOfPoints=\"" << mesh->numberOfNodes() << "\" NumberOfCells=\"" << mesh->numberOfCells()
-           << "\">\n";
-      fout << "<CellData>\n";
-      for (const auto& [name, item_value_variant] : output_named_item_value_set) {
-        std::visit([&, name = name](auto&& item_value) { return this->_write_cell_value(fout, name, item_value); },
-                   item_value_variant);
-      }
-      fout << "</CellData>\n";
-      fout << "<PointData>\n";
-      for (const auto& [name, item_value_variant] : output_named_item_value_set) {
-        std::visit([&, name = name](auto&& item_value) { return this->_write_node_value(fout, name, item_value); },
-                   item_value_variant);
-      }
-      fout << "</PointData>\n";
-      fout << "<Points>\n";
-      {
-        using Rd                      = TinyVector<MeshType::Dimension>;
-        const NodeValue<const Rd>& xr = mesh->xr();
-        Array<TinyVector<3>> positions(mesh->numberOfNodes());
-        parallel_for(
-          mesh->numberOfNodes(), PUGS_LAMBDA(NodeId r) {
-            for (unsigned short i = 0; i < MeshType::Dimension; ++i) {
-              positions[r][i] = xr[r][i];
-            }
-            for (unsigned short i = MeshType::Dimension; i < 3; ++i) {
-              positions[r][i] = 0;
-            }
-          });
-        _write_array(fout, "Positions", positions);
-      }
-      fout << "</Points>\n";
-
-      fout << "<Cells>\n";
-      {
-        const auto& cell_to_node_matrix = mesh->connectivity().cellToNodeMatrix();
-
-        _write_array(fout, "connectivity", cell_to_node_matrix.entries());
-      }
-
-      {
-        const auto& cell_to_node_matrix = mesh->connectivity().cellToNodeMatrix();
-        Array<unsigned int> offsets(mesh->numberOfCells());
-        unsigned int offset = 0;
-        for (CellId j = 0; j < mesh->numberOfCells(); ++j) {
-          const auto& cell_nodes = cell_to_node_matrix[j];
-          offset += cell_nodes.size();
-          offsets[j] = offset;
-        }
-        _write_array(fout, "offsets", offsets);
-      }
-
-      {
-        Array<int8_t> types(mesh->numberOfCells());
-        const auto& cell_type           = mesh->connectivity().cellType();
-        const auto& cell_to_node_matrix = mesh->connectivity().cellToNodeMatrix();
-
-        parallel_for(
-          mesh->numberOfCells(), PUGS_LAMBDA(CellId j) {
-            switch (cell_type[j]) {
-            case CellType::Line: {
-              types[j] = 3;
-              break;
-            }
-            case CellType::Triangle: {
-              types[j] = 5;
-              break;
-            }
-            case CellType::Quadrangle: {
-              types[j] = 9;
-              break;
-            }
-            case CellType::Tetrahedron: {
-              types[j] = 10;
-              break;
-            }
-            case CellType::Pyramid: {
-              if (cell_to_node_matrix[j].size() == 5) {
-                types[j] = 14;   // quadrangle basis
-              } else {
-                types[j] = 41;   // polygonal basis
-              }
-              break;
-            }
-            case CellType::Prism: {
-              types[j] = 13;
-              break;
-            }
-            case CellType::Hexahedron: {
-              types[j] = 12;
-              break;
-            }
-            case CellType::Diamond: {
-              types[j] = 41;
-              break;
-            }
-            default: {
-              std::ostringstream os;
-              os << __FILE__ << ':' << __LINE__ << ": unknown cell type";
-              throw UnexpectedError(os.str());
-            }
-            }
-          });
-        _write_array(fout, "types", types);
-        if constexpr (MeshType::Dimension == 3) {
-          const bool has_general_polyhedron = [&] {
-            for (size_t i = 0; i < types.size(); ++i) {
-              if (types[i] == 41)
-                return true;
-            }
-            return false;
-          }();
-          if (has_general_polyhedron) {
-            const auto& cell_to_face_matrix   = mesh->connectivity().cellToFaceMatrix();
-            const auto& face_to_node_matrix   = mesh->connectivity().faceToNodeMatrix();
-            const auto& cell_face_is_reversed = mesh->connectivity().cellFaceIsReversed();
-
-            Array<size_t> faces_offsets(mesh->numberOfCells());
-            size_t next_offset = 0;
-            fout << "<DataArray type=\"Int64\" IdType=\"1\" Name=\"faces\">\n";
-            for (CellId cell_id = 0; cell_id < mesh->numberOfCells(); ++cell_id) {
-              const auto& cell_nodes = cell_to_node_matrix[cell_id];
-              std::unordered_map<size_t, size_t> node_id_to_local_number_in_cell;
-              for (size_t i_cell_node = 0; i_cell_node < cell_nodes.size(); ++i_cell_node) {
-                node_id_to_local_number_in_cell[cell_nodes[i_cell_node]] = i_cell_node;
-              }
-
-              const auto& cell_faces = cell_to_face_matrix[cell_id];
-              fout << cell_faces.size() << '\n';
-              next_offset++;
-              for (size_t i_cell_face = 0; i_cell_face < cell_faces.size(); ++i_cell_face) {
-                const FaceId& face_id  = cell_faces[i_cell_face];
-                const auto& face_nodes = face_to_node_matrix[face_id];
-                fout << face_nodes.size();
-                next_offset++;
-                Array<size_t> face_node_in_cell(face_nodes.size());
-
-                for (size_t i_face_node = 0; i_face_node < face_nodes.size(); ++i_face_node) {
-                  const NodeId& node_id                  = face_nodes[i_face_node];
-                  auto i_node_id_to_local_number_in_cell = node_id_to_local_number_in_cell.find(node_id);
-                  Assert(i_node_id_to_local_number_in_cell != node_id_to_local_number_in_cell.end());
-                  face_node_in_cell[i_face_node] = i_node_id_to_local_number_in_cell->second;
-                }
-
-                if (cell_face_is_reversed(cell_id, i_cell_face)) {
-                  for (size_t i = 0; i < face_node_in_cell.size(); ++i) {
-                    fout << ' ' << face_node_in_cell[face_node_in_cell.size() - 1 - i];
-                  }
-                } else {
-                  for (size_t i = 0; i < face_node_in_cell.size(); ++i) {
-                    fout << ' ' << face_node_in_cell[i];
-                  }
-                }
-
-                next_offset += face_nodes.size();
-
-                fout << '\n';
-              }
-              faces_offsets[cell_id] = next_offset;
-            }
-            fout << "</DataArray>\n";
-            fout << "<DataArray type=\"Int64\" IdType=\"1\" Name=\"faceoffsets\">\n";
-            for (size_t i_face_offsets = 0; i_face_offsets < faces_offsets.size(); ++i_face_offsets) {
-              fout << faces_offsets[i_face_offsets] << '\n';
-            }
-            fout << "</DataArray>\n";
-          }
-        }
-      }
-
-      fout << "</Cells>\n";
-      fout << "</Piece>\n";
-      fout << "</UnstructuredGrid>\n";
-      fout << "</VTKFile>\n";
-    }
-    m_file_number++;
-  }
+ public:
+  void write(const std::shared_ptr<const IMesh>& mesh,
+             const OutputNamedItemValueSet& output_named_item_value_set,
+             double time,
+             bool forced_output = false) const final;
 
   VTKWriter(const std::string& base_filename, const double time_period)
     : m_base_filename(base_filename),
diff --git a/src/scheme/DiscreteFunctionDescriptorP0.hpp b/src/scheme/DiscreteFunctionDescriptorP0.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..97bfbbf729d4224dbb7af41bf61d489c5a3080cb
--- /dev/null
+++ b/src/scheme/DiscreteFunctionDescriptorP0.hpp
@@ -0,0 +1,24 @@
+#ifndef DISCRETE_FUNCTION_DESCRIPTOR_P_0HPP
+#define DISCRETE_FUNCTION_DESCRIPTOR_P_0HPP
+
+#include <scheme/IDiscreteFunctionDescriptor.hpp>
+
+class DiscreteFunctionDescriptorP0 : public IDiscreteFunctionDescriptor
+{
+ public:
+  DiscreteFunctionType
+  type() const final
+  {
+    return DiscreteFunctionType::P0;
+  }
+
+  DiscreteFunctionDescriptorP0() noexcept = default;
+
+  DiscreteFunctionDescriptorP0(const DiscreteFunctionDescriptorP0&) = default;
+
+  DiscreteFunctionDescriptorP0(DiscreteFunctionDescriptorP0&&) noexcept = default;
+
+  ~DiscreteFunctionDescriptorP0() noexcept = default;
+};
+
+#endif   // DISCRETE_FUNCTION_DESCRIPTOR_P_0HPP
diff --git a/src/scheme/DiscreteFunctionInterpoler.cpp b/src/scheme/DiscreteFunctionInterpoler.cpp
index 60b481cdeffcbae4571c5a297a5521099d01686d..7ac4919ab77bd0be973925a88b59a94cf7dc57d5 100644
--- a/src/scheme/DiscreteFunctionInterpoler.cpp
+++ b/src/scheme/DiscreteFunctionInterpoler.cpp
@@ -1,50 +1,8 @@
 #include <scheme/DiscreteFunctionInterpoler.hpp>
 
-#include <language/utils/InterpolateItemValue.hpp>
-#include <mesh/Connectivity.hpp>
-#include <mesh/Mesh.hpp>
-#include <mesh/MeshData.hpp>
-#include <mesh/MeshDataManager.hpp>
+#include <scheme/DiscreteFunctionP0.hpp>
 #include <utils/Exceptions.hpp>
 
-template <size_t Dimension, typename DataType>
-class DiscreteFunctionP0 : public IDiscreteFunction
-{
- private:
-  using MeshType = Mesh<Connectivity<Dimension>>;
-
-  std::shared_ptr<const MeshType> m_mesh;
-  CellValue<DataType> m_cell_values;
-
- public:
-  std::shared_ptr<const IMesh>
-  mesh() const
-  {
-    return m_mesh;
-  }
-
-  PUGS_FORCEINLINE
-  DataType&
-  operator[](const CellId& cell_id) const noexcept(NO_ASSERT)
-  {
-    return m_cell_values[cell_id];
-  }
-
-  DiscreteFunctionP0(const std::shared_ptr<const MeshType>& mesh, const FunctionSymbolId& function_id) : m_mesh(mesh)
-  {
-    using MeshDataType      = MeshData<Dimension>;
-    MeshDataType& mesh_data = MeshDataManager::instance().getMeshData(*mesh);
-    m_cell_values =
-      InterpolateItemValue<DataType(TinyVector<Dimension>)>::template interpolate<ItemType::cell>(function_id,
-                                                                                                  mesh_data.xj());
-  }
-
-  DiscreteFunctionP0(const DiscreteFunctionP0&) noexcept = default;
-  DiscreteFunctionP0(DiscreteFunctionP0&&) noexcept      = default;
-
-  ~DiscreteFunctionP0() = default;
-};
-
 std::shared_ptr<IDiscreteFunction>
 DiscreteFunctionInterpoler::interpolate() const
 {
diff --git a/src/scheme/DiscreteFunctionP0.hpp b/src/scheme/DiscreteFunctionP0.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4fb0fcc980bc09a71c8668ad16e069ab8c6d0a2d
--- /dev/null
+++ b/src/scheme/DiscreteFunctionP0.hpp
@@ -0,0 +1,65 @@
+#ifndef DISCRETE_FUNCTION_P0_HPP
+#define DISCRETE_FUNCTION_P0_HPP
+
+#include <scheme/IDiscreteFunction.hpp>
+
+#include <language/utils/InterpolateItemValue.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshData.hpp>
+#include <mesh/MeshDataManager.hpp>
+#include <scheme/DiscreteFunctionDescriptorP0.hpp>
+
+template <size_t Dimension, typename DataType>
+class DiscreteFunctionP0 : public IDiscreteFunction
+{
+ private:
+  using MeshType = Mesh<Connectivity<Dimension>>;
+
+  std::shared_ptr<const MeshType> m_mesh;
+  CellValue<DataType> m_cell_values;
+
+  DiscreteFunctionDescriptorP0 m_discrete_function_descriptor;
+
+ public:
+  CellValue<DataType>
+  cellValues() const
+  {
+    return m_cell_values;
+  }
+
+  std::shared_ptr<const IMesh>
+  mesh() const
+  {
+    return m_mesh;
+  }
+
+  const IDiscreteFunctionDescriptor&
+  descriptor() const final
+  {
+    return m_discrete_function_descriptor;
+  }
+
+  PUGS_FORCEINLINE
+  DataType&
+  operator[](const CellId& cell_id) const noexcept(NO_ASSERT)
+  {
+    return m_cell_values[cell_id];
+  }
+
+  DiscreteFunctionP0(const std::shared_ptr<const MeshType>& mesh, const FunctionSymbolId& function_id) : m_mesh(mesh)
+  {
+    using MeshDataType      = MeshData<Dimension>;
+    MeshDataType& mesh_data = MeshDataManager::instance().getMeshData(*mesh);
+    m_cell_values =
+      InterpolateItemValue<DataType(TinyVector<Dimension>)>::template interpolate<ItemType::cell>(function_id,
+                                                                                                  mesh_data.xj());
+  }
+
+  DiscreteFunctionP0(const DiscreteFunctionP0&) noexcept = default;
+  DiscreteFunctionP0(DiscreteFunctionP0&&) noexcept      = default;
+
+  ~DiscreteFunctionP0() = default;
+};
+
+#endif   // DISCRETE_FUNCTION_P0_HPP
diff --git a/src/scheme/IDiscreteFunction.hpp b/src/scheme/IDiscreteFunction.hpp
index eafea0e961c03b4acae7032bf96c3334e8048810..7a7deb08747f74b28e65458a54fe5a775db86b43 100644
--- a/src/scheme/IDiscreteFunction.hpp
+++ b/src/scheme/IDiscreteFunction.hpp
@@ -1,16 +1,16 @@
 #ifndef I_DISCRETE_FUNCTION_HPP
 #define I_DISCRETE_FUNCTION_HPP
 
-#include <scheme/DiscreteFunctionType.hpp>
-
 #include <memory>
 
 class IMesh;
+class IDiscreteFunctionDescriptor;
 
 class IDiscreteFunction
 {
  public:
-  virtual std::shared_ptr<const IMesh> mesh() const = 0;
+  virtual std::shared_ptr<const IMesh> mesh() const             = 0;
+  virtual const IDiscreteFunctionDescriptor& descriptor() const = 0;
 
   IDiscreteFunction() = default;
 
diff --git a/src/scheme/IDiscreteFunctionDescriptor.hpp b/src/scheme/IDiscreteFunctionDescriptor.hpp
index fcd9fdd1aa8e07973aa9d3503424035d6ce9c646..19d411507e087c0e3262f8e712aa393d96796b20 100644
--- a/src/scheme/IDiscreteFunctionDescriptor.hpp
+++ b/src/scheme/IDiscreteFunctionDescriptor.hpp
@@ -1,6 +1,8 @@
 #ifndef I_DISCRETE_FUNCTION_DESCRIPTOR_HPP
 #define I_DISCRETE_FUNCTION_DESCRIPTOR_HPP
 
+#include <scheme/DiscreteFunctionType.hpp>
+
 class IDiscreteFunctionDescriptor
 {
  public:
@@ -15,22 +17,4 @@ class IDiscreteFunctionDescriptor
   virtual ~IDiscreteFunctionDescriptor() noexcept = default;
 };
 
-class DiscreteFunctionDescriptorP0 : public IDiscreteFunctionDescriptor
-{
- public:
-  DiscreteFunctionType
-  type() const final
-  {
-    return DiscreteFunctionType::P0;
-  }
-
-  DiscreteFunctionDescriptorP0() noexcept = default;
-
-  DiscreteFunctionDescriptorP0(const DiscreteFunctionDescriptorP0& other) = default;
-
-  DiscreteFunctionDescriptorP0(DiscreteFunctionDescriptorP0&& other) noexcept = default;
-
-  ~DiscreteFunctionDescriptorP0() noexcept = default;
-};
-
 #endif   // I_DISCRETE_FUNCTION_DESCRIPTOR_HPP
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 06518e70d327fc29760ad6ac2c38142bd2afa138..c4413ddc7c6f06665b4efb7b091548fb26140fab 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -114,6 +114,7 @@ target_link_libraries (unit_tests
   PugsMesh
   PugsAlgebra
   PugsScheme
+  PugsOutput
   PugsUtils
   kokkos
   ${PARMETIS_LIBRARIES}
@@ -137,6 +138,7 @@ target_link_libraries (mpi_unit_tests
   PugsUtils
   PugsLanguageUtils
   PugsScheme
+  PugsOutput
   PugsUtils
   PugsAlgebra
   PugsMesh