diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000000000000000000000000000000000000..60cf9d536e56d8675468717814ffb5698298cf1e
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,11 @@
+---
+# https://clang.llvm.org/docs/ClangFormatStyleOptions.html#
+# the "Visual Studio" style is similar to:
+BasedOnStyle: LLVM
+UseTab: Never
+IndentWidth: 4
+BreakBeforeBraces: Allman
+AccessModifierOffset: -4
+SortIncludes: false
+ColumnLimit: 0
+...
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..de6044b96d078e062de7f658aef508ff77461564
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,8 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
diff --git a/.gitignore b/.gitignore
index 109a5c8905a52423e8e2214a2c8b8621554f8cf6..4bb53200ef03cc1a3c0738d0797d1a5a2034923e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ __pycache__/
 *.zip
 *.7z
 *.rar
+build/
diff --git a/cxxfem/CMakeLists.txt b/cxxfem/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..be50e6426998b5bab4d01a37e76b8461ff3965db
--- /dev/null
+++ b/cxxfem/CMakeLists.txt
@@ -0,0 +1,128 @@
+PROJECT(FEM CXX)
+CMAKE_MINIMUM_REQUIRED(VERSION 3.14)
+
+OPTION(FEM_USE_MKL "Use Pardiso linear solver from MKL" ON)
+
+
+# ------------------------------------------------------------------------------
+# Find libraries and setup compiler
+# ------------------------------------------------------------------------------
+
+# put executables/libraries together in bin/ folder
+SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin CACHE PATH
+                        "Single output directory for building all libraries.")
+SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin CACHE PATH
+                        "Single output directory for building all executables.")
+MARK_AS_ADVANCED(CMAKE_LIBRARY_OUTPUT_DIRECTORY 
+                 CMAKE_RUNTIME_OUTPUT_DIRECTORY)
+
+# build type is "" by default in Linux
+IF(NOT CMAKE_BUILD_TYPE)
+    SET( CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE)
+ENDIF()
+
+IF(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
+    SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")  # parallel build with MSVC
+ENDIF()
+
+# enable C++11
+SET(CMAKE_CXX_STANDARD 17)
+SET(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+IF(APPLE)
+    # on macOS, do not give priority to frameworks/apps
+    SET(CMAKE_FIND_APPBUNDLE LAST)
+    SET(CMAKE_FIND_FRAMEWORK LAST)
+ENDIF()
+
+# find gmsh-sdk
+# gmsh.h
+FIND_PATH(GMSH_INCLUDE_DIRS NAMES "gmsh.h")
+MESSAGE(STATUS "GMSH_INCLUDE_DIRS=" ${GMSH_INCLUDE_DIRS})
+if(NOT GMSH_INCLUDE_DIRS)
+    MESSAGE(FATAL_ERROR "gmsh.h not found!")
+ENDIF()
+INCLUDE_DIRECTORIES(${GMSH_INCLUDE_DIRS})
+
+# libgmsh.so
+FIND_LIBRARY(GMSH_LIBRARIES gmsh)
+MESSAGE(STATUS "GMSH_LIBRARIES=" ${GMSH_LIBRARIES})
+IF(NOT GMSH_LIBRARIES)
+    MESSAGE(FATAL_ERROR "gmsh library not found!")
+ENDIF()
+
+# find MKL
+
+IF(FEM_USE_MKL)
+    # header (mkl.h) searched using INCLUDE or MKLROOT
+    FIND_PATH(MKL_INCLUDE_DIRS NAMES "mkl.h" PATHS "$ENV{MKLROOT}/include")
+    MESSAGE(STATUS "MKL_INCLUDE_DIRS=${MKL_INCLUDE_DIRS}")
+    # library (mkl_rt.so) searched using LIBRARY_PATH (Linux/macOS) or LIB (windows)
+    FIND_LIBRARY(MKL_LIBRARIES mkl_rt PATHS ENV LIBRARY_PATH)
+    IF(MINGW)
+        FIND_LIBRARY(MKL_LIB1 mkl_intel_lp64_dll PATHS ENV LIBRARY_PATH)
+        FIND_LIBRARY(MKL_LIB2 mkl_intel_thread_dll PATHS ENV LIBRARY_PATH)
+        FIND_LIBRARY(MKL_LIB3 mkl_core_dll PATHS ENV LIBRARY_PATH)
+        FIND_LIBRARY(MKL_LIB4 libiomp5md PATHS ENV LIBRARY_PATH)
+        # MESSAGE(STATUS "MKL_LIB1=${MKL_LIB1}")
+        # MESSAGE(STATUS "MKL_LIB2=${MKL_LIB2}")
+        # MESSAGE(STATUS "MKL_LIB3=${MKL_LIB3}")
+        # MESSAGE(STATUS "MKL_LIB4=${MKL_LIB4}")
+        SET(MKL_LIBRARIES ${MKL_LIB1} ${MKL_LIB2} ${MKL_LIB3} ${MKL_LIB4})
+    ENDIF()
+    MESSAGE(STATUS "MKL_LIBRARIES=${MKL_LIBRARIES}")
+ENDIF()
+
+# find Eigen
+FIND_PATH(EIGEN_INCLUDE_DIRS "Eigen/Dense" 
+          PATHS "${PROJECT_SOURCE_DIR}/lib/eigen" "/usr/include/eigen3")
+MESSAGE(STATUS "EIGEN_INCLUDE_DIRS=" ${EIGEN_INCLUDE_DIRS})
+IF(NOT EIGEN_INCLUDE_DIRS)
+    MESSAGE(FATAL_ERROR "Eigen include dir not found!")
+ENDIF()
+INCLUDE_DIRECTORIES(${EIGEN_INCLUDE_DIRS})
+
+# increase warning level
+INCLUDE(CheckCXXCompilerFlag)
+CHECK_CXX_COMPILER_FLAG("-Wall" WALL)
+IF(WALL AND NOT MSVC)
+    ADD_COMPILE_OPTIONS(-Wall)
+ENDIF()
+
+# OpenMP --
+FIND_PACKAGE(OpenMP)
+IF(OPENMP_FOUND)
+    MESSAGE ("OpenMP enabled (CXX_FLAGS=${OpenMP_CXX_FLAGS})")
+ELSE()
+    MESSAGE ("OpenMP disabled.")
+ENDIF()
+
+# Python --
+# use Python3_ROOT_DIR if wrong python found (e.g. anaconda)
+FIND_PACKAGE(Python3 COMPONENTS Interpreter Development REQUIRED)
+MESSAGE(STATUS "Python3_EXECUTABLE=${Python3_EXECUTABLE}")
+MESSAGE(STATUS "Python3_LIBRARIES=${Python3_LIBRARIES}")
+MESSAGE(STATUS "Python3_INCLUDE_DIRS=${Python3_INCLUDE_DIRS}")
+MESSAGE(STATUS "Python3_VERSION=${Python3_VERSION}")
+
+# SWIG --
+FIND_PACKAGE(SWIG REQUIRED)
+IF(CMAKE_GENERATOR MATCHES "Visual Studio") # not MSVC because of nmake & jom
+    SET(CMAKE_SWIG_OUTDIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$(Configuration)/")
+ELSE()
+    SET(CMAKE_SWIG_OUTDIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
+ENDIF()
+SET(CMAKE_SWIG_FLAGS -py3 -O)
+INCLUDE(${SWIG_USE_FILE})
+
+
+
+
+# ------------------------------------------------------------------------------
+CONFIGURE_FILE(config.h.in config.h)
+INCLUDE_DIRECTORIES(${PROJECT_BINARY_DIR})
+
+ADD_SUBDIRECTORY( src )
+ADD_SUBDIRECTORY( exe )
+ADD_SUBDIRECTORY( _src )
+
diff --git a/cxxfem/__init__.py b/cxxfem/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a3577e00eb47acd97f625cc658016a766d30094
--- /dev/null
+++ b/cxxfem/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# fem MODULE initialization file
+
+from femi import *
diff --git a/cxxfem/_src/CMakeLists.txt b/cxxfem/_src/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f3549cc40fadd9fdb29ddb5bf1dda8023b028f5f
--- /dev/null
+++ b/cxxfem/_src/CMakeLists.txt
@@ -0,0 +1,17 @@
+# CMake input file of the SWIG wrapper around "fem.so"
+#   Uses new policy CMP0078 (UseSWIG generates standard target names)
+#   Requires CMake >=3.13
+
+FILE(GLOB SRCS *.h *.cpp *.inl *.swg)
+FILE(GLOB ISRCS *.i)
+
+SET_SOURCE_FILES_PROPERTIES(${ISRCS} PROPERTIES CPLUSPLUS ON)
+SET(CMAKE_SWIG_FLAGS "-interface" "_femi") # avoids "import _module_d" with MSVC/Debug
+SWIG_ADD_LIBRARY(femi LANGUAGE python SOURCES ${ISRCS} ${SRCS})
+SET_PROPERTY(TARGET femi PROPERTY SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON)
+# MACRO_DebugPostfix(femi)
+
+TARGET_INCLUDE_DIRECTORIES(femi PRIVATE 
+    ${PROJECT_SOURCE_DIR}/src
+    ${Python3_INCLUDE_DIRS})
+TARGET_LINK_LIBRARIES(femi PRIVATE fem ${Python3_LIBRARIES})
diff --git a/cxxfem/_src/femi.i b/cxxfem/_src/femi.i
new file mode 100644
index 0000000000000000000000000000000000000000..41956c6d8aee6d5576b4b3205edbfaad6e8b17b0
--- /dev/null
+++ b/cxxfem/_src/femi.i
@@ -0,0 +1,82 @@
+// SWIG input file of the 'fem' module
+
+%feature("autodoc","1");
+
+%module(docstring=
+"'femi' module: FEM
+(c) ULiège - A&M",
+threads="1"
+) femi
+%{
+
+#include "fem.h"
+#include "femDirichlet.h"
+#include "femNeumann.h"
+#include "femProblem.h"
+#include "femSolver.h"
+#include "femPost.h"
+#include "femMedium.h"
+#include "femExtractor.h"
+
+#include <string>
+// #include <sstream>
+
+%}
+
+%include "std_string.i"
+%include "std_vector.i"
+%include "std_map.i"
+
+// Instantiate templates
+namespace std {
+   %template(StringDoubleMap)  std::map<std::string, double>;
+   %template(StringVector)  std::vector<std::string>;
+   %template(DoubleVector)  std::vector<double>;
+}
+
+// --------- EXCEPTIONS ---------------
+
+%include "exception.i"
+
+// from: http://swig.10945.n7.nabble.com/Trapping-Swig-DirectorException-td6013.html
+// le code suivant permet de voir la call stack dans les appels C++ => python
+
+%{ 
+   static void handle_exception(void) { 
+     try { 
+       throw; 
+     } catch (std::exception &e) { 
+        std::stringstream txt; 
+        txt << e.what(); // << ' ' << typeid(e).name();
+        PyErr_SetString(PyExc_RuntimeError, e.what()); 
+     } 
+     catch(...) 
+     {
+        PyErr_SetString(PyExc_RuntimeError, "Unknown C++ Runtime Error");
+     } 
+   } 
+%} 
+
+%exception { 
+   try { 
+     $action 
+   } catch (...) { 
+     // Note that if a director method failed, the Python error indicator 
+     // already contains full details of the exception, and it will be 
+     // reraised when we go to SWIG_fail; so no need to convert the C++ 
+     // exception back to a Python one 
+     if (!PyErr_Occurred()) { 
+       handle_exception(); 
+     } 
+     SWIG_fail; 
+   } 
+} 
+
+// ----------- FEM CLASSES ---------------
+%include "fem.h"
+%include "femDirichlet.h"
+%include "femNeumann.h"
+%include "femSolver.h"
+%include "femProblem.h"
+%include "femPost.h"
+%include "femMedium.h"
diff --git a/cxxfem/config.h.in b/cxxfem/config.h.in
new file mode 100644
index 0000000000000000000000000000000000000000..715648e7ecd248f2e16e6e270a85cd48e9530a8e
--- /dev/null
+++ b/cxxfem/config.h.in
@@ -0,0 +1,2 @@
+#cmakedefine PROJECT_SOURCE_DIR "@PROJECT_SOURCE_DIR@"
+#cmakedefine FEM_USE_MKL
diff --git a/cxxfem/exe/CMakeLists.txt b/cxxfem/exe/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e02d9e4445ceb4f395597b9f89c61271a919d6fd
--- /dev/null
+++ b/cxxfem/exe/CMakeLists.txt
@@ -0,0 +1,9 @@
+
+# compile each .cpp into an exe
+FILE(GLOB SRCS *.cpp)
+FOREACH(SFILE ${SRCS})
+    GET_FILENAME_COMPONENT(PROG_NAME ${SFILE} NAME_WE)
+    ADD_EXECUTABLE(${PROG_NAME} ${SFILE})
+    TARGET_LINK_LIBRARIES(${PROG_NAME} PUBLIC fem)
+    TARGET_INCLUDE_DIRECTORIES(fem PUBLIC ${PROJECT_SOURCE_DIR}/exe)
+ENDFOREACH()
diff --git a/cxxfem/exe/beam.cpp b/cxxfem/exe/beam.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..70bab05cc7708dfbdf936bcc8bf5d4fcf629454d
--- /dev/null
+++ b/cxxfem/exe/beam.cpp
@@ -0,0 +1,66 @@
+// build & run:
+//      win:
+//          set OMP_NUM_THREADS=10
+//          cmake .. && make -j 10 && bin\square.exe
+//      win debug:
+//          cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j 10 && bin\square.exe
+//
+//      linux:
+//          export OMP_NUM_THREADS=10
+//          cmake .. && make -j 10 && ./bin/square
+//      linux debug:
+//          cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j 10 && ./bin/square
+
+#include "fem.h"
+#include "femProblem.h"
+#include "femMedium.h"
+#include "femDirichlet.h"
+#include "femSolver.h"
+#include "femPost.h"
+#include "OpenMP.h"
+#include "build_square.h"
+#include "femGmsh.h"
+#include <stdexcept>
+
+int main(int argc, char **argv)
+{
+    try
+    {
+        // If compiled with OpenMP support (ubuntu), gmsh::initialize
+        // also sets the number of threads to "General.NumThreads".
+        int nthreads = OpenMP::get_max_threads();
+        std::cout << "nthreads = " << nthreads << '\n';
+        gmsh::initialize();
+        OpenMP::set_num_threads(nthreads);
+        gmsh::option::setNumber("General.Terminal", 1);
+
+        // build geometry & mesh
+        build_square(0.0, 0.0, 10.0, 1.0, 80, 8, true, true);
+
+        fem::Problem pbl;
+
+        new fem::Medium(pbl, "interior", 100.0, 0.3);
+
+        new fem::Dirichlet(pbl, "left", "X", 0.0);
+        new fem::Dirichlet(pbl, "left", "Y", 0.0);
+        new fem::Dirichlet(pbl, "pt3", "Y", -0.1);
+
+        fem::Solver solver(pbl);
+        solver.solve();
+
+        fem::Post post(solver);
+        post.build_views();
+        post.show_views( { "Y" });
+        post.view();
+
+        // gmsh::fltk::run();
+
+        gmsh::finalize();
+        return EXIT_SUCCESS;
+    }
+    catch (const std::exception &e)
+    {
+        std::cerr << "\n**ERROR: " << e.what() << '\n';
+        return EXIT_FAILURE;
+    }
+}
diff --git a/cxxfem/exe/build_square.h b/cxxfem/exe/build_square.h
new file mode 100644
index 0000000000000000000000000000000000000000..a5ed152276f03227624a9c793e9dc905bb06207c
--- /dev/null
+++ b/cxxfem/exe/build_square.h
@@ -0,0 +1,73 @@
+#ifndef BUILD_SQUARE_H
+#define BUILD_SQUARE_H
+
+/// build a square using Gmsh API
+
+#include "femGmsh.h"
+
+inline void build_square(double cx = 0.0, double cy = 0.0,
+                         double Lx = 1.0, double Ly = 1.0,
+                         int nx = 2, int ny = 2,
+                         bool structured = true, bool recombine = true)
+{
+    gmsh::model::add("square");
+    double lc = 1.0; // no impact
+    gmsh::model::geo::addPoint(cx, cy, 0, lc, 1);
+    gmsh::model::geo::addPoint(cx + Lx, cy, 0, lc, 2);
+    gmsh::model::geo::addPoint(cx + Lx, cy + Ly, 0, lc, 3);
+    gmsh::model::geo::addPoint(cx, cy + Ly, 0, lc, 4);
+
+    gmsh::model::geo::addLine(1, 2, 1);
+    gmsh::model::geo::addLine(2, 3, 2);
+    gmsh::model::geo::addLine(4, 3, 3);
+    gmsh::model::geo::addLine(1, 4, 4);
+
+    gmsh::model::geo::addCurveLoop({1, 2, -3, -4}, 1);
+
+    gmsh::model::geo::addPlaneSurface({1}, 1);
+
+    gmsh::model::geo::synchronize(); // sinon warning "unknown entity..." lors du addPhysicalGroup
+
+    // mesh lines
+    gmsh::model::mesh::setTransfiniteCurve(1, nx + 1);
+    gmsh::model::mesh::setTransfiniteCurve(3, nx + 1);
+    gmsh::model::mesh::setTransfiniteCurve(2, ny + 1);
+    gmsh::model::mesh::setTransfiniteCurve(4, ny + 1);
+
+    // recombine
+    if (structured)
+        gmsh::model::mesh::setTransfiniteSurface(1);
+    if (recombine)
+        gmsh::model::mesh::setRecombine(2, 1); // quads
+
+    // physical groups - corners
+    auto pt1 = gmsh::model::addPhysicalGroup(0, {1});
+    auto pt2 = gmsh::model::addPhysicalGroup(0, {2});
+    auto pt3 = gmsh::model::addPhysicalGroup(0, {3});
+    auto pt4 = gmsh::model::addPhysicalGroup(0, {4});
+    gmsh::model::setPhysicalName(0, pt1, "pt1");
+    gmsh::model::setPhysicalName(0, pt2, "pt2");
+    gmsh::model::setPhysicalName(0, pt3, "pt3");
+    gmsh::model::setPhysicalName(0, pt4, "pt4");
+
+    // physical groups - lines
+    auto bottom = gmsh::model::addPhysicalGroup(1, {1}, 1000);
+    auto right = gmsh::model::addPhysicalGroup(1, {2});
+    auto top = gmsh::model::addPhysicalGroup(1, {3});
+    auto left = gmsh::model::addPhysicalGroup(1, {4});
+    gmsh::model::setPhysicalName(1, bottom, "bottom");
+    gmsh::model::setPhysicalName(1, right, "right");
+    gmsh::model::setPhysicalName(1, top, "top");
+    gmsh::model::setPhysicalName(1, left, "left");
+
+    // physical groups - surfaces
+    auto interior = gmsh::model::addPhysicalGroup(2, {1});
+    gmsh::model::setPhysicalName(2, interior, "interior");
+
+    // finalize and mesh the geometry
+    gmsh::model::mesh::generate(2);
+
+    // gmsh::write("square.msh");
+}
+
+#endif //BUILD_SQUARE_H
diff --git a/cxxfem/exe/simple_shear.cpp b/cxxfem/exe/simple_shear.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0a2e3946a0f54adb885630cf415d45d6b006ecca
--- /dev/null
+++ b/cxxfem/exe/simple_shear.cpp
@@ -0,0 +1,66 @@
+// build & run:
+//      win:
+//          set OMP_NUM_THREADS=10
+//          cmake .. && make -j 10 && bin\square.exe
+//      win debug:
+//          cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j 10 && bin\square.exe
+//
+//      linux:
+//          export OMP_NUM_THREADS=10
+//          cmake .. && make -j 10 && ./bin/square
+//      linux debug:
+//          cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j 10 && ./bin/square
+
+#include "fem.h"
+#include "femProblem.h"
+#include "femMedium.h"
+#include "femDirichlet.h"
+#include "femSolver.h"
+#include "femPost.h"
+#include "OpenMP.h"
+#include "build_square.h"
+#include "femGmsh.h"
+#include <stdexcept>
+
+int main(int argc, char **argv)
+{
+    try
+    {
+        // If compiled with OpenMP support (ubuntu), gmsh::initialize
+        // also sets the number of threads to "General.NumThreads".
+        int nthreads = OpenMP::get_max_threads();
+        std::cout << "nthreads = " << nthreads << '\n';
+        gmsh::initialize();
+        OpenMP::set_num_threads(nthreads);
+        gmsh::option::setNumber("General.Terminal", 1);
+
+        // build geometry & mesh
+        build_square(0.0, 0.0, 1.0, 1.0, 10, 10, true, true); // structured
+
+        fem::Problem pbl;
+
+        new fem::Medium(pbl, "interior", 100., 0.3);
+
+        new fem::Dirichlet(pbl, "top", "X", 0.1);
+        new fem::Dirichlet(pbl, "top", "Y", 0.0);
+        new fem::Dirichlet(pbl, "bottom", "X", 0.0);
+        new fem::Dirichlet(pbl, "bottom", "Y", 0.0); 
+
+        fem::Solver solver(pbl);
+        solver.solve();
+
+        fem::Post post(solver);
+        post.build_views();
+        post.view();
+
+        // gmsh::fltk::run();
+
+        gmsh::finalize();
+        return EXIT_SUCCESS;
+    }
+    catch (const std::exception &e)
+    {
+        std::cerr << "\n**ERROR: " << e.what() << '\n';
+        return EXIT_FAILURE;
+    }
+}
diff --git a/cxxfem/exe/square.cpp b/cxxfem/exe/square.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..dae56bfb9eb8cae84a5ab5659b92a72ff09b1062
--- /dev/null
+++ b/cxxfem/exe/square.cpp
@@ -0,0 +1,65 @@
+// build & run:
+//      win:
+//          set OMP_NUM_THREADS=10
+//          cmake .. && make -j 10 && bin\square.exe
+//      win debug:
+//          cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j 10 && bin\square.exe
+//
+//      linux:
+//          export OMP_NUM_THREADS=10
+//          cmake .. && make -j 10 && ./bin/square
+//      linux debug:
+//          cmake -DCMAKE_BUILD_TYPE=Debug .. && make -j 10 && ./bin/square
+
+#include "fem.h"
+#include "femProblem.h"
+#include "femMedium.h"
+#include "femDirichlet.h"
+#include "femSolver.h"
+#include "femPost.h"
+#include "OpenMP.h"
+#include "build_square.h"
+#include "femGmsh.h"
+#include <stdexcept>
+
+int main(int argc, char **argv)
+{
+    try
+    {
+        // If compiled with OpenMP support (ubuntu), gmsh::initialize
+        // also sets the number of threads to "General.NumThreads".
+        int nthreads = OpenMP::get_max_threads();
+        std::cout << "nthreads = " << nthreads << '\n';
+        gmsh::initialize();
+        OpenMP::set_num_threads(nthreads);
+        gmsh::option::setNumber("General.Terminal", 1);
+
+        // build geometry & mesh
+        build_square(0.0, 0.0, 1.0, 1.0, 10, 10, true, true); // structured
+
+        fem::Problem pbl;
+
+        new fem::Medium(pbl, "interior", 100.0, 0.3);
+
+        new fem::Dirichlet(pbl, "bottom", "Y", 0.0);
+        new fem::Dirichlet(pbl, "left", "X", 0.0);
+        new fem::Dirichlet(pbl, "right", "X", 1.0); // tensile prescribed displ
+
+        fem::Solver solver(pbl);
+        solver.solve();
+
+        fem::Post post(solver);
+        post.build_views();
+        post.view();
+
+        // gmsh::fltk::run();
+
+        gmsh::finalize();
+        return EXIT_SUCCESS;
+    }
+    catch (const std::exception &e)
+    {
+        std::cerr << "\n**ERROR: " << e.what() << '\n';
+        return EXIT_FAILURE;
+    }
+}
diff --git a/cxxfem/src/CMakeLists.txt b/cxxfem/src/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..cb63466ab1083dea6ad58cd6c302f730d5db3f35
--- /dev/null
+++ b/cxxfem/src/CMakeLists.txt
@@ -0,0 +1,12 @@
+
+FILE(GLOB SRCS *.cpp)
+ADD_LIBRARY(fem SHARED ${SRCS})
+TARGET_LINK_LIBRARIES(fem PUBLIC ${GMSH_LIBRARIES})
+TARGET_INCLUDE_DIRECTORIES(fem PUBLIC ${PROJECT_SOURCE_DIR}/src)
+IF(OpenMP_CXX_FOUND)
+    TARGET_LINK_LIBRARIES(fem PUBLIC OpenMP::OpenMP_CXX)
+ENDIF()
+IF(FEM_USE_MKL)
+    TARGET_LINK_LIBRARIES(fem PUBLIC ${MKL_LIBRARIES})
+    TARGET_INCLUDE_DIRECTORIES(fem PUBLIC ${MKL_INCLUDE_DIRS})
+ENDIF()
\ No newline at end of file
diff --git a/cxxfem/src/OpenMP.h b/cxxfem/src/OpenMP.h
new file mode 100644
index 0000000000000000000000000000000000000000..fd3036120c524c3c14bf7b878323898d052d014e
--- /dev/null
+++ b/cxxfem/src/OpenMP.h
@@ -0,0 +1,37 @@
+#ifndef OPENMP_H
+#define OPENMP_H
+
+#include <iostream>
+#ifdef _OPENMP
+#include <omp.h>
+#endif
+
+namespace OpenMP
+{
+    inline bool enabled()
+    {
+#ifdef _OPENMP
+        return true;
+#else
+        return false;
+#endif
+    }
+
+    inline int get_max_threads()
+    {
+#ifdef _OPENMP
+        return omp_get_max_threads();
+#else
+        return 1;
+#endif
+    }
+    inline void set_num_threads(int n)
+    {
+#ifdef _OPENMP
+        std::cout << "setting num threads to " << n << '\n';
+        return omp_set_num_threads(n);
+#endif
+    }
+};
+
+#endif
diff --git a/cxxfem/src/fem.cpp b/cxxfem/src/fem.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a9cfd1d4fbff7c25645f3fc536ea59379554f490
--- /dev/null
+++ b/cxxfem/src/fem.cpp
@@ -0,0 +1,14 @@
+#include "fem.h"
+
+namespace fem
+{
+
+FEM_API int debug_level(int lvl)
+{
+    static int level=0;
+    if(lvl>=0)
+        level = lvl;
+    return level;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/fem.h b/cxxfem/src/fem.h
new file mode 100644
index 0000000000000000000000000000000000000000..fd9c2d69004d41f006773e67257af893f5c18f8c
--- /dev/null
+++ b/cxxfem/src/fem.h
@@ -0,0 +1,70 @@
+#ifndef FEM_H
+#define FEM_H
+
+#if defined(WIN32)
+#ifdef fem_EXPORTS
+#define FEM_API __declspec(dllexport)
+#else
+#define FEM_API __declspec(dllimport)
+#endif
+#else
+#define FEM_API
+#endif
+
+#ifdef _MSC_VER
+#pragma warning(disable : 4251) // DLL/templates non exportes
+//#define NOMINMAX // avoids the definition of "min/max" macro  in <minwindef.h> (which could shadow std::max)
+#endif //_MSC_VER
+
+#include "config.h"
+#include <vector>
+#include <string>
+#include <iostream>
+
+namespace fem
+{
+class Problem;
+class Entity;
+class ElementSet;
+class Group;
+class Medium;
+class Dirichlet;
+class Neumann;
+class Solver;
+class Partition;
+class Part;
+class DOF;
+class DOFs;
+class DOFMask;
+class Post;
+class Timer;
+class Extractor;
+} // namespace fem
+
+namespace fem
+{
+
+template <typename T>
+std::ostream &
+operator<<(std::ostream &out, std::vector<T> const &v)
+{
+    out << "[ ";
+    for (auto const &i : v)
+        out << i << " ";
+    out << "]";
+    return out;
+}
+
+template <typename T1, typename T2>
+std::ostream &
+operator<<(std::ostream &out, std::pair<T1, T2> const &v)
+{
+    out << "(" << v.first << "," << v.second << ")";
+    return out;
+}
+
+FEM_API int debug_level(int lvl=-1);
+
+} // namespace fem
+
+#endif // FEM_H
\ No newline at end of file
diff --git a/cxxfem/src/femDOF.cpp b/cxxfem/src/femDOF.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..883bfa19ae6d2bd8bcd6be7484196b110268c768
--- /dev/null
+++ b/cxxfem/src/femDOF.cpp
@@ -0,0 +1,49 @@
+#include "femDOF.h"
+
+namespace fem
+{
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, DOF const &obj)
+{
+    out << obj.dofs.name(obj);
+    return out;
+}
+
+DOFs::DOFs()
+{
+}
+
+void DOFs::add(std::string const &name)
+{
+    size_t idx = names.size();
+    names.push_back(name);
+    idx_by_name[name] = idx;
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, DOFMask const &obj)
+{
+    bool first = true;
+    for (size_t i = 0; i < obj.dofs.size(); ++i)
+    {
+        DOF dof = obj.dofs[i];
+        if (obj & dof)
+        {
+            if (!first)
+                out << '|';
+            out << dof;
+            first = false;
+        }
+    }
+    return out;
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, DOFs const &obj)
+{
+    out << "DOF types (size=" << obj.names.size() << "): " << obj.names << '\n';
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femDOF.h b/cxxfem/src/femDOF.h
new file mode 100644
index 0000000000000000000000000000000000000000..2e2ad4fadbfc8b129bb4b3a49ff32688868c74eb
--- /dev/null
+++ b/cxxfem/src/femDOF.h
@@ -0,0 +1,86 @@
+#ifndef FEMDOF_H
+#define FEMDOF_H
+
+#include "fem.h"
+#include <map>
+
+namespace fem
+{
+
+/// a dof type (an index in the dofs table) e.g. "X"=>0, "Y"=>1, etc.
+
+class FEM_API DOF
+{
+    DOFs const &dofs; ///< set of flags
+    size_t idx;       ///< readonly index
+public:
+    size_t index() const { return idx; }
+    friend FEM_API std::ostream &operator<<(std::ostream &out, DOF const &obj);
+
+private:
+    friend class DOFs;    // DOF must be created by DOFs
+    friend class DOFMask; // used for accessing dofs (for printing)
+    DOF(DOFs const &_dofs, size_t _idx) : dofs(_dofs), idx(_idx) {}
+};
+
+/// stores all dof types
+///      this is the only class which is able to create a new type of dof
+
+class FEM_API DOFs
+{
+    std::vector<std::string> names;
+    std::map<std::string, size_t> idx_by_name;
+
+public:
+    DOFs();
+    ~DOFs() {}
+    size_t size() const { return names.size(); }
+    std::string name(DOF const &dof) const { return names.at(dof.idx); }
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, DOFs const &obj);
+    void add(std::string const &dofname);
+    DOF operator[](std::string const &dofname) const { return DOF(*this, idx_by_name.at(dofname)); }
+    DOF operator[](char const *dofname) const { return DOF(*this, idx_by_name.at(std::string(dofname))); }
+    DOF operator[](size_t idx) const { return DOF(*this, idx); }
+};
+
+/// a mask of dofs
+///     allows the user to select several dof types using |
+
+class FEM_API DOFMask
+{
+    DOFs const &dofs; ///< set
+    size_t v;         ///< mask
+public:
+    /// create a mask from a single dof (implicitly if needed)
+    DOFMask(DOF const &dof) : dofs(dof.dofs), v(1 << dof.index()) {}
+    /// combine masks & other dofs
+    DOFMask operator|(DOFMask const &obj) const
+    {
+        if (&dofs != &obj.dofs)
+            throw std::runtime_error("incompatible sets"); // use assert?
+        return DOFMask(dofs, v | obj.v);
+    }
+    /// check a mask versus another one
+    bool operator&(DOFMask const &obj) const
+    {
+        if (&dofs != &obj.dofs)
+            throw std::runtime_error("incompatible sets"); // use assert?
+        return v & obj.v;
+    }
+    /// pretty print
+    friend FEM_API std::ostream &operator<<(std::ostream &out, DOFMask const &obj);
+
+private:
+    /// rebuild a mask from the result of a boolean operation
+    DOFMask(DOFs const &_dofs, size_t _v) : dofs(_dofs), v(_v) {}
+};
+
+inline DOFMask operator|(DOF const &d1, DOF const &d2)
+{
+    return (DOFMask)d1 | d2;
+}
+
+} // namespace fem
+
+#endif // FEMDOF_H
diff --git a/cxxfem/src/femDirichlet.cpp b/cxxfem/src/femDirichlet.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..db33313b6571cabfd004e8e3448b609e209f757d
--- /dev/null
+++ b/cxxfem/src/femDirichlet.cpp
@@ -0,0 +1,29 @@
+#include "femDirichlet.h"
+#include "femProblem.h"
+#include "femGroup.h"
+
+namespace fem
+{
+
+Dirichlet::Dirichlet(Problem &pbl, std::string const &groupname,
+                     std::string const &dof, double val) : m_dofname(dof), m_val(val)
+{
+    group = pbl.groups_by_name.at(groupname);
+    pbl.dirichlets.push_back(this);
+}
+
+Dirichlet::~Dirichlet()
+{
+    if(debug_level()>0)
+        std::cout << "~Dirichlet()\n";
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Dirichlet const &obj)
+{
+    out << "Dirichlet BC on \"" << obj.group->name << "\": "
+        << obj.m_dofname << '=' << obj.m_val;
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femDirichlet.h b/cxxfem/src/femDirichlet.h
new file mode 100644
index 0000000000000000000000000000000000000000..ae6e80fd4a3fa37702d529770c2ff75f9c3c5426
--- /dev/null
+++ b/cxxfem/src/femDirichlet.h
@@ -0,0 +1,32 @@
+#ifndef FEMDIRICHLET_H
+#define FEMDIRICHLET_H
+
+#include "fem.h"
+
+namespace fem
+{
+
+/// Dirichlet boundary condition:
+///     a target group and a value to be applied on a given dof
+
+class FEM_API Dirichlet
+{
+public:
+#ifndef SWIG
+    Group *group;        ///< target group
+    std::string m_dofname; ///< target degree of freedom
+    double m_val;          ///< prescribed value
+#endif
+
+public:
+    Dirichlet(Problem &pbl, std::string const &groupname, std::string const &dof, double val);
+    ~Dirichlet();
+
+#ifndef SWIG
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Dirichlet const &obj);
+#endif
+};
+
+} // namespace fem
+
+#endif // FEMDIRICHLET_H
diff --git a/cxxfem/src/femElementSet.cpp b/cxxfem/src/femElementSet.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..dd1f0958a8cc01e289cac47c18394db2e4a69c71
--- /dev/null
+++ b/cxxfem/src/femElementSet.cpp
@@ -0,0 +1,48 @@
+#include "femElementSet.h"
+#include "femGmsh.h"
+
+namespace fem
+{
+
+ElementSet::ElementSet(int _typ,
+                       std::vector<std::size_t> const &_etags,
+                       std::vector<std::size_t> const &_ntags) : type_id(_typ),
+                                                                 etags(_etags),
+                                                                 ntags_v(_ntags),
+                                                                 ntags(&ntags_v[0], etags.size(), ntags_v.size() / etags.size())
+{
+
+    // properties of type "type_id" (could be shared)
+    std::vector<double> localNodeCoord;
+    gmsh::model::mesh::getElementProperties(type_id,
+                                            type_name, type_dim,
+                                            type_order, type_nnods,
+                                            localNodeCoord,
+                                            type_numPrimaryNodes);
+
+    // localNodeCoord is [xi1, eta1, xi2, eta2, xi3, eta3, ...] for 2d elements
+    // which is inconsistent with localCoords returned for the Gauss points
+    // [xi1, eta1, 0, xi2, eta2, 0, xi3, eta3, 0, ...]
+    type_localNodeCoord.resize(type_nnods * 3);
+    std::fill(type_localNodeCoord.begin(), type_localNodeCoord.end(), 0.0);
+
+    for (int i = 0; i < type_nnods; ++i)
+        for (int j = 0; j < type_dim; ++j)
+            type_localNodeCoord[3 * i + j] = localNodeCoord[type_dim * i + j];
+}
+
+ElementSet::~ElementSet()
+{
+    // std::cout << "~ElementSet()\n";
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, ElementSet const &obj)
+{
+    out << "set of " << obj.etags.size() << " \"" << obj.type_name
+        << "\" (type=" << obj.type_id << ", order=" << obj.type_order
+        << ", nnods=" << obj.type_nnods << ")";
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femElementSet.h b/cxxfem/src/femElementSet.h
new file mode 100644
index 0000000000000000000000000000000000000000..878edaa3ac821c090afe71b99d2fcf1d88d93be4
--- /dev/null
+++ b/cxxfem/src/femElementSet.h
@@ -0,0 +1,40 @@
+#ifndef FEMELEMENTSET_H
+#define FEMELEMENTSET_H
+
+#include "fem.h"
+#include <Eigen/Core>
+
+namespace fem
+{
+    
+/// a set of elements of a given type
+
+class FEM_API ElementSet
+{
+public:
+    int type_id;                      ///< type (integer) e.g. 5 => "Hexahedron 8"
+    std::vector<std::size_t> etags;   ///< element tags
+    std::vector<std::size_t> ntags_v; ///< node tags [nodes of elem1, then elem2, elem3, etc.]
+
+    Eigen::Map<Eigen::Matrix<size_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>
+        ntags;
+
+    // element type data (could be shared)
+    std::string type_name;
+    int type_dim;
+    int type_order;
+    int type_nnods;
+    std::vector<double> type_localNodeCoord;
+    int type_numPrimaryNodes;
+
+public:
+    ElementSet(int _typ, std::vector<std::size_t> const &_etags,
+               std::vector<std::size_t> const &_ntags);
+    ~ElementSet();
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, ElementSet const &obj);
+};
+
+} // namespace fem
+
+#endif // FEMELEMENTSET_H
diff --git a/cxxfem/src/femEntity.cpp b/cxxfem/src/femEntity.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b71cd63adc19c98c70626298f39721fc348e3bb1
--- /dev/null
+++ b/cxxfem/src/femEntity.cpp
@@ -0,0 +1,55 @@
+#include "femEntity.h"
+#include "femElementSet.h"
+#include "femGmsh.h"
+
+namespace fem
+{
+
+Entity::Entity(int _dim, int _tag) : dim(_dim), tag(_tag)
+{
+    gmsh::model::getEntityName(dim, tag, name);
+    gmsh::model::getType(dim, tag, typ);
+
+    // mesh
+    std::vector<int> elementTypes;
+    std::vector<std::vector<std::size_t>> elementTags;
+    std::vector<std::vector<std::size_t>> nodeTags;
+    gmsh::model::mesh::getElements(elementTypes, elementTags, nodeTags, dim, tag);
+    for (size_t i = 0; i < elementTypes.size(); ++i)
+        esets.push_back(new ElementSet(elementTypes[i], elementTags[i], nodeTags[i]));
+}
+
+Entity::~Entity()
+{
+    // std::cout << "~Entity()\n";
+    for (auto &e : esets)
+        delete e;
+}
+
+void Entity::link_to_groups(std::map<std::pair<int, int>, Group *> const &groups_by_dimtag)
+{
+    std::vector<int> tags;
+    gmsh::model::getPhysicalGroupsForEntity(dim, tag, tags);
+    for (auto tag : tags)
+    {
+        try
+        {
+            groups.push_back(groups_by_dimtag.at(std::make_pair(dim, tag)));
+        }
+        catch (const std::exception &e)
+        {
+            throw std::runtime_error("Entity::link_to_groups: entity (" + std::to_string(dim) + "," + std::to_string(tag) + ") not found!");
+        }
+    }
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Entity const &obj)
+{
+    out << obj.typ << " (" << obj.dim << "d, " << obj.tag << ") named \""
+        << obj.name << "\" with " << obj.esets.size() << " mesh(es), belonging to "
+        << obj.groups.size() << " group(s)";
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femEntity.h b/cxxfem/src/femEntity.h
new file mode 100644
index 0000000000000000000000000000000000000000..f63cb958be1f62c4d4fe15b4eff2ac9ee0687e7a
--- /dev/null
+++ b/cxxfem/src/femEntity.h
@@ -0,0 +1,33 @@
+#ifndef FEMENTITY_H
+#define FEMENTITY_H
+
+#include "fem.h"
+#include <map>
+
+namespace fem
+{
+
+/// a gmsh entity
+
+class FEM_API Entity
+{
+public:
+    int dim;                         ///< dimension 0, 1, 2 or 3
+    int tag;                         ///< gmsh id
+    std::string name;                ///< name (usually "")
+    std::string typ;                 ///< Point, Surface, Plane, Line, Volume, etc.
+    std::vector<ElementSet *> esets; ///< maillages (1 maillage = 1 type d'élément)
+    std::vector<Group *> groups;     ///< physical groups
+
+public:
+    Entity(int _dim, int _tag);
+    ~Entity();
+
+    void link_to_groups(std::map<std::pair<int, int>, Group *> const &groups_by_dimtag);
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Entity const &obj);
+};
+
+} // namespace fem
+
+#endif // FEMENTITY_H
diff --git a/cxxfem/src/femExtractor.cpp b/cxxfem/src/femExtractor.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fc0a89749bf9ed6aea663fdd2e2d860fe6667bf4
--- /dev/null
+++ b/cxxfem/src/femExtractor.cpp
@@ -0,0 +1,23 @@
+#include "femExtractor.h"
+
+namespace fem
+{
+
+Extractor::Extractor()
+{
+}
+
+Extractor::~Extractor()
+{
+    if(debug_level()>0)
+        std::cout << "~Extractor()\n";
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Extractor const &obj)
+{
+    out << "Extractor\n";
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femExtractor.h b/cxxfem/src/femExtractor.h
new file mode 100644
index 0000000000000000000000000000000000000000..6f4348c5cd8544468ec39551fc8c094620210d9f
--- /dev/null
+++ b/cxxfem/src/femExtractor.h
@@ -0,0 +1,23 @@
+#ifndef FEMEXTRACTOR_H
+#define FEMEXTRACTOR_H
+
+#include "fem.h"
+
+namespace fem
+{
+    
+///
+
+class FEM_API Extractor
+{
+public:
+public:
+    Extractor();
+    ~Extractor();
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Extractor const &obj);
+};
+
+} // namespace fem
+
+#endif // FEMEXTRACTOR_H
diff --git a/cxxfem/src/femGmsh.h b/cxxfem/src/femGmsh.h
new file mode 100644
index 0000000000000000000000000000000000000000..e6e0428900c06a918547acf90152b2bc2ca2a13b
--- /dev/null
+++ b/cxxfem/src/femGmsh.h
@@ -0,0 +1,5 @@
+#ifdef _MSC_VER
+#include <gmsh.h_cwrap>
+#else
+#include <gmsh.h>
+#endif
diff --git a/cxxfem/src/femGroup.cpp b/cxxfem/src/femGroup.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6ca75075cb6b20cb772e18cb66bc286c4ae37779
--- /dev/null
+++ b/cxxfem/src/femGroup.cpp
@@ -0,0 +1,35 @@
+#include "femGroup.h"
+#include "femGmsh.h"
+
+namespace fem
+{
+
+Group::Group(int _dim, int _tag) : dim(_dim), tag(_tag)
+{
+    gmsh::model::getPhysicalName(dim, tag, name);
+    // this-> entities filled later with 'link_to_entities'
+}
+
+Group::~Group()
+{
+    if(debug_level()>0)
+        std::cout << "~Group()\n";
+}
+
+void Group::link_to_entities(std::map<std::pair<int,int>, Entity*> const &entities_by_dimtag)
+{
+    std::vector<int> tags;
+    gmsh::model::getEntitiesForPhysicalGroup(dim, tag, tags);
+    for(auto tag : tags)
+        entities.push_back( entities_by_dimtag.at(std::make_pair(dim,tag)) );
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Group const &obj)
+{
+    out << "Group \"" << obj.name << "\" (" << obj.dim << "d, " << obj.tag
+        << ") made of " << obj.entities.size() << " entities";
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femGroup.h b/cxxfem/src/femGroup.h
new file mode 100644
index 0000000000000000000000000000000000000000..f4c3ac7ffd17b5bc63b55a336d48b8f0326b60a7
--- /dev/null
+++ b/cxxfem/src/femGroup.h
@@ -0,0 +1,31 @@
+#ifndef FEMGROUP_H
+#define FEMGROUP_H
+
+#include "fem.h"
+#include <map>
+
+namespace fem
+{
+
+/// a set of entities (physical group)
+
+class FEM_API Group
+{
+public:
+    int dim; ///< dimension 0, 1, 2 or 3
+    int tag; ///< gmsh id
+    std::string name;
+    std::vector<Entity *> entities;
+
+public:
+    Group(int _dim, int _tag);
+    ~Group();
+
+    void link_to_entities(std::map<std::pair<int, int>, Entity *> const &entities_by_dimtag);
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Group const &obj);
+};
+
+} // namespace fem
+
+#endif // FEMGROUP_H
diff --git a/cxxfem/src/femMaps.h b/cxxfem/src/femMaps.h
new file mode 100644
index 0000000000000000000000000000000000000000..ce25ca3d26bd9a30579e70cc71298eadb266a2c0
--- /dev/null
+++ b/cxxfem/src/femMaps.h
@@ -0,0 +1,114 @@
+#ifndef FEMMAPS_H
+#define FEMMAPS_H
+
+#include "fem.h"
+#include <Eigen/Dense>
+
+namespace fem
+{
+    
+/// helper class for accessing gmsh jacobian vector as an Eigen Matrix3d
+///     WARNING: this class does not copy the initial vector!
+///
+/// with Eigen, the jacobian is NOT transposed by default
+///     it is returned "by column" by gmsh and Eigen uses column-major
+///     storage by default
+///     (!= C storage or numpy's default = row major)
+///
+///  gmsh =  [ element1, element2, element3, ... ]
+///            element1 = [ gp1, gp2, gp3, ... ]
+///                         gp1 = [ dx/du, dy/du, dz/du, dx/dv, ... ]
+///
+///                    [ dx/du dx/dv dx/dw ]
+///  JacoMap(e, gp) =  [ dy/du dy/dv dy/dw ]
+///                    [ dz/du dz/dv dz/dw ]
+///
+class JacoMap
+{
+    std::vector<double> &jacobians;
+    size_t nelems; ///< only used to check bounds in debug - TODO: could be computed from jacobians.size()
+    size_t nGP;
+
+public:
+    typedef Eigen::Map<Eigen::Matrix3d> M;
+
+    JacoMap(std::vector<double> &_jacobians,
+            size_t _nelems, size_t _nGP) : jacobians(_jacobians),
+                                           nelems(_nelems), nGP(_nGP)
+    {
+    }
+    M operator()(size_t e, size_t gp) const
+    {
+        assert(e < nelems && gp < nGP);
+        return M(&jacobians[(9 * nGP) * e + 9 * gp]);
+    }
+};
+
+/// helper class for accessing gmsh coord vector as an Eigen Vector3d
+///     WARNING: this class does not copy the initial vector!
+///
+///  gmsh = [ elem1, elem2, elem3, ... ]
+///           elem1 = [ gp1, gp2, gp3, ... ]
+///                     gp1 = [ x, y, z ]
+///
+///  CoordMap(e, gp) = [ x ]
+///                    [ y ]
+///                    [ z ]
+
+class CoordMap
+{
+    std::vector<double> &coord;
+    size_t nelems; ///< only used to check bounds in debug - TODO: could be computed from coord.size()
+    size_t nGP;
+
+public:
+    typedef Eigen::Map<Eigen::Vector3d> M;
+
+    CoordMap(std::vector<double> &_coord,
+             size_t _nelems, size_t _nGP) : coord(_coord),
+                                            nelems(_nelems), nGP(_nGP)
+    {
+    }
+    M operator()(size_t e, size_t gp) const
+    {
+        assert(e < nelems && gp < nGP);
+        return M(&coord[(3 * nGP) * e + 3 * gp]);
+    }
+};
+
+/// helper class for accessing gmsh dshapef vector as an Eigen MatrixXd
+///
+///    gmsh = [ gp1, gp2, gp2, ... ]
+///             gp1 = [ shp1, shp2, shp3, ... ]
+///                     shp1 = [df/du, df/dv, df/dw]
+///
+/// DShapeMap(gp) = [ df/du, df/dv, df/dw  ] <= shp1
+///                 [ df/du, df/dv, df/dw  ] <= shp2
+///                 [ df/du, df/dv, df/dw  ] <= shp3
+///                 [ ...                  ]
+
+class DShapeMap
+{
+    std::vector<double> &basisDF;
+    size_t nGP; ///< only used to check bounds in debug - TODO: could be computed from basisDF.size()
+    int nnod;
+
+public:
+    typedef Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> M;
+
+    DShapeMap(std::vector<double> &_basisDF,
+              size_t _nGP, int _nnod) : basisDF(_basisDF), nGP(_nGP),
+                                        nnod(_nnod)
+    {
+    }
+
+    M operator()(size_t gp) const
+    {
+        assert(gp < nGP);
+        return M(&basisDF[(nnod * 3) * gp], nnod, 3);
+    }
+};
+
+} // namespace fem
+
+#endif // FEMMAPS_H
diff --git a/cxxfem/src/femMedium.cpp b/cxxfem/src/femMedium.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d6037909cbecc6bd792b33cf3eaccc79c02c086a
--- /dev/null
+++ b/cxxfem/src/femMedium.cpp
@@ -0,0 +1,50 @@
+#include "femMedium.h"
+#include "femProblem.h"
+#include "femGroup.h"
+
+namespace fem
+{
+
+Medium::Medium(Problem &pbl, std::string const &groupname,
+               double E, double nu) : m_E(E), m_nu(nu), H2D(3, 3), H3D(6, 6)
+{
+    if (pbl.entities.empty()) // is the problem already sync'ed?
+        pbl.sync();
+
+    group = pbl.groups_by_name.at(groupname);
+    pbl.media.push_back(this);
+
+    // construct 2D-plane-stress Hooke matrix
+    H2D << 1.0, nu, 0.0,
+        nu, 1, 0.0,
+        0.0, 0.0, (1.0 - nu) / 2.0;
+    H2D *= E / (1 - nu * nu);
+
+    // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+    // std::cout << "H2D =" << H2D.format(fmt);
+
+    // construct 3D Hooke matrix
+    H3D << 1.0 - nu, nu, nu, 0.0, 0.0, 0.0,
+        nu, 1.0 - nu, nu, 0.0, 0.0, 0.0,
+        nu, nu, 1.0 - nu, 0.0, 0.0, 0.0,
+        0.0, 0.0, 0.0, (1.0 - 2 * nu) / 2.0, 0.0, 0.0,
+        0.0, 0.0, 0.0, 0.0, (1.0 - 2 * nu) / 2.0, 0.0,
+        0.0, 0.0, 0.0, 0.0, 0.0, (1.0 - 2 * nu) / 2.0;
+    H3D *= E / (1 + nu) / (1 - 2 * nu);
+}
+
+Medium::~Medium()
+{
+    if(debug_level()>0)
+        std::cout << "~Medium()\n";
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Medium const &obj)
+{
+    out << "Medium on \"" << obj.group->name
+        << "\": E=" << obj.m_E << ", nu=" << obj.m_nu;
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femMedium.h b/cxxfem/src/femMedium.h
new file mode 100644
index 0000000000000000000000000000000000000000..88e779cce05787dec1d06b6b6cafb2f1b546b186
--- /dev/null
+++ b/cxxfem/src/femMedium.h
@@ -0,0 +1,40 @@
+#ifndef FEMMEDIUM_H
+#define FEMMEDIUM_H
+
+#include "fem.h"
+#include <Eigen/Dense>
+
+namespace fem
+{
+
+/// physical properties on a domain defined by a group
+
+class FEM_API Medium
+{
+public:
+#ifndef SWIG
+    double m_E;  ///< Young's modulus
+    double m_nu; ///< Poisson's ratio
+    Group *group;
+
+    Eigen::MatrixXd H2D; ///< Hooke
+    Eigen::MatrixXd H3D; ///< Hooke
+#endif
+
+public:
+    Medium(Problem &pbl, std::string const &groupname, double E, double nu);
+    ~Medium();
+
+#ifndef SWIG
+    Eigen::MatrixXd &hooke(int ndim) 
+    {
+        return (ndim==2)? H2D : H3D;
+    }
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Medium const &obj);
+#endif
+};
+
+} // namespace fem
+
+#endif // FEMMEDIUM_H
diff --git a/cxxfem/src/femNeumann.cpp b/cxxfem/src/femNeumann.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2a2b6948fdeefb2be4b1abbabbadfd0d6fc1c1e9
--- /dev/null
+++ b/cxxfem/src/femNeumann.cpp
@@ -0,0 +1,49 @@
+#include "femNeumann.h"
+#include "femProblem.h"
+#include "femGroup.h"
+
+namespace fem
+{
+
+Neumann::Neumann(Problem &pbl, std::string const &groupname,
+                 std::string const &dof, double val)
+    : m_nodetag(0), m_dofname(dof), m_val(val)
+{
+    try
+    {
+        group = pbl.groups_by_name.at(groupname);
+    }
+    catch (const std::exception &e)
+    {
+        throw std::runtime_error("Neumann: group \"" + groupname + "\" not found!");
+    }
+    pbl.neumanns.push_back(this);
+}
+
+Neumann::Neumann(Problem &pbl, size_t nodetag,
+                 std::string const &dof, double val)
+    : group(nullptr), m_nodetag(nodetag), m_dofname(dof), m_val(val)
+{
+    pbl.neumanns.push_back(this);
+}
+
+Neumann::~Neumann()
+{
+    // if(debug_level()>0)
+    //     std::cout << "~Neumann()\n";
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Neumann const &obj)
+{
+    if (obj.group)
+        out << "Neumann BC on \"" << obj.group->name << "\": "
+            << obj.m_dofname << '=' << obj.m_val;
+    // else
+    //     out << "Neumann BC on node " << obj.m_nodetag << ": "
+    //         << obj.m_dofname << '=' << obj.m_val;
+
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femNeumann.h b/cxxfem/src/femNeumann.h
new file mode 100644
index 0000000000000000000000000000000000000000..7a51cad5900a6a3f1e78961b9795e49f9f468603
--- /dev/null
+++ b/cxxfem/src/femNeumann.h
@@ -0,0 +1,34 @@
+#ifndef FEMNEUMANN_H
+#define FEMNEUMANN_H
+
+#include "fem.h"
+
+namespace fem
+{
+
+/// Neumann boundary condition:
+///     a target group and a value to be applied on a given dof
+
+class FEM_API Neumann
+{
+public:
+#ifndef SWIG
+    Group *group;           ///< option1: target group
+    size_t m_nodetag;       ///< option2: target node tag
+    std::string m_dofname;  ///< target degree of freedom
+    double m_val;           ///< prescribed value
+#endif
+
+public:
+    Neumann(Problem &pbl, std::string const &groupname, std::string const &dof, double val);
+    Neumann(Problem &pbl, size_t nodetag, std::string const &dof, double val);
+    ~Neumann();
+
+#ifndef SWIG
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Neumann const &obj);
+#endif
+};
+
+} // namespace fem
+
+#endif // FEMNEUMANN_H
diff --git a/cxxfem/src/femNewClass.cpp b/cxxfem/src/femNewClass.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..22db923ca26a0a5100492428a025437f73225e63
--- /dev/null
+++ b/cxxfem/src/femNewClass.cpp
@@ -0,0 +1,23 @@
+#include "femNewClass.h"
+
+namespace fem
+{
+
+NewClass::NewClass()
+{
+}
+
+NewClass::~NewClass()
+{
+    if(debug_level()>0)
+        std::cout << "~NewClass()\n";
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, NewClass const &obj)
+{
+    out << "NewClass\n";
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femNewClass.h b/cxxfem/src/femNewClass.h
new file mode 100644
index 0000000000000000000000000000000000000000..4decb13d9db89f6a333474fa49b6f0978476e64c
--- /dev/null
+++ b/cxxfem/src/femNewClass.h
@@ -0,0 +1,23 @@
+#ifndef FEMNEWCLASS_H
+#define FEMNEWCLASS_H
+
+#include "fem.h"
+
+namespace fem
+{
+    
+///
+
+class FEM_API NewClass
+{
+public:
+public:
+    NewClass();
+    ~NewClass();
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, NewClass const &obj);
+};
+
+} // namespace fem
+
+#endif // FEMNEWCLASS_H
diff --git a/cxxfem/src/femPart.cpp b/cxxfem/src/femPart.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b872b8d21e997a14f3654228e82fdfae7a151118
--- /dev/null
+++ b/cxxfem/src/femPart.cpp
@@ -0,0 +1,28 @@
+#include "femPart.h"
+#include "femPartition.h"
+
+namespace fem
+{
+
+Part::Part(Partition &partition, 
+           DOFMask const &_dofmask,
+           DOFMask const &_bcmask) : dofmask(_dofmask), bcmask(_bcmask)
+{
+    idx = partition.parts.size();
+    partition.parts.push_back(this);
+}
+
+Part::~Part()
+{
+    if(debug_level()>0)
+        std::cout << "~Part()\n";
+}
+
+std::ostream &
+operator<<(std::ostream &out, Part const &obj)
+{
+    out << "Part #" << obj.idx << ": " << obj.dofmask << ", " << obj.bcmask;
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femPart.h b/cxxfem/src/femPart.h
new file mode 100644
index 0000000000000000000000000000000000000000..79555e147e49b14d929b6318cc1452b204c3a5ea
--- /dev/null
+++ b/cxxfem/src/femPart.h
@@ -0,0 +1,28 @@
+#ifndef FEMPART_H
+#define FEMPART_H
+
+#include "fem.h"
+#include "femDOF.h"
+
+namespace fem
+{
+
+///
+
+class FEM_API Part
+{
+public:
+    DOFMask dofmask; ///< DOF type
+    DOFMask bcmask;  ///< BC type
+    size_t idx;      ///< partition number (index in partition.parts)
+
+public:
+    Part(Partition &partition, DOFMask const &_dofmask, DOFMask const &_bcmask);
+    ~Part();
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Part const &obj);
+};
+
+} // namespace fem
+
+#endif // FEMPART_H
diff --git a/cxxfem/src/femPartition.cpp b/cxxfem/src/femPartition.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..90298e9789dc782cb53a4687fe6fbfd407c04eb9
--- /dev/null
+++ b/cxxfem/src/femPartition.cpp
@@ -0,0 +1,26 @@
+#include "femPartition.h"
+#include "femPart.h"
+
+namespace fem
+{
+
+Partition::Partition()
+{
+}
+
+Partition::~Partition()
+{
+    if(debug_level()>0)
+        std::cout << "~Partition()\n";
+}
+
+std::ostream &
+operator<<(std::ostream &out, Partition const &obj)
+{
+    out << "Partition\n";
+    for(auto p : obj.parts)
+        out << '\t' << *p << '\n';
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femPartition.h b/cxxfem/src/femPartition.h
new file mode 100644
index 0000000000000000000000000000000000000000..24c9360f6d0679c5686979ef98beec7032af2172
--- /dev/null
+++ b/cxxfem/src/femPartition.h
@@ -0,0 +1,25 @@
+#ifndef FEMPARTITION_H
+#define FEMPARTITION_H
+
+#include "fem.h"
+
+namespace fem
+{
+
+/// a partition is a set of parts
+
+class FEM_API Partition
+{
+public:
+    std::vector<Part *> parts;
+
+public:
+    Partition();
+    ~Partition();
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Partition const &obj);
+};
+
+} // namespace fem
+
+#endif // FEMPARTITION_H
diff --git a/cxxfem/src/femPost.cpp b/cxxfem/src/femPost.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a421fb3997faf9f372db711111767a7718179e48
--- /dev/null
+++ b/cxxfem/src/femPost.cpp
@@ -0,0 +1,409 @@
+#include "femPost.h"
+#include "femSolver.h"
+#include "femProblem.h"
+#include "femGmsh.h"
+#include "femGroup.h"
+#include "femEntity.h"
+#include <numeric>
+
+namespace fem
+{
+
+Post::Post(Solver &_solver) : verbose(false), solver(_solver)
+{
+    // global gmsh options
+    gmsh::option::setNumber("General.Orthographic", 0); // better perspective
+    gmsh::option::setNumber("Mesh.VolumeEdges", 0);     // don't display the mesh
+    gmsh::option::setNumber("Mesh.SurfaceEdges", 0);    // don't display the mesh
+}
+
+Post::~Post()
+{
+    if (debug_level() > 0)
+        std::cout << "~Post()\n";
+}
+
+/// rebuild solution vectors for gmsh and add views
+
+void Post::build_views()
+{
+    std::cout << "creating views...\n";
+    // nodal dofs
+    add_dofs_view(solver.pbl, solver.dofs, solver.X, solver.dofPart, solver.dofIdx);
+
+    // nodal vectors
+    add_nodalvector_view(solver.pbl, "displacement", solver.dofs, solver.X, solver.dofPart, solver.dofIdx);
+    add_nodalvector_view(solver.pbl, "force", solver.dofs, solver.f, solver.dofPart, solver.dofIdx);
+
+    // compute heat fluxes, strains, stresses
+
+    std::vector<size_t> etags_all;                              // tags of elements
+    std::vector<std::vector<double>> sigma_all;                 // values of stresses at the nodes of each element
+    std::vector<std::vector<double>> epsilon_all;               // values of strains at the nodes of each element
+    std::map<size_t, std::vector<Eigen::VectorXd>> sig_by_node; // value of all the stress vectors (Voigt) at a given node (key of the dict)
+    std::map<size_t, std::vector<Eigen::VectorXd>> eps_by_node; // same for strain vector
+    solver.compute_sig_epl(etags_all, sigma_all, epsilon_all, sig_by_node, eps_by_node);
+
+    // ...discontinuous by node by element
+    add_tensor_views("stress", etags_all, sigma_all);
+    add_tensor_views("strain", etags_all, epsilon_all);
+
+    // ...continuous averaged nodal fields
+    add_smooth_views("stress", sig_by_node);
+    add_smooth_views("strain", eps_by_node);
+}
+
+/// add 1 scalar view for each dof type to gmsh.
+
+void Post::add_dofs_view(Problem &pbl, DOFs &dofs,
+                         std::vector<Eigen::VectorXd *> &X,
+                         Eigen::ArrayXXi &dofPart,
+                         Eigen::ArrayXXi &dofIdx)
+{
+    // scalar view of each dof
+    for (size_t di = 0; di < dofs.size(); ++di) // TODO: iterator
+    {
+        DOF dof = dofs[di]; // extract each dof type separately
+        std::vector<double> x_all(pbl.nodeTags.size(), 0.0);
+        for (size_t i = 0; i < pbl.nodeTags.size(); ++i)
+        {
+            size_t ntag = pbl.nodeTags[i];
+            size_t gidx = pbl.nodemap[ntag];
+            int part = dofPart(gidx, dof.index());
+            int idx = dofIdx(gidx, dof.index());
+            x_all[i] = (*X[part])[idx];
+        }
+        // Export solution to gmsh
+        std::string name = dofs.name(dof);
+        int sview = add_view(name);
+        gmsh::view::addHomogeneousModelData(sview, 0, "", "NodeData",
+                                            pbl.nodeTags, x_all);
+    }
+}
+
+// add 1 tensor view to gmsh and 6 scalar views for the components.
+// tensor_all is in gmsh format: array of "nbelem" elements (same size as "etags_all")
+// each element contains the tensor data:
+// [a11 a12 a13 a21 a22 a23 a31 a32 a33 a11 a12 a13 a21 a22 a23 a31 a32 a33 ...]
+// <--------------node1--------------> <--------------node2--------------> <--
+
+void Post::add_tensor_views(std::string const &vname,
+                            std::vector<size_t> const &etags_all,
+                            std::vector<std::vector<double>> const &tensor_all)
+{
+    // std::cout << "Post::add_tensor_views " << vname << "\n";
+    // std::cout << "\tetags_all.size() = " << etags_all.size() << "\n";
+    // std::cout << "\ttensor_all.size() = " << tensor_all.size() << "\n";
+
+    // tensor view (by node, by element)
+    std::string name = vname + "_tensor";
+    int sview = add_view(name);
+    gmsh::view::addModelData(sview, 0, "", "ElementNodeData",
+                             etags_all, tensor_all, 0.0, 9);
+
+    // component view (by node, by element)
+    //   components are retrieved from the tensor data in gmsh format
+    // [ [0]     [ [XX]     [ [0] [1] [2]    [ [XX] [XY] [XZ]
+    //   [4]       [YY]       [3] [4] [5]      [XY] [YY] [YZ]
+    //   [8]       [ZZ]       [6] [7] [8] ]    [XZ] [YZ] [ZZ] ]
+    //   [1or3]    [XY]
+    //   [2or6]    [XZ]
+    //   [5or7] ]  [YZ] ]
+
+    std::vector<int> comp_id = {0, 4, 8, 1, 2, 5};
+    std::vector<std::string> comp_name = {"xx", "yy", "zz", "xy", "xz", "yz"};
+    for (size_t i = 0; i < comp_id.size(); ++i)
+    {
+        std::string name = vname + "_" + comp_name[i];
+        int sview = add_view(name);
+        std::vector<std::vector<double>> comp(tensor_all.size());
+        for (size_t ie = 0; ie < tensor_all.size(); ++ie)
+        {
+            comp[ie].resize(tensor_all[ie].size() / 9);
+            for (size_t in = 0; in < tensor_all[ie].size() / 9; ++in)
+                comp[ie][in] = tensor_all[ie][9 * in + comp_id[i]];
+        }
+        gmsh::view::addModelData(sview, 0, "", "ElementNodeData",
+                                 etags_all, comp, 0.0, 1);
+    }
+}
+
+/// add the nodal vector (displacement, forces) as a new gmsh view.
+
+void Post::add_nodalvector_view(Problem &pbl,
+                                std::string const &vname,
+                                DOFs &dofs,
+                                std::vector<Eigen::VectorXd *> &X,
+                                Eigen::ArrayXXi &dofPart,
+                                Eigen::ArrayXXi &dofIdx)
+{
+    std::vector<std::vector<double>> uvw_all(pbl.nodeTags.size());
+    for (size_t i = 0; i < pbl.nodeTags.size(); ++i)
+    {
+        uvw_all[i].resize(3);
+        size_t ntag = pbl.nodeTags[i];
+        size_t gidx = pbl.nodemap[ntag];
+        for (size_t di = 0; di < dofs.size(); ++di)
+        {
+            DOF dof = dofs[di];
+            int part = dofPart(gidx, dof.index());
+            int idx = dofIdx(gidx, dof.index());
+            uvw_all[i][di] = (*X[part])[idx];
+        }
+    }
+    // Export solution to gmsh
+    std::string name = vname + "_vector";
+    int sview = add_view(name);
+    gmsh::view::addModelData(sview, 0, "", "NodeData",
+                             pbl.nodeTags, uvw_all);
+}
+
+/// add 6 scalar views for the components of a smoothed field (stresses or strains)
+///         voigt_by_node is a map such that:
+///         voigt_by_node[nodetag] = array of vectors of stresses/strains (Voigt - 6 comps)
+///                                  evaluated from the neighbouring elements
+
+void Post::add_smooth_views(std::string const &vname,
+                            std::map<size_t, std::vector<Eigen::VectorXd>> &voigt_by_node)
+{
+    std::vector<Eigen::VectorXd> smooth_sig;
+    std::vector<size_t> smooth_ntags;
+
+    std::vector<std::vector<double>> tensors;
+
+    // compute the average
+
+    for (auto const &[ntag, sigs] : voigt_by_node)
+    {
+        // auto s = std::reduce(sigs.begin(), sigs.end()) / sigs.size(); // C++17 / not avail in gcc 8.1 mingw
+        Eigen::VectorXd s0 = Eigen::VectorXd::Zero(6);
+        // std::cout << "ntag=" << ntag << " sigs.size() = " << sigs.size() << std::endl;
+        //  auto s = std::accumulate(sigs.begin(), sigs.end(), s0) / sigs.size(); // core dump sous gcc 9.4 ubuntu ???
+        for (auto &v : sigs)
+            s0 += v;
+        auto s = s0 / sigs.size();
+        // std::cout << "s=" << s << std::endl;
+        smooth_sig.push_back(s);
+        smooth_ntags.push_back(ntag);
+
+        tensors.push_back({s[0], s[3], s[4], s[3], s[1], s[5], s[4], s[5], s[2]});
+    }
+
+    // tensor view (by node)
+    std::string name = "smooth_" + vname + "_tensor";
+    int sview = add_view(name);
+    gmsh::view::addModelData(sview, 0, "", "NodeData",
+                             smooth_ntags, tensors, 0.0, 9);
+
+    // loop over the 6 components
+
+    std::vector<std::string> comps = {"xx", "yy", "zz", "xy", "xz", "yz"};
+
+    for (size_t c = 0; c < comps.size(); ++c)
+    {
+        std::string name = "smooth_" + vname + "_" + comps[c];
+        int sview = add_view(name);
+        std::vector<double> comp;
+        for (auto &s : smooth_sig)
+            comp.push_back(s[c]);
+        gmsh::view::addHomogeneousModelData(sview, 0, "", "NodeData",
+                                            smooth_ntags, comp);
+    }
+
+    // TODO: we could rebuild the average tensor here [DONE]
+    //       we could also calculate the 2nd invariant (Von Mises)
+}
+
+/// add a new gmsh view and configure it
+/// store the view in a dict called 'views' so that the views can be easily configured
+/// later by the user using their names
+
+int Post::add_view(std::string const &name)
+{
+    // if(self.verbose): print(f'\tadding new view: {name}')
+
+    int sview = gmsh::view::add(name);
+    // sview = 1 for the first view although it is named Views[0] in the GUI and the options!!
+    views[name] = sview;
+    configure(sview);
+    return sview;
+}
+
+void Post::set_camera_3D()
+{
+    gmsh::option::setNumber("General.TrackballQuaternion0", 0.62);
+    gmsh::option::setNumber("General.TrackballQuaternion1", 0.17);
+    gmsh::option::setNumber("General.TrackballQuaternion2", 0.20);
+    gmsh::option::setNumber("General.TrackballQuaternion3", 0.74);
+}
+
+void Post::deform(double factor)
+{
+    int v_disp = views["displacement_vector"];
+    for (auto const &[vname, v] : views)
+    {
+        if (v != v_disp)
+        {
+            gmsh::option::setNumber("View[" + std::to_string(v - 1) + "].UseGeneralizedRaise", 1);
+            gmsh::option::setNumber("View[" + std::to_string(v - 1) + "].GeneralizedRaiseView", v_disp - 1);
+            gmsh::option::setNumber("View[" + std::to_string(v - 1) + "].GeneralizedRaiseFactor", factor);
+        }
+    }
+
+    // v_disp = self.views['displacement_vector']
+    // for vname in self.views:
+    //     v = self.views[vname]
+    //     if v != v_disp:
+    //         gmsh.option.setNumber(f"View[{v-1}].UseGeneralizedRaise", 1)
+    //         gmsh.option.setNumber(f"View[{v-1}].GeneralizedRaiseView", v_disp-1)
+    //         gmsh.option.setNumber(f"View[{v-1}].GeneralizedRaiseFactor", factor)
+}
+
+void Post::configure(int sview)
+{
+    // scalar views
+    gmsh::option::setNumber("View[" + std::to_string(sview - 1) + "].IntervalsType", 3); // filled iso
+    gmsh::option::setNumber("View[" + std::to_string(sview - 1) + "].NbIso", 20);        //
+
+    // vector views
+    gmsh::option::setNumber("View[" + std::to_string(sview - 1) + "].GlyphLocation", 2); // at the node (default=barycenter)
+    gmsh::option::setNumber("View[" + std::to_string(sview - 1) + "].VectorType", 2);    // simple Arrow (default=3D arrow)
+
+    gmsh::option::setNumber("View[" + std::to_string(sview - 1) + "].ShowElement", 1);  // view black mesh outline
+    gmsh::option::setNumber("View[" + std::to_string(sview - 1) + "].DrawSkinOnly", 1); // display skin only (should be faster)
+    gmsh::option::setNumber("View[" + std::to_string(sview - 1) + "].Visible", 0);      // hide all views
+}
+
+/// display results
+
+void Post::view()
+{
+    std::cout << "viewing results...\n";
+    gmsh::fltk::run();
+}
+
+void Post::show_views(std::vector<std::string> const &names)
+{
+    for (auto &v : names)
+    {
+        auto it = views.find(v);
+        if (it != views.end())
+            gmsh::option::setNumber("View[" + std::to_string(it->second - 1) + "].Visible", 1);
+        else
+            std::cout << "WARNING: view named \"" << v << "\" does not exist!\n";
+    }
+}
+
+/// write mesh/data to disk
+
+void Post::write()
+{
+    gmsh::option::setNumber("Mesh.Binary", 1); // writes files in binary format
+
+    gmsh::write("mesh.opt"); //  save the options
+    gmsh::write("mesh.msh"); //  only save the mesh
+    for (auto const &[name, sview] : views)
+    {
+        // gmsh::view::write(sview, name+".pos"); // pos format: list-based data (indep. of any model)
+        gmsh::view::write(sview, name + ".msh"); // writes the mesh in each file - each view is in a separate file
+        // gmsh::view::write(sview, "mesh.msh", true); // single file, but the mesh is repeated
+    }
+}
+
+std::vector<double>
+Post::probe(std::string const &name, std::string const &grpname)
+{
+    // std::cout << "Post::probe(" << grpname << ")\n";
+    // get physical group from grpname
+    Group *grp = solver.pbl.groups_by_name.at(grpname); // TODO: better handling of errors
+
+    // get the view
+    auto it = views.find(name);
+    if (it == views.end())
+        throw std::runtime_error("WARNING: view named \"" + name + "\" does not exist!");
+    int nview = it->second;
+
+    // probe values
+    std::vector<double> all_values;
+
+    std::vector<std::size_t> nodeTags;
+    std::vector<double> coord;
+    gmsh::model::mesh::getNodesForPhysicalGroup(grp->dim, grp->tag, nodeTags, coord);
+    // j'ai essayé de boucler sur les entités du groupe et faire un getNode de l'entité 
+    // mais ça ne me retourne rien (?)
+
+    // std::cout << "nodeTags.size()=" << nodeTags.size() << '\n';
+
+    for (size_t i = 0; i < nodeTags.size(); ++i)
+    {
+        std::vector<double> values;
+        double distance = 0.0;
+        int step = -1;
+        int numComp = -1; // do not use numComp=comp (gmsh returns [ ] for comp!=0 )
+        bool gradient = false;
+        double distanceMax = -1.; // find closest node
+        gmsh::view::probe(nview, coord[i * 3], coord[i * 3 + 1], coord[i * 3 + 2], values,
+                          distance, step, numComp, gradient, distanceMax);
+
+        // debug
+        // std::cout << "probing " << name << " at node #" << nodeTags[i] << ":";
+        // std::cout << "\tnview: " << nview << '\n';
+        // std::cout << "\tcoords: " << coord[i * 3] << ", " << coord[i * 3 + 1] << ", " << coord[i * 3 + 2] << '\n';
+        // std::cout << "\tgot from gmsh: " << values << '\n';
+        // std::cout << "\tdistance = " << distance << std::endl;
+
+        all_values.insert(all_values.end(), values.begin(), values.end());
+    }
+
+    return all_values;
+}
+
+/// Probe a field at a given nodetag
+
+std::vector<double>
+Post::probe(std::string const &name, size_t nodetag)
+{
+    // get the view
+    auto it = views.find(name);
+    if (it == views.end())
+        throw std::runtime_error("WARNING: view named \"" + name + "\" does not exist!");
+    int nview = it->second;
+
+    // retrieve node coordinates
+    std::vector<double> coord;
+    std::vector<double> parametricCoord; // unused
+    int dim;                             // unused
+    int tag;                             // unused
+    gmsh::model::mesh::getNode(nodetag, coord, parametricCoord, dim, tag);
+
+    // probe value
+
+    std::vector<double> values;
+    double distance = 0.0;
+    int step = -1;
+    int numComp = -1; // do not use numComp=comp (gmsh returns [ ] for comp!=0 )
+    bool gradient = false;
+    double distanceMax = -1.; // find closest node
+    gmsh::view::probe(nview, coord[0], coord[1], coord[2], values, distance, step, numComp, gradient, distanceMax);
+
+    // debug
+    // std::cout << "probing " << name << " at node #" << nodetag << ":";
+    // std::cout << "\tnview: "<< nview << '\n';
+    // std::cout << "\tcoords: "<< coord << '\n';
+    // std::cout << "\tgot from gmsh: "<< values << '\n';
+    // std::cout << "\tdistance = "<< distance << std::endl;
+
+    // std::cout << "probing \"" << name << "\" (comp=" << comp
+    //           << ") at node #" << nodetag << ": " << values[comp] << std::endl;
+
+    return values;
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Post const &obj)
+{
+    out << "Post\n";
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femPost.h b/cxxfem/src/femPost.h
new file mode 100644
index 0000000000000000000000000000000000000000..1f48d93f831f1fc8731c9078a85c6342762ed243
--- /dev/null
+++ b/cxxfem/src/femPost.h
@@ -0,0 +1,64 @@
+#ifndef FEMPOST_H
+#define FEMPOST_H
+
+#include "fem.h"
+#include <map>
+#include <list>
+#include <Eigen/Dense>
+
+namespace fem
+{
+
+///
+
+class FEM_API Post
+{
+public:
+#ifndef SWIG
+    bool verbose;
+    Solver &solver;
+    std::map<std::string, int> views;
+#endif
+
+public:
+    Post(Solver &_solver);
+    ~Post();
+
+    void build_views();
+    void view();
+    void show_views(std::vector<std::string> const &names);
+    void set_camera_3D();
+    void deform(double factor=1.0);
+    void write();
+    
+    std::vector<double> probe(std::string const &name, std::string const &grpname);    
+    std::vector<double> probe(std::string const &name, size_t nodetag);
+
+#ifndef SWIG
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Post const &obj);
+#endif
+
+private:
+    void add_dofs_view(Problem &pbl, DOFs &dofs,
+                       std::vector<Eigen::VectorXd *> &X,
+                       Eigen::ArrayXXi &dofPart,
+                       Eigen::ArrayXXi &dofIdx);
+    void add_nodalvector_view(Problem &pbl,
+                              std::string const &vname,
+                              DOFs &dofs,
+                              std::vector<Eigen::VectorXd *> &X,
+                              Eigen::ArrayXXi &dofPart,
+                              Eigen::ArrayXXi &dofIdx);
+    void add_tensor_views(std::string const &vname,
+                          std::vector<size_t> const &etags_all,
+                          std::vector<std::vector<double>> const &tensor_all);
+    void add_smooth_views(std::string const &vname,
+                          std::map<size_t, std::vector<Eigen::VectorXd>> &voigt_by_node);
+
+    int add_view(std::string const &name);
+    void configure(int sview);
+};
+
+} // namespace fem
+
+#endif // FEMPOST_H
diff --git a/cxxfem/src/femProblem.cpp b/cxxfem/src/femProblem.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d120a18cf008cc58a34436b8583b1ee724f5695
--- /dev/null
+++ b/cxxfem/src/femProblem.cpp
@@ -0,0 +1,148 @@
+#include "femProblem.h"
+#include "femEntity.h"
+#include "femElementSet.h"
+#include "femGroup.h"
+#include "femMedium.h"
+#include "femDirichlet.h"
+#include "femNeumann.h"
+#include "femGmsh.h"
+#include <fstream>
+
+namespace fem
+{
+
+Problem::Problem() : quadrature("Gauss2")
+{
+    gmsh::initialize();
+    // General.Verbosity
+    // (0: silent except for fatal errors, 1: +errors, 2: +warnings, 3: +direct, 4: +information, 5: +status (default), 99: +debug)
+    gmsh::option::setNumber("General.Verbosity", 2);
+    gmsh::option::setNumber("General.Terminal", 1); // Should information be printed on the terminal? 0: no (default) ; 1: yes
+}
+
+Problem::~Problem()
+{
+    for (auto &e : entities)
+        delete e;
+}
+
+/// fill internal helper objects from gmsh data
+
+void Problem::sync()
+{
+    // -- entities
+    std::cout << "retrieving entities...\n";
+    gmsh::vectorpair dimTags;
+    gmsh::model::getEntities(dimTags);
+    for (auto &p : dimTags)
+        entities.push_back(new Entity(p.first, p.second));
+    // create useful maps
+    for (auto e : entities)
+        entities_by_dimtag[std::make_pair(e->dim, e->tag)] = e;
+
+    // -- physical groups
+    std::cout << "retrieving physical groups...\n";
+    dimTags.clear();
+    gmsh::model::getPhysicalGroups(dimTags);
+    for (auto &p : dimTags)
+        groups.push_back(new Group(p.first, p.second));
+    // create useful maps
+    for (auto g : groups)
+    {
+        groups_by_dimtag[std::make_pair(g->dim, g->tag)] = g;
+        groups_by_name[g->name] = g;
+    }
+
+    // link entities <=> groups
+    for (auto g : groups)
+        g->link_to_entities(entities_by_dimtag);
+    for (auto e : entities)
+        e->link_to_groups(groups_by_dimtag);
+
+    // -- nodes
+    // get all the node tags and create a map ("node tag" => "index")
+    // that could be used to store things at nodes in separate arrays
+    std::vector<double> coord;
+    std::vector<double> parametricCoord;
+    gmsh::model::mesh::getNodes(nodeTags, coord, parametricCoord);
+    for (size_t i = 0; i < nodeTags.size(); ++i)
+        nodemap[nodeTags[i]] = i;
+
+    //std::cout << *this;
+}
+
+void Problem::load(std::string const &filename,
+                   std::map<std::string, double> const &pars,
+                   int order)
+{
+    // std::cout << "pars=\n";
+    // for (auto &p : pars)
+    //     std::cout << p.first << " : " << p.second << '\n';
+
+    std::string fext;
+    auto idx = filename.rfind('.');
+    if(idx != std::string::npos)
+        fext = filename.substr(idx);
+
+    if (fext!=".geo" && fext!=".msh")
+        throw std::runtime_error("unknown file extension ("+fext+")");
+
+    // load file and initialise gmsh
+    // gmsh::initialize();
+    for (auto &p : pars)
+    {
+        if(debug_level()>1)
+            std::cout << "setting parameter " << p.first << "=" << p.second << '\n';
+        std::vector<double> v;
+        v.push_back(p.second);
+        gmsh::onelab::setNumber(p.first, v);
+    }
+    std::cout << "loading " << filename << '\n';
+    if(! std::ifstream(filename.c_str()).good() ) // not tested by gmsh
+        throw std::runtime_error("file " + filename + " does not exist");
+    gmsh::open(filename);
+
+    // generate mesh
+    if (fext==".geo")
+    {
+        gmsh::model::mesh::generate(3);
+        gmsh::model::mesh::setOrder(order); // after generation
+        quadrature = "Gauss" + std::to_string(2*order); // change default quadrature rule 
+    }
+
+    // do not sync now, so that we can add things such as
+    // additional physical groups
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Problem const &obj)
+{
+    out << "Problem:\n";
+    out << '\t' << obj.entities.size() << " entities\n";
+    if(debug_level()>1)
+        for (auto e : obj.entities)
+        {
+            out << "\t\t" << *e << '\n';
+            for (auto m : e->esets)
+                out << "\t\t\tmesh: " << *m << '\n';
+        }
+    out << '\t' << obj.groups.size() << " groups\n";
+    if(debug_level()>1)
+        for (auto g : obj.groups)
+            out << "\t\t" << *g << '\n';
+    out << '\t' << obj.media.size() << " media\n";
+    if(debug_level()>1)
+        for (auto m : obj.media)
+            out << "\t\t" << *m << '\n';
+    out << '\t' << obj.dirichlets.size() << " dirichlets\n";
+    if(debug_level()>1)
+        for (auto bc : obj.dirichlets)
+            out << "\t\t" << *bc << '\n';
+    out << '\t' << obj.neumanns.size() << " neumanns\n";
+    if(debug_level()>1) // TODO: si >20 ... afficher les 10 premiers et 10 derniers   
+        for (auto bc : obj.neumanns)
+            out << "\t\t" << *bc << '\n';
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femProblem.h b/cxxfem/src/femProblem.h
new file mode 100644
index 0000000000000000000000000000000000000000..fec3242b5ff48f485a8d3340cb5b25c7343edf8f
--- /dev/null
+++ b/cxxfem/src/femProblem.h
@@ -0,0 +1,50 @@
+#ifndef FEMPROBLEM_H
+#define FEMPROBLEM_H
+
+#include "fem.h"
+#include <map>
+
+namespace fem
+{
+
+/// contains:
+/// - entities: a list of entities
+/// - groups: a list of physical groups
+
+class FEM_API Problem
+{
+public:
+#ifndef SWIG
+    std::vector<Entity *> entities;
+    std::map<std::pair<int, int>, Entity *> entities_by_dimtag;
+
+    std::vector<Group *> groups;
+    std::map<std::pair<int, int>, Group *> groups_by_dimtag;
+    std::map<std::string, Group *> groups_by_name;
+
+    std::vector<std::size_t> nodeTags;
+    std::map<size_t, size_t> nodemap; ///< node tag => index in "nodeTags"
+
+    std::vector<Medium *> media;
+    std::vector<Dirichlet *> dirichlets;
+    std::vector<Neumann *> neumanns;
+
+    std::string quadrature;
+#endif
+public:
+    Problem();
+    ~Problem();
+
+    void sync();
+    void load(std::string const &filename,
+              std::map<std::string, double> const &pars, 
+              int order=1);
+
+#ifndef SWIG
+        friend FEM_API std::ostream &operator<<(std::ostream &out, Problem const &obj);
+#endif
+};
+
+} // namespace fem
+
+#endif // FEMPROBLEM_H
diff --git a/cxxfem/src/femSolver.cpp b/cxxfem/src/femSolver.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ec1058bb7f8ffec5816d563a9b0bdaf5737c9d60
--- /dev/null
+++ b/cxxfem/src/femSolver.cpp
@@ -0,0 +1,1008 @@
+#include "femSolver.h"
+#include "femProblem.h"
+#include "femPartition.h"
+#include "femPart.h"
+#include "femDirichlet.h"
+#include "femNeumann.h"
+#include "femGroup.h"
+#include "femEntity.h"
+#include "femElementSet.h"
+#include "femMedium.h"
+#include "femMaps.h"
+#include "OpenMP.h"
+#include "femGmsh.h"
+#include <iomanip>
+#include <algorithm>
+#include <Eigen/IterativeLinearSolvers>
+#ifdef FEM_USE_MKL
+#include <Eigen/PardisoSupport>
+#endif
+
+namespace fem
+{
+
+Solver::Solver(Problem &_pbl) : pbl(_pbl),
+                                partition(nullptr),
+                                part_Mfree(nullptr),
+                                part_Mfixed(nullptr),
+                                linear_solver("ldlt")
+{
+    dofs.add("X");
+    dofs.add("Y");
+    dofs.add("Z");
+    bcs.add("FREE");
+    bcs.add("PRESCRIBED");
+
+    std::cout << "#threads = " << OpenMP::get_max_threads() << '\n';
+}
+
+Solver::~Solver()
+{
+    if(debug_level()>0)
+        std::cout << "~Solver()\n";
+    if (partition)
+        delete partition;
+    if (part_Mfree)
+        delete part_Mfree;
+    if (part_Mfixed)
+        delete part_Mfixed;
+
+    // free sparse K
+    for (size_t i = 0; i < K.size(); ++i)
+        for (size_t j = 0; j < K[i].size(); ++j)
+            delete K[i][j];
+
+    // free f, X
+    for (size_t i = 0; i < f.size(); ++i)
+        delete f[i];
+    for (size_t i = 0; i < f.size(); ++i)
+        delete X[i];
+}
+
+/// solve the problem
+
+void Solver::solve()
+{
+    std::cout << pbl;
+
+    create_partition();
+    fill_partition();
+    fill_rhs();
+    fill_lhs();
+    apply_bc();
+    solve_static();
+
+    std::cout << "Summary of CPU times:\n";
+
+    std::ios cout_state(nullptr);
+    cout_state.copyfmt(std::cout);
+
+    std::cout << std::fixed << std::setprecision(1);
+    for (auto const &[name, timer] : timers)
+    {
+        std::cout << '\t' << std::setw(15) << std::left << name << ":"
+                  << std::setw(10) << std::right << timer.read() << '\n';
+    }
+
+    std::cout.copyfmt(cout_state);
+}
+
+// create a partition of the dofs
+
+void Solver::create_partition()
+{
+    partition = new Partition();
+    part_Mfree = new Part(*partition, dofs["X"] | dofs["Y"] | dofs["Z"], bcs["FREE"]);
+    part_Mfixed = new Part(*partition, dofs["X"] | dofs["Y"] | dofs["Z"], bcs["PRESCRIBED"]);
+    std::cout << *partition;
+}
+
+// sort nodes & dofs into the defined partition
+// output:
+//     dofFixed: is a dof fixed or free?
+//     dofPart: in which part is a given dof of a given node?
+//     dofIdx: what is its number in its part?
+//     partSizes: what are the size of the parts in the partition?
+
+void Solver::fill_partition()
+{
+    timers["fill_partition"].start();
+
+    std::cout << "fill_partition:\n";
+
+    // pbl.nodeTags (size: nb of nodes): gives the gmsh tag for each node
+    // pbl.nodemap (size: max gmsh tag): gives the global node idx (position in nodeTags)
+    //                                   from the gmsh tag
+    //                                   (some space could be unused if non-contiguous numbering)
+    //
+    // dofPart (size: nb of nodes x size of dofs): for each node/dof, gives its partition number
+    // dofIdx  (size: nb of nodes x size of dofs): for each node/dof, gives its index in its partition
+
+    // fill the 'dofFixed' array:
+    //             T  X  Y  Z
+    //  node 0   [ x  x  x  x ]
+    //  node 1   [ x  x  x  x ]  where 'x' is PRESCRIBED or FREE
+    //  node 2   [ x  x  x  x ]
+    //  ...
+
+    dofFixed.setConstant(pbl.nodeTags.size(), dofs.size(), 0); // = false == FREE by default
+
+    for (auto &bc : pbl.dirichlets)
+    {
+        DOF bc_dof = dofs[bc->m_dofname];
+        for (auto &entity : bc->group->entities)
+            for (auto &elems : entity->esets)
+                for (size_t i = 0; i < elems->etags.size(); ++i)
+                    for (int j = 0; j < elems->type_nnods; ++j)
+                    {
+                        // size_t n = elems->ntags_v[i * elems->type_nnods + j];
+                        size_t n = elems->ntags(i, j);
+                        size_t gidx = pbl.nodemap[n];
+                        dofFixed(gidx, bc_dof.index()) = 1; // PRESCRIBED
+                    }
+    }
+
+    Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+    // std::cout << "dofFixed =\n"
+    //           << dofFixed.format(fmt) << '\n';
+
+    // fill dofPart (default:-1 => no partition assigned)
+    //             T  X  Y  Z
+    //  node 0   [ x  x  x  x ]
+    //  node 1   [ x  x  x  x ]  where 'x' is the partition of the dof
+    //  node 2   [ x  x  x  x ]
+    //  ...
+
+    dofPart.setConstant(pbl.nodeTags.size(), dofs.size(), -1);
+
+    for (size_t i = 0; i < pbl.nodeTags.size(); ++i)
+        for (size_t j = 0; j < dofs.size(); ++j) // TODO: iterator
+        {
+            DOF dof = dofs[j];
+            DOF bc = dofFixed(i, j) ? bcs["PRESCRIBED"] : bcs["FREE"];
+            for (auto &p : partition->parts)
+                if ((p->dofmask & dof) && (p->bcmask & bc))
+                {
+                    dofPart(i, j) = p->idx;
+                    break;
+                }
+        }
+
+    // std::cout << "dofPart =\n"
+    //           << dofPart.format(fmt) << '\n';
+
+    // count nodes in partitions
+    int nnods_Mfree = (dofPart == part_Mfree->idx).count();
+    std::cout << "\tfree M dofs = " << nnods_Mfree << '\n';
+    int nnods_Mfixed = (dofPart == part_Mfixed->idx).count();
+    std::cout << "\tfixed M dofs = " << nnods_Mfixed << std::endl;
+
+    // renumber nodes in each partition
+    // fill dofIdx (default:-1 => no part assigned)
+    //             T  X  Y  Z
+    //  node 0   [ x  x  x  x ]
+    //  node 1   [ x  x  x  x ]  where 'x' is the index of the dof in its partition
+    //  node 2   [ x  x  x  x ]
+    //  ...
+    // fill also 'partSizes' (nb of dofs in each part)
+
+    dofIdx.setConstant(pbl.nodeTags.size(), dofs.size(), 0);
+    partSizes.resize(partition->parts.size(), 0);
+
+    for (size_t i = 0; i < pbl.nodeTags.size(); ++i)
+        for (size_t j = 0; j < dofs.size(); ++j) // TODO: iterator
+        {
+            int p = dofPart(i, j);
+            dofIdx(i, j) = partSizes[p];
+            partSizes[p]++;
+        }
+
+    // std::cout << "dofIdx =\n"
+    //           << dofIdx.format(fmt) << '\n';
+    // std::cout << "partSizes = " << partSizes << std::endl;
+
+    // TODO: check that all dofs are assigned?
+    timers["fill_partition"].stop();
+    std::cout << "\tcpu = " << timers["fill_partition"].read() << "s\n";
+}
+
+/// fill right-hand side of the system of equations
+
+void Solver::fill_rhs()
+{
+    timers["fill_rhs"].start();
+
+    std::cout << "assembly of LHS...\n";
+
+    // -- prepare structural matrices
+    //
+    //  Kijv[nparts][nparts], Cijv[nparts][nparts]:
+    //       "ijv" versions of K, C and M used during assembly
+
+    std::vector<std::vector<std::vector<Eigen::Triplet<double>> *>> Kijv(partition->parts.size());
+    for (size_t i = 0; i < partition->parts.size(); ++i)
+    {
+        Kijv[i].resize(partition->parts.size());
+        for (size_t j = 0; j < partition->parts.size(); ++j)
+            Kijv[i][j] = new std::vector<Eigen::Triplet<double>>;
+    }
+
+    // -- pre-compute sub-matrices structures
+    std::vector<std::vector<size_t>> ntriplets(partition->parts.size(), std::vector<size_t>(partition->parts.size(), 0));
+
+    std::vector<DOF> dofs_M = {dofs["X"], dofs["Y"], dofs["Z"]};
+    for (auto medium : pbl.media)
+        for (auto entity : medium->group->entities)
+            for (auto elems : entity->esets)
+            {
+                const int ndim = elems->type_dim;
+                for (size_t ie = 0; ie < elems->etags.size(); ++ie)
+                {
+                    // brutal assembly (needs optimisation)
+                    for (int i = 0; i < elems->type_nnods; ++i)
+                    {
+                        int i_tag = elems->ntags(ie, i);   // gmsh tag of node i
+                        int i_gidx = pbl.nodemap[i_tag];   // global index of node i
+                        auto i_part = dofPart.row(i_gidx); // parts of dofs of node i
+                        for (int j = 0; j < elems->type_nnods; ++j)
+                        {
+                            int j_tag = elems->ntags(ie, j);   // gmsh tag of node j
+                            int j_gidx = pbl.nodemap[j_tag];   // global index of node j
+                            auto j_part = dofPart.row(j_gidx); // parts of dofs of node j
+
+                            for (int ni = 0; ni < ndim; ++ni)
+                            {
+                                DOF &di = dofs_M[ni];
+                                for (int nj = 0; nj < ndim; ++nj)
+                                {
+                                    DOF &dj = dofs_M[nj];
+
+                                    int ip = i_part(di.index());
+                                    int jp = j_part(dj.index());
+                                    ntriplets[ip][jp]++;
+                                }
+                            }
+                        }
+                    }
+                }
+            }                    
+
+    // reserve the correct amount of triplets 
+    // (avoids reallocation in later push_backs)
+    for (size_t i = 0; i < partition->parts.size(); ++i)
+    {
+        Kijv[i].resize(partition->parts.size());
+        for (size_t j = 0; j < partition->parts.size(); ++j)
+        {
+            std::cout << "expecting " << ntriplets[i][j] 
+                      << " triplets for submatrix " << i << "," << j << '\n';
+            Kijv[i][j]->reserve(ntriplets[i][j]);
+        }
+    }
+
+    // -- compute elemental matrices
+
+    for (auto medium : pbl.media)
+        for (auto entity : medium->group->entities)
+            for (auto elems : entity->esets)
+            {
+                std::cout << "\tprocessing " << *elems << '\n';
+                size_t nelems = elems->etags.size();
+                std::cout << "\t\tnelems = " << nelems << '\n';
+                // integration points
+                std::vector<double> localCoord;
+                std::vector<double> weights;
+                gmsh::model::mesh::getIntegrationPoints(elems->type_id,
+                                                        pbl.quadrature,
+                                                        localCoord,
+                                                        weights);
+                size_t nGP = weights.size();
+                std::cout << "\t\tnGP = " << nGP << '\n';
+                // jacobians
+                std::vector<double> jacobians_v;
+                std::vector<double> determinants_v;
+                std::vector<double> coord_v;
+                gmsh::model::mesh::getJacobians(elems->type_id, localCoord,
+                                                jacobians_v, determinants_v,
+                                                coord_v, entity->tag);
+                // reshape (map) into matrices
+                JacoMap jacobians(jacobians_v, nelems, nGP);
+                Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>
+                    determinants(&determinants_v[0], nelems, nGP);
+                CoordMap coord(coord_v, nelems, nGP);
+
+                // print GP data
+                // for (size_t i = 0; i < nelems; ++i)
+                // {
+                //     std::cout << "\telem #" << i << " (tag=" << elems->ntags[i] << "):\n";
+                //     for (size_t j = 0; j < nGP; ++j)
+                //     {
+                //         std::cout << "\t\tGP #" << j + 1 << '/' << nGP << ":\n";
+                //         Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t\t\t\t[", "]");
+                //         std::cout << "\t\t\tJ =\n"
+                //                   << jacobians(i, j).format(fmt) << '\n';
+                //         std::cout << "\t\t\tdet = " << jacobians(i, j).determinant()
+                //                   << " (gmsh = " << determinants(i, j) << ")\n";
+                //         std::cout << "\t\t\tcoord =\n"
+                //                   << coord(i, j).format(fmt) << '\n';
+                //     }
+                // }
+
+                // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                // std::cout << "determinants =\n"
+                //         << determinants.format(fmt) << '\n';
+                // std::cout << "determinants_v =\n" << determinants_v << '\n';
+                // std::cout << "coord_v =\n" << coord_v << '\n';
+                // std::cout << "coord_v.size() =" << coord_v.size() << '\n';
+
+                // basis functions
+                int numComponents; // unused
+                std::vector<double> basisF_v;
+                int numOrientations; // unused
+                gmsh::model::mesh::getBasisFunctions(elems->type_id,
+                                                     localCoord, "Lagrange",
+                                                     numComponents, // unused
+                                                     basisF_v,
+                                                     numOrientations); // unused
+                Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>
+                    basisF(&basisF_v[0], nGP, elems->type_nnods);
+                // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                // std::cout << "basisF =\n"
+                //         << basisF.format(fmt) << '\n';
+                // std::cout << "basisF_v =\n"
+                //         << basisF_v << '\n';
+                // exit(1);
+
+                // basis functions (derivatives)
+                std::vector<double> basisDF_v;
+                gmsh::model::mesh::getBasisFunctions(elems->type_id,
+                                                     localCoord, "GradLagrange",
+                                                     numComponents, // reused
+                                                     basisDF_v,
+                                                     numOrientations); // reused
+                DShapeMap basisDF(basisDF_v, nGP, elems->type_nnods);
+                // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                // std::cout << "basisDF =\n"
+                //           << basisDF(0).format(fmt) << '\n';
+                // std::cout << "basisDF_v =\n"
+                //           << basisDF_v << '\n';
+                // exit(1);
+
+                #ifdef _MSC_VER
+                #define OPENMP_INT int
+                #else
+                #define OPENMP_INT size_t
+                #endif
+
+                // loop over the elements
+                #pragma omp parallel for
+                for (OPENMP_INT ie = 0; ie < elems->etags.size(); ++ie)
+                {
+                    // std::cout << "\tprocessing element #" << elems->etags[ie] << '\n';
+                    const int ndim = elems->type_dim;
+
+                    // compute elemental matrices
+                    Eigen::MatrixXd Ke_MM = Eigen::MatrixXd::Zero(ndim * elems->type_nnods, ndim * elems->type_nnods);
+
+                    // loop over Gauss points
+                    for (size_t igp = 0; igp < nGP; ++igp)
+                    {
+                        auto N_T = basisF.row(igp); // matrix of shape functions; shape=(1,elems.type_nnods)
+                        // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                        // std::cout << "N_T (igp=" << igp << ") = " << N_T.format(fmt) << '\n';
+                        auto DN = basisDF(igp).block(0, 0, elems->type_nnods, ndim); // matrix of shp fct derivatives in 'xi' space; shape=(elems.type_nnods,3)
+                        // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                        // std::cout << "DN (igp=" << igp << ") =\n"
+                        //           << DN.format(fmt) << '\n';
+                        auto J = jacobians(ie, igp).block(0, 0, ndim, ndim);
+                        auto Jt = J.transpose();   // transposed jacobian matrix; shape=(3,3)
+                        auto Jtinv = Jt.inverse(); // transposed jacobian inverse; shape=(3,3)
+                        // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                        // std::cout << "Jt*Jtinv = " << (Jt*Jtinv).format(fmt) << '\n';
+                        auto B_T = Jtinv * DN.transpose(); // matrix of shp fct derivatives in 'x' space; shape=(3, nnods_by_element)
+                        // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                        // std::cout << "B_T = " << B_T.format(fmt) << '\n';
+                        // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                        // std::cout << "basisF = " << basisF.format(fmt) << '\n';
+
+                        Eigen::MatrixXd N_M(ndim, elems->type_nnods * ndim);
+                        for (int j = 0; j < elems->type_nnods; ++j)
+                        {
+                            // double v = basisF(igp, j);
+                            double v = N_T(j);
+                            // std::cout << "v = " << v << '\n';
+                            N_M.block(0, ndim * j, ndim, ndim) = Eigen::MatrixXd::Identity(ndim, ndim) * v;
+                        }
+                        // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                        // std::cout << "N_M = " << N_M.format(fmt) << '\n';
+
+                        // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                        // std::cout << "B_T = " << B_T.format(fmt) << '\n';
+
+                        Eigen::MatrixXd B_M;
+
+                        switch (ndim)
+                        {
+                        case 2:
+
+                            B_M.resize(3, elems->type_nnods * ndim); // 3 en 2D; 6 en 3D
+                            for (int j = 0; j < elems->type_nnods; ++j)
+                            {
+                                double ddx = B_T(0, j);
+                                double ddy = B_T(1, j);
+                                Eigen::MatrixXd Bs(3, 2);
+                                Bs << ddx, 0.0,
+                                    0.0, ddy,
+                                    ddy, ddx;
+                                B_M.block(0, ndim * j, 3, 2) = Bs;
+                            }
+                            break;
+                        case 3:
+
+                            B_M.resize(6, elems->type_nnods * ndim); // 3 en 2D; 6 en 3D
+                            for (int j = 0; j < elems->type_nnods; ++j)
+                            {
+                                // double v = basisF(igp, j);
+                                double ddx = B_T(0, j);
+                                double ddy = B_T(1, j);
+                                double ddz = B_T(2, j);
+                                Eigen::MatrixXd Bs(6, 3);
+                                Bs << ddx, 0.0, 0.0,
+                                    0.0, ddy, 0.0,
+                                    0.0, 0.0, ddz,
+                                    ddy, ddx, 0.0,
+                                    ddz, 0.0, ddx,
+                                    0.0, ddz, ddy;
+                                // std::cout << "v = " << v << '\n';
+                                B_M.block(0, ndim * j, 6, 3) = Bs;
+                            }
+                            break;
+                        default:
+                            throw std::runtime_error("dimension " + std::to_string(ndim) + " not implemented");
+                        }
+                        // std::cout << "B_M = " << B_M.format(fmt) << '\n';
+
+                        Ke_MM += B_M.transpose() * medium->hooke(ndim) * B_M * determinants(ie, igp) * weights[igp];
+                        //
+                        // exit(1);
+                    }
+                    // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                    // std::cout << "Ke_MM = \n"
+                    //           << Ke_MM.format(fmt) << '\n';
+                    // exit(1);
+
+                    std::vector<DOF> dofs_M = {dofs["X"], dofs["Y"], dofs["Z"]};
+
+                    // brutal assembly (needs optimisation)
+                    #pragma omp critical
+                    for (int i = 0; i < elems->type_nnods; ++i)
+                    {
+                        int i_tag = elems->ntags(ie, i);   // gmsh tag of node i
+                        int i_gidx = pbl.nodemap[i_tag];   // global index of node i
+                        auto i_part = dofPart.row(i_gidx); // parts of dofs of node i
+                        auto i_idx = dofIdx.row(i_gidx);   // index of these dofs in their parts
+                        for (int j = 0; j < elems->type_nnods; ++j)
+                        {
+                            int j_tag = elems->ntags(ie, j);   // gmsh tag of node j
+                            int j_gidx = pbl.nodemap[j_tag];   // global index of node j
+                            auto j_part = dofPart.row(j_gidx); // parts of dofs of node j
+                            auto j_idx = dofIdx.row(j_gidx);   // index of these dofs in their parts
+
+                            for (int ni = 0; ni < ndim; ++ni)
+                            {
+                                DOF &di = dofs_M[ni];
+                                for (int nj = 0; nj < ndim; ++nj)
+                                {
+                                    DOF &dj = dofs_M[nj];
+                                    // std::cout << "(" << di.index() << ',' << dj.index() << ")\n";
+                                    // get vector of triplets
+                                    auto K_sp = Kijv[i_part(di.index())][j_part(dj.index())];
+                                    K_sp->push_back(Eigen::Triplet<double>(
+                                        i_idx(di.index()),
+                                        j_idx(dj.index()),
+                                        Ke_MM(i * ndim + ni, j * ndim + nj)));
+                                }
+                            }
+                        }
+                    } // assembly
+
+                } // loop over the elements
+            }     // for (auto &elems : entity->esets)
+
+    // create sparse K
+
+    K.resize(partition->parts.size());
+    for (size_t i = 0; i < partition->parts.size(); ++i)
+    {
+        K[i].resize(partition->parts.size());
+        for (size_t j = 0; j < partition->parts.size(); ++j)
+        {
+            K[i][j] = new Eigen::SparseMatrix<double>(partSizes[i], partSizes[j]);
+            K[i][j]->setFromTriplets(Kijv[i][j]->begin(), Kijv[i][j]->end()); // TODO: use std::list!
+        }
+    }
+
+    // free triplets
+    for (size_t i = 0; i < Kijv.size(); ++i)
+        for (size_t j = 0; j < Kijv[i].size(); ++j)
+            delete Kijv[i][j];
+
+    timers["fill_rhs"].stop();
+    std::cout << "\tcpu = " << timers["fill_rhs"].read() << "s\n";
+}
+
+/// compute right-hand side 'f' (sources)
+
+void Solver::fill_lhs()
+{
+    timers["fill_lhs"].start();
+
+    std::cout << "assembly of RHS...\n";
+
+    // create RHS
+    f.resize(partition->parts.size());
+    for (size_t i = 0; i < partition->parts.size(); ++i)
+    {
+        f[i] = new Eigen::VectorXd(partSizes[i]);
+        f[i]->setZero();
+    }
+
+    // sources
+    // ...
+
+    // std::cout << "Neumann BCs...\n";
+
+    for (auto src : pbl.neumanns)
+    {
+        DOF bc_dof = dofs[src->m_dofname];
+        if (src->group)
+        {
+            for (auto entity : src->group->entities) // each source has a target group of entities
+                for (auto elems : entity->esets)     // each entity has a several sets of elements
+                {
+                    size_t nelems = elems->etags.size();
+                    // integration points
+                    std::vector<double> localCoord;
+                    std::vector<double> weights;
+                    gmsh::model::mesh::getIntegrationPoints(elems->type_id,
+                                                            pbl.quadrature,
+                                                            localCoord,
+                                                            weights);
+                    size_t nGP = weights.size();
+                    // jacobians
+                    std::vector<double> jacobians_v;
+                    std::vector<double> determinants_v;
+                    std::vector<double> coord_v;
+                    gmsh::model::mesh::getJacobians(elems->type_id, localCoord,
+                                                    jacobians_v, determinants_v,
+                                                    coord_v, entity->tag);
+                    // reshape (map) into matrices
+                    JacoMap jacobians(jacobians_v, nelems, nGP);
+                    Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>
+                        determinants(&determinants_v[0], nelems, nGP);
+                    CoordMap coord(coord_v, nelems, nGP);
+                    // basis functions
+                    int numComponents; // unused
+                    std::vector<double> basisF_v;
+                    int numOrientations; // unused
+                    gmsh::model::mesh::getBasisFunctions(elems->type_id,
+                                                         localCoord, "Lagrange",
+                                                         numComponents, // unused
+                                                         basisF_v,
+                                                         numOrientations); // unused
+                    Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>
+                        basisF(&basisF_v[0], nGP, elems->type_nnods);
+
+                    // loop over the elements
+                    for (size_t ie = 0; ie < elems->etags.size(); ++ie)
+                    {
+                        Eigen::VectorXd fe = Eigen::VectorXd::Zero(elems->type_nnods);
+                        // loop over Gauss points
+                        for (size_t igp = 0; igp < nGP; ++igp)
+                        {
+                            auto N_T = basisF.row(igp); // matrix of shape functions; shape=(1,elems.type_nnods)
+                            fe += src->m_val * N_T * determinants(ie, igp) * weights[igp];
+                        }
+
+                        // brutal assembly (needs optimisation)
+                        for (int i = 0; i < elems->type_nnods; ++i)
+                        {
+                            int i_tag = elems->ntags(ie, i);                   // gmsh tag of node i
+                            int i_gidx = pbl.nodemap[i_tag];                   // global index of node i
+                            auto i_part = dofPart.row(i_gidx)[bc_dof.index()]; // parts of dofs of node i
+                            auto i_idx = dofIdx.row(i_gidx)[bc_dof.index()];   // index of these dofs in their parts
+                            (*f[i_part])[i_idx] += fe[i];
+                        }
+                    }
+                }
+        } // if (src->group)
+        else
+        {
+            // neumann applied on a single node
+            int i_tag = src->m_nodetag;                        // gmsh tag of node i
+            int i_gidx = pbl.nodemap[i_tag];                   // global index of node i
+            auto i_part = dofPart.row(i_gidx)[bc_dof.index()]; // parts of dofs of node i
+            auto i_idx = dofIdx.row(i_gidx)[bc_dof.index()];   // index of these dofs in their parts
+            (*f[i_part])[i_idx] += src->m_val;
+        }
+    }
+
+    timers["fill_lhs"].stop();
+    std::cout << "\tcpu = " << timers["fill_lhs"].read() << "s\n";
+
+}
+
+// create the X vector (containing dofs)
+// and prescribe dirichlet boundary conditions
+void Solver::apply_bc()
+{
+    timers["apply_bc"].start();
+    std::cout << "applying dirichlet boundary conditions...\n";
+
+    // create vectors of dofs
+    X.resize(partition->parts.size());
+    for (size_t i = 0; i < partition->parts.size(); ++i)
+    {
+        X[i] = new Eigen::VectorXd(partSizes[i]);
+        X[i]->setZero();
+    }
+
+    // apply boundary conditions
+
+    for (auto &bc : pbl.dirichlets)
+    {
+        DOF bc_dof = dofs[bc->m_dofname];
+        for (auto &entity : bc->group->entities)
+            for (auto &elems : entity->esets)
+                for (size_t i = 0; i < elems->etags.size(); ++i)
+                    for (int j = 0; j < elems->type_nnods; ++j)
+                    {
+                        // size_t n = elems->ntags_v[i * elems->type_nnods + j];
+                        size_t n = elems->ntags(i, j);
+                        size_t gidx = pbl.nodemap[n];
+                        int part = dofPart(gidx, bc_dof.index());
+                        int idx = dofIdx(gidx, bc_dof.index());
+                        (*X[part])[idx] = bc->m_val;
+                    }
+    }
+
+    timers["apply_bc"].stop();
+    std::cout << "\tcpu = " << timers["apply_bc"].read() << "s\n";
+}
+
+void Solver::solve_static()
+{
+    timers["solve_static"].start();
+
+    std::cout << "solving linear system...\n";
+
+    // create vectors of tmp_rhs
+    std::vector<Eigen::VectorXd *> tmp_rhs(partition->parts.size());
+    for (size_t i = 0; i < partition->parts.size(); ++i)
+    {
+        tmp_rhs[i] = new Eigen::VectorXd(partSizes[i]);
+        tmp_rhs[i]->setZero();
+    }
+
+    // system: (F="free" ; P="prescribed")
+    //     [ K_FF K_FP ] [ x_F ] = [ f_F ]
+    //     [ K_PF K_PP ] [ x_P ]   [ f_P ]
+    // construct right-hand sides
+    //      [ K_FF ][ x_F ] + [ K_FP ][ x_P ] = [ f_F ]
+    //   => [ K_FF ][ x_F ] = [ f_F ] - [ K_FP ][ x_P ]
+    //                        <-------tmp_rhs_F------->
+    //   (same procedure for both thermal and mechanical problems)
+
+    size_t MF = part_Mfree->idx;
+    size_t MP = part_Mfixed->idx;
+
+    // mechanical
+    *tmp_rhs[MF] = *f[MF] - (*K[MF][MP]) * (*X[MP]);
+
+    // mechanical problem: [ x_F ] = [ K_FF ]^(-1) [ tmp_rhs_F ]
+    std::cout << "\t#unknowns = " << K[MF][MF]->rows() << '\n';
+    std::cout << "\t#threads = " << OpenMP::get_max_threads() << '\n';
+
+    if (linear_solver == "cg")
+    {
+        // --conjugate gradient
+        std::cout << "\tusing Eigen::ConjugateGradient (linear_solver=\""+linear_solver+"\")\n";
+        Eigen::ConjugateGradient<Eigen::SparseMatrix<double>, Eigen::Lower | Eigen::Upper> solver;
+        solver.compute(*K[MF][MF]);
+        if(solver.info()!=Eigen::Success) 
+            throw std::runtime_error("solver.compute() returned "+std::to_string(solver.info()));
+        *X[MF] = solver.solve(*tmp_rhs[MF]);
+        std::cout << "\t#iterations:     " << solver.iterations() << std::endl;
+        std::cout << "\testimated error: " << solver.error() << std::endl;
+        if(solver.info()!=Eigen::Success) 
+            throw std::runtime_error("solver.solve() returned "+std::to_string(solver.info()));
+    }
+    else if (linear_solver == "ldlt")
+    {
+        // --direct solver (LDLT)
+        std::cout << "\tusing Eigen::SimplicialLDLT (linear_solver=\""+linear_solver+"\")\n";
+        Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> solver;
+        solver.compute(*K[MF][MF]);
+        if(solver.info()!=Eigen::Success) 
+            throw std::runtime_error("solver.compute() returned "+std::to_string(solver.info()));
+        *X[MF] = solver.solve(*tmp_rhs[MF]); // use the factorization to solve for the given right-hand side
+        if(solver.info()!=Eigen::Success) 
+            throw std::runtime_error("solver.solve() returned "+std::to_string(solver.info()));
+    }
+#ifdef FEM_USE_MKL
+    else if (linear_solver == "pardiso")
+    {
+        // -- symmetric pardiso (LDLT)
+        std::cout << "\tusing Eigen::PardisoLDLT (linear_solver=\""+linear_solver+"\")\n";
+        Eigen::PardisoLDLT<Eigen::SparseMatrix<double>> solver;
+        solver.compute(*K[MF][MF]);
+        if(solver.info()!=Eigen::Success) 
+            throw std::runtime_error("solver.compute() returned "+std::to_string(solver.info()));
+        *X[MF] = solver.solve(*tmp_rhs[MF]); // use the factorization to solve for the given right-hand side
+        if(solver.info()!=Eigen::Success) 
+            throw std::runtime_error("solver.solve() returned "+std::to_string(solver.info()));
+    }
+#endif // FEM_USE_MKL
+    else
+    {
+        throw std::runtime_error("unknown linear_solver: "+linear_solver);
+    }
+
+    // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+    // std::cout << "X[MF] = \n"
+    //           << (*X[MF]).format(fmt) << '\n';
+
+    // compute reactions: [ f_P ] = [ K_PF ][ x_F ] + [ K_PP ][ x_P ]
+    //    values written previously in [ f_P ] during source / Neuman-BCs
+    //    treatment are overwritten
+
+    *f[MP] = (*K[MP][MF]) * (*X[MF]) + (*K[MP][MP]) * (*X[MP]);
+
+    // std::cout << "f[MP] = \n"
+    //           << (*f[MP]).format(fmt) << '\n';
+
+    // energy: work of ext forces (using their "final" value - thus twice the int energy)
+    double workF = (*f[MP]).dot(*X[MP]) +  (*f[MF]).dot(*X[MF]);
+    std::cout << "workF = " << workF << '\n';
+    int_energy = workF/2;
+
+    // free vectors of tmp_rhs
+    for (size_t i = 0; i < tmp_rhs.size(); ++i)
+        delete tmp_rhs[i];
+
+    timers["solve_static"].stop();
+    std::cout << "\tcpu = " << timers["solve_static"].read() << "s\n";
+}
+
+// strain and stresses (epsilon, sigma) per node, per element
+// (discontinuous across element boundaries)
+
+void Solver::compute_sig_epl(std::vector<size_t> &etags_all,                            // tags of elements
+                             std::vector<std::vector<double>> &sigma_all,               // values of stresses at the nodes of each element
+                             std::vector<std::vector<double>> &epsilon_all,             // values of strains at the nodes of each element
+                             std::map<size_t, std::vector<Eigen::VectorXd>> &sig_by_node, // value of all the stress vectors (Voigt) at a given node (key of the dict)
+                             std::map<size_t, std::vector<Eigen::VectorXd>> &eps_by_node) // same for strain vector
+{
+    timers["compute_sig_epl"].start();
+
+    std::cout << "computing strain and stresses...\n";
+
+    // pre-allocate things
+    std::map<size_t, size_t> nneighbours;
+    size_t nelems=0;
+    for (auto &medium : pbl.media)
+        for (auto &entity : medium->group->entities)
+        {
+            for (auto &elems : entity->esets)
+            {
+                nelems+=elems->etags.size(); // accumulate nb of elems
+
+                for (size_t ie = 0; ie < elems->etags.size(); ++ie)
+                    for (int inod = 0; inod < elems->ntags.cols(); ++inod)
+                    {
+                        size_t ntag = elems->ntags(ie, inod);
+                        nneighbours[ntag]++;
+                    }
+            }
+        }
+    std::cout << "\texpecting " << nelems << " elements\n";
+    etags_all.reserve(nelems);
+    epsilon_all.reserve(nelems);
+    sigma_all.reserve(nelems);
+
+    size_t maxneigh=0;
+    for(auto const &[ntag, nneigh] : nneighbours)
+    {
+        maxneigh = std::max(nneigh, maxneigh);
+        eps_by_node[ntag].reserve(nneigh);
+        sig_by_node[ntag].reserve(nneigh);
+    }
+    std::cout << "\tmax #neighbours = " << maxneigh << '\n';
+
+    // loop over the media
+
+    for (auto &medium : pbl.media)
+        for (auto &entity : medium->group->entities)
+            for (auto &elems : entity->esets)
+            {
+                // std::cout << "processing " << *elems << '\n';
+                size_t nelems = elems->etags.size();
+
+                // jacobians at nodes
+                std::vector<double> jacobians_v;
+                std::vector<double> determinants_v;
+                std::vector<double> coord_v;
+                gmsh::model::mesh::getJacobians(elems->type_id, elems->type_localNodeCoord,
+                                                jacobians_v, determinants_v,
+                                                coord_v, entity->tag);
+                // reshape (map) into matrices
+                JacoMap jacobians(jacobians_v, nelems, elems->type_nnods);
+                Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>>
+                    determinants(&determinants_v[0], nelems, elems->type_nnods);
+                CoordMap coord(coord_v, nelems, elems->type_nnods);
+
+                // basis functions (derivatives)
+                int numComponents;   // unused
+                int numOrientations; // unused
+                std::vector<double> basisDF_v;
+                gmsh::model::mesh::getBasisFunctions(elems->type_id,
+                                                     elems->type_localNodeCoord,
+                                                     "GradLagrange",
+                                                     numComponents, // unused
+                                                     basisDF_v,
+                                                     numOrientations); // unused
+                DShapeMap basisDF(basisDF_v, elems->type_nnods, elems->type_nnods);
+                // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                // std::cout << "basisDF =\n"
+                //           << basisDF(0).format(fmt) << '\n';
+                // std::cout << "basisDF_v =\n"
+                //           << basisDF_v << '\n';
+                // exit(1);
+
+                // loop over the elements
+                //#pragma omp parallel
+                for (size_t ie = 0; ie < elems->etags.size(); ++ie)
+                {
+                    // std::cout << "element " << ie+1 << "/" << elems->etags.size() << '\n';
+                    size_t etag = elems->etags[ie];
+                    const int ndim = elems->type_dim;
+
+                    // compute the nodal displacement vectors ("displ")
+                    //                   and the temperatures ("temp")
+                    //                   of the current element
+                    Eigen::VectorXd displ(elems->ntags.cols() * ndim);
+                    // loop over the nodes
+                    for (int inod = 0; inod < elems->ntags.cols(); ++inod)
+                    {
+                        size_t ntag = elems->ntags(ie, inod);
+                        size_t gidx = pbl.nodemap[ntag];
+                        for (int di = 0; di < ndim; ++di)
+                        {
+                            DOF dof = dofs[di];
+                            int part = dofPart(gidx, dof.index());
+                            int idx = dofIdx(gidx, dof.index());
+                            displ[ndim * inod + di] = (*X[part])[idx];
+                        }
+                    }
+
+                    std::vector<double> eps_nodes(9 * elems->ntags.cols(), 0.0); // strains at the nodes of the element in a 1d array
+                    std::vector<double> sig_nodes(9 * elems->ntags.cols(), 0.0); // stresses at the nodes of the element in a 1d array
+
+                    // loop over the nodes
+                    // and compute the stresses/strains/heat_fluxes at the nodes
+
+                    for (int inod = 0; inod < elems->ntags.cols(); ++inod)
+                    {
+                        size_t ntag = elems->ntags(ie, inod);
+                        auto DN = basisDF(inod).block(0, 0, elems->type_nnods, ndim); // matrix of shp fct derivatives in 'xi' space; shape=(elems.type_nnods,3)
+                        auto J = jacobians(ie, inod).block(0, 0, ndim, ndim);
+                        auto Jt = J.transpose();           // transposed jacobian matrix; shape=(3,3)
+                        auto Jtinv = Jt.inverse();         // transposed jacobian inverse; shape=(3,3)
+                        auto B_T = Jtinv * DN.transpose(); // matrix of shp fct derivatives in 'x' space; shape=(3, nnods_by_element)
+
+                        Eigen::MatrixXd B_M;
+                        switch (ndim)
+                        {
+                        case 2:
+                            B_M.resize(3, elems->type_nnods * ndim); // 3 en 2D; 6 en 3D
+                            for (int j = 0; j < elems->type_nnods; ++j)
+                            {
+                                double ddx = B_T(0, j);
+                                double ddy = B_T(1, j);
+                                Eigen::MatrixXd Bs(3, 2);
+                                Bs << ddx, 0.0,
+                                    0.0, ddy,
+                                    ddy, ddx;
+                                B_M.block(0, ndim * j, 3, 2) = Bs;
+                            }
+                            break;
+                        case 3:
+                            B_M.resize(6, elems->type_nnods * ndim); // 3 en 2D; 6 en 3D
+                            for (int j = 0; j < elems->type_nnods; ++j)
+                            {
+                                // double v = basisF(igp, j);
+                                double ddx = B_T(0, j);
+                                double ddy = B_T(1, j);
+                                double ddz = B_T(2, j);
+                                Eigen::MatrixXd Bs(6, 3);
+                                Bs << ddx, 0.0, 0.0,
+                                    0.0, ddy, 0.0,
+                                    0.0, 0.0, ddz,
+                                    ddy, ddx, 0.0,
+                                    ddz, 0.0, ddx,
+                                    0.0, ddz, ddy;
+                                // std::cout << "v = " << v << '\n';
+                                B_M.block(0, ndim * j, 6, 3) = Bs;
+                            }
+                            break;
+                        default:
+                            throw std::runtime_error("dimension " + std::to_string(ndim) + " not implemented");
+                        }
+
+                        auto eps = B_M * displ;               // nodal epsilon (Voigt)
+                        auto sig = medium->hooke(ndim) * eps; // nodal sigma (Voigt)
+                        // Eigen::IOFormat fmt(6, 0, ", ", "\n", "\t[", "]");
+                        // std::cout << "sig = \n"
+                        //           << sig.format(fmt) << '\n';
+
+                        // convert to 3D Voigt vectors
+                        Eigen::VectorXd eps3d(6), sig3d(6); // TODO: use fixed-size vectors
+                        if (ndim == 3)
+                        {
+                            eps3d = eps;
+                            sig3d = sig;
+                        }
+                        else
+                        {
+                            double eps_zz = -medium->m_nu / medium->m_E * (sig[0] + sig[1]);
+                            eps3d << eps(0), eps(1), eps_zz,
+                                eps(2), 0.0, 0.0;
+                            sig3d << sig(0), sig(1), 0.0,
+                                sig(2), 0.0, 0.0;
+                        }
+
+                        // [ [0]     [ [XX]     [ [0] [3] [4]    [ [XX] [XY] [XZ]
+                        //   [1]       [YY]       [3] [1] [5]      [XY] [YY] [YZ]
+                        //   [2]       [ZZ]       [4] [5] [2] ]    [XZ] [YZ] [ZZ] ]
+                        //   [3]       [XY]
+                        //   [4]       [XZ]
+                        //   [5] ]     [YZ] ]
+
+                        std::vector<int> voigt3d = {0, 3, 4, 3, 1, 5, 4, 5, 2};
+                        for (int i = 0; i < 9; ++i)
+                        {
+                            eps_nodes.at(inod * 9 + i) = eps3d(voigt3d[i]); // TODO: remove .at => []
+                            sig_nodes.at(inod * 9 + i) = sig3d(voigt3d[i]);
+                        }
+
+                        //#pragma omp critical
+                        {
+                            // store data also by node for smoothed fields
+                            eps_by_node[ntag].push_back(eps3d);
+                            sig_by_node[ntag].push_back(sig3d);
+                        }
+
+                    } // for (int inod = 0; ... (nodes of element ie)
+
+                    //#pragma omp critical
+                    {
+                        etags_all.push_back(etag);
+                        epsilon_all.push_back(eps_nodes);
+                        sigma_all.push_back(sig_nodes);
+                    }
+
+                } // for (size_t ie = 0; ... (elements)
+            } // for (auto &elems : entity->esets)
+
+    timers["compute_sig_epl"].stop();
+    std::cout << "\tcpu = " << timers["compute_sig_epl"].read() << "s\n";
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Solver const &obj)
+{
+    out << "Solver\n";
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femSolver.h b/cxxfem/src/femSolver.h
new file mode 100644
index 0000000000000000000000000000000000000000..52af8f36f295e5559899745d68e2afe3fa05fc64
--- /dev/null
+++ b/cxxfem/src/femSolver.h
@@ -0,0 +1,69 @@
+#ifndef FEMSOLVER_H
+#define FEMSOLVER_H
+
+#include "fem.h"
+#include "femDOF.h"
+#include "femTimer.h"
+#include <Eigen/Dense>
+#include <Eigen/Sparse>
+
+namespace fem
+{
+
+/// FEM solver
+
+class FEM_API Solver
+{
+public:
+#ifndef SWIG
+    Problem &pbl;         ///< problem data
+    Partition *partition; ///< partition of the dofs
+    Part *part_Mfree;
+    Part *part_Mfixed;
+
+    DOFs dofs;
+    DOFs bcs;
+
+    Eigen::ArrayXXi dofFixed; ///< is a dof fixed or free?
+    Eigen::ArrayXXi dofPart;  ///< in which part is a given dof of a given node?
+    Eigen::ArrayXXi dofIdx;
+    std::vector<size_t> partSizes;
+
+    std::vector<std::vector<Eigen::SparseMatrix<double> *>> K;
+    std::vector<Eigen::VectorXd *> f;
+    std::vector<Eigen::VectorXd *> X;
+
+    std::map<std::string, Timer> timers;
+
+#endif
+    std::string linear_solver;
+    double int_energy;
+
+public:
+    Solver(Problem &_pbl);
+    ~Solver();
+
+    void solve();
+
+#ifndef SWIG
+    void compute_sig_epl(std::vector<size_t> &etags_all,
+                         std::vector<std::vector<double>> &sigma_all,
+                         std::vector<std::vector<double>> &epsilon_all,
+                         std::map<size_t, std::vector<Eigen::VectorXd>> &sig_by_node,
+                         std::map<size_t, std::vector<Eigen::VectorXd>> &eps_by_node);
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Solver const &obj);
+#endif
+
+private:
+    void create_partition();
+    void fill_partition();
+    void fill_rhs();
+    void fill_lhs();
+    void apply_bc();
+    void solve_static();
+};
+
+} // namespace fem
+
+#endif // FEMSOLVER_H
diff --git a/cxxfem/src/femTimer.cpp b/cxxfem/src/femTimer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bcda2e44b4bf5b51835f999777b0277ec00e0d01
--- /dev/null
+++ b/cxxfem/src/femTimer.cpp
@@ -0,0 +1,55 @@
+#include "femTimer.h"
+
+namespace fem
+{
+
+Timer::Timer()
+{
+    reset();
+}
+
+Timer::~Timer()
+{
+    // std::cout << "~Timer()\n";
+}
+
+void Timer::start()
+{
+    running = true;
+    t0 = std::chrono::high_resolution_clock::now();
+}
+
+void Timer::stop()
+{
+    telapsed = read();
+    t0 = std::chrono::high_resolution_clock::time_point();
+    running = false;
+}
+
+void Timer::reset()
+{
+    t0 = std::chrono::high_resolution_clock::time_point();
+    telapsed = 0.0;
+    running = false;
+}
+
+double Timer::read() const
+{
+    if (running)
+    {
+        auto tnow = std::chrono::high_resolution_clock::now();
+        double dt = std::chrono::duration_cast<std::chrono::duration<double>>(tnow - t0).count();
+        return telapsed + dt;
+    }
+    else
+        return telapsed;
+}
+
+FEM_API std::ostream &
+operator<<(std::ostream &out, Timer const &obj)
+{
+    out << obj.read();
+    return out;
+}
+
+} // namespace fem
diff --git a/cxxfem/src/femTimer.h b/cxxfem/src/femTimer.h
new file mode 100644
index 0000000000000000000000000000000000000000..9c5cf29877adf1268e302735dd732b9583677a1b
--- /dev/null
+++ b/cxxfem/src/femTimer.h
@@ -0,0 +1,32 @@
+#ifndef FEMTIMER_H
+#define FEMTIMER_H
+
+#include "fem.h"
+#include <chrono>
+
+namespace fem
+{
+    
+/// a basic timer
+
+class FEM_API Timer
+{
+    std::chrono::high_resolution_clock::time_point t0;
+    double telapsed;
+    bool running;
+
+public:
+    Timer();
+    ~Timer();
+
+    void start();
+    void stop();
+    void reset();
+    double read() const;
+
+    friend FEM_API std::ostream &operator<<(std::ostream &out, Timer const &obj);
+};
+
+} // namespace fem
+
+#endif // FEMTIMER_H
diff --git a/cxxfem/tests/beam2d.py b/cxxfem/tests/beam2d.py
new file mode 100644
index 0000000000000000000000000000000000000000..891407ab21223ee7fbe2f70b43579b0637008b76
--- /dev/null
+++ b/cxxfem/tests/beam2d.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import cxxfem as fem
+import os
+import gmsh
+
+
+def build_square(cx = 0.0, cy = 0.0,
+                 Lx = 1.0, Ly = 1.0,
+                 nx = 2, ny = 2,
+                 structured = True, recombine = True):
+
+    gmsh.model.add("square")
+    lc = 1.0 # no impact
+    gmsh.model.geo.addPoint(cx, cy, 0, lc, 1)
+    gmsh.model.geo.addPoint(cx + Lx, cy, 0, lc, 2)
+    gmsh.model.geo.addPoint(cx + Lx, cy + Ly, 0, lc, 3)
+    gmsh.model.geo.addPoint(cx, cy + Ly, 0, lc, 4)
+
+    gmsh.model.geo.addLine(1, 2, 1)
+    gmsh.model.geo.addLine(2, 3, 2)
+    gmsh.model.geo.addLine(4, 3, 3)
+    gmsh.model.geo.addLine(1, 4, 4)
+
+    gmsh.model.geo.addCurveLoop([1, 2, -3, -4], 1)
+
+    gmsh.model.geo.addPlaneSurface([1], 1)
+
+    gmsh.model.geo.synchronize() # sinon warning "unknown entity..." lors du addPhysicalGroup
+
+    # mesh lines
+    gmsh.model.mesh.setTransfiniteCurve(1, nx + 1)
+    gmsh.model.mesh.setTransfiniteCurve(3, nx + 1)
+    gmsh.model.mesh.setTransfiniteCurve(2, ny + 1)
+    gmsh.model.mesh.setTransfiniteCurve(4, ny + 1)
+
+    # recombine
+    if structured:
+        gmsh.model.mesh.setTransfiniteSurface(1)
+    if recombine:
+        gmsh.model.mesh.setRecombine(2, 1) # quads
+
+    # physical groups - corners
+    pt1 = gmsh.model.addPhysicalGroup(0, [1])
+    pt2 = gmsh.model.addPhysicalGroup(0, [2])
+    pt3 = gmsh.model.addPhysicalGroup(0, [3])
+    pt4 = gmsh.model.addPhysicalGroup(0, [4])
+    gmsh.model.setPhysicalName(0, pt1, "pt1")
+    gmsh.model.setPhysicalName(0, pt2, "pt2")
+    gmsh.model.setPhysicalName(0, pt3, "pt3")
+    gmsh.model.setPhysicalName(0, pt4, "pt4")
+
+    # physical groups - lines
+    bottom = gmsh.model.addPhysicalGroup(1, [1], 1000)
+    right = gmsh.model.addPhysicalGroup(1, [2])
+    top = gmsh.model.addPhysicalGroup(1, [3])
+    left = gmsh.model.addPhysicalGroup(1, [4])
+    gmsh.model.setPhysicalName(1, bottom, "bottom")
+    gmsh.model.setPhysicalName(1, right, "right")
+    gmsh.model.setPhysicalName(1, top, "top")
+    gmsh.model.setPhysicalName(1, left, "left")
+
+    # physical groups - surfaces
+    interior = gmsh.model.addPhysicalGroup(2, [1])
+    gmsh.model.setPhysicalName(2, interior, "interior")
+
+    # finalize and mesh the geometry
+    gmsh.model.mesh.generate(2)
+
+    # gmsh.write("square.msh")
+
+
+if __name__ == "__main__":
+
+    pbl = fem.Problem()    #gmsh.initialize()
+    build_square(0.0, 0.0, 10.0, 1.0, 80, 8, True, True)
+    # build_square(0.0, 0.0, 10.0, 1.0, 2, 1, True, True)
+
+    # input()
+
+    m = fem.Medium(pbl, "interior", 100.0, 0.3)
+
+    d1 = fem.Dirichlet(pbl, "left", "X", 0.0)
+    d2 = fem.Dirichlet(pbl, "left", "Y", 0.0)
+    # d3 = fem.Dirichlet(pbl, "pt3", "Y", -0.1) # option 1
+    n3 = fem.Neumann(pbl, "top", "Y", -1e-3)    # option 2
+
+    d4 = fem.Dirichlet(pbl, "interior", "Z", 0.0)
+
+
+    solver = fem.Solver(pbl)
+    solver.solve()
+
+    post = fem.Post(solver)
+    post.build_views()
+    post.show_views( [ "stress_tensor", "force_vector" ] )
+    post.deform(5)  
+    post.probe("force_vector", 1)  
+    post.view()
+
+    
\ No newline at end of file
diff --git a/cxxfem/tests/beam3d.py b/cxxfem/tests/beam3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6de2a7baf4854357806dfd9ec1804d0e1d1e159
--- /dev/null
+++ b/cxxfem/tests/beam3d.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import cxxfem as fem
+import os
+
+fem.debug_level(0)
+
+if __name__ == "__main__":
+
+    # fine mesh
+    # pars = {
+    #     "Lx": 5.0, # width
+    #     "Ly": 1.0, # depth
+    #     "Lz": 1.0, # height/thickness
+    #     "nx": 60,         
+    #     "ny": 30,
+    #     "nz": 30
+    # }
+
+    # same as python
+    pars = {
+        "Lx": 5.0, # width
+        "Ly": 1.0, # depth
+        "Lz": 1.0, # height/thickness
+        "nx": 15,         
+        "ny": 2,
+        "nz": 10
+    }
+
+    # mono-element for debug
+    # pars = {
+    #     "Lx": 5.0, # width
+    #     "Ly": 1.0, # depth
+    #     "Lz": 1.0, # height/thickness
+    #     "nx": 20,         
+    #     "ny": 1,
+    #     "nz": 1
+    # }
+
+    pbl = fem.Problem()
+    f = os.path.join(os.path.dirname(__file__), 'parallelepiped.geo')
+    pbl.load(f, fem.StringDoubleMap(pars))
+
+    m = fem.Medium(pbl, "Volume", E=100.0, nu=0.3)
+
+    # - mechanical BCs
+
+    # clamping
+    bcs = []
+    for d in ['X','Y','Z']:
+        bcs.append(fem.Dirichlet(pbl, "Left", dof=d, val=0.0))
+    
+    # Vertical displacement
+    # bcs.append(fem.Dirichlet(pbl, "Right", dof='Z', val=-1.0)) # prescribed displ
+    
+    # bcs.append(fem.Neumann(pbl, "Corner", 'Z', -0.1)) # concentrated force (seems OK)
+    bcs.append(fem.Neumann(pbl, "Top", 'Z', -1e-2)) # distributed load
+    bcs.append(fem.Neumann(pbl, "Top", 'Y', -5e-2)) # distributed load
+
+    solver = fem.Solver(pbl)
+    solver.linear_solver = "pardiso"
+    solver.solve()
+
+    post = fem.Post(solver)
+    # post.build_views(ther=False, smooth=False)
+    post.build_views()
+    post.show_views(['force_vector','stress_tensor'])
+    post.set_camera_3D()
+    # post.write()    
+    post.deform()
+    post.view()
+
diff --git a/cxxfem/tests/parallelepiped.geo b/cxxfem/tests/parallelepiped.geo
new file mode 100644
index 0000000000000000000000000000000000000000..e60b38c6da2a4bca6a32d403b5231bee082b483c
--- /dev/null
+++ b/cxxfem/tests/parallelepiped.geo
@@ -0,0 +1,46 @@
+// Parameterized parallelepiped meshed with hexahedra
+
+DefineConstant[ Lx = { 2.0, Min 1, Max 20, Step 1, Name "Lx" }  ];// width
+DefineConstant[ Ly = { 2.0, Min 1, Max 20, Step 1, Name "Ly" }  ];// depth
+DefineConstant[ Lz = { 2.0, Min 1, Max 20, Step 1, Name "Lz" }  ];// height
+DefineConstant[ nx = { 2, Min 1, Max 20, Step 1, Name "nx" }  ];// nb of elements along x
+DefineConstant[ ny = { 2, Min 1, Max 20, Step 1, Name "ny" }  ];// nb of elements along y
+DefineConstant[ nz = { 2, Min 1, Max 20, Step 1, Name "nz" }  ];// nb of elements along z (thickness)
+DefineConstant[
+  recombine = {1, Choices{0="no",1="yes"},
+    Name "recombine"}
+];
+
+d=0.05; // not used
+Point(1) = {0, 0, 0, d};
+Point(2) = {0, Ly, 0, d};
+Point(3) = {Lx, Ly, 0, d};
+Point(4) = {Lx, 0, 0, d};
+Line(1) = {1, 4};
+Line(2) = {4, 3};
+Line(3) = {2, 3};
+Line(4) = {1, 2};
+Line Loop(5) = {4, 3, -2, -1};
+Plane Surface(6) = {5};
+Transfinite Line {4, 2} = ny+1 Using Progression 1;
+Transfinite Line {1, 3} = nx+1 Using Progression 1;
+
+If(recombine == 0)
+  Extrude {0, 0, Lz} { Surface{6}; Layers{nz}; }
+Else
+  Transfinite Surface {6} = {1, 4, 3, 2};
+  Recombine Surface {6};
+  Extrude {0, 0, Lz} { Surface{6}; Layers{nz}; Recombine; }
+EndIf
+
+Physical Volume("Volume") = {1};
+
+Physical Surface("Bottom") = {6}; // Z = 0
+Physical Surface("Top") = {28};   // Z = Lz
+Physical Surface("Front") = {27}; // Y = 0
+Physical Surface("Back") = {19};  // Y = Ly
+Physical Surface("Left") = {15};  // X = 0
+Physical Surface("Right") = {23}; // X = Lx
+
+//+
+Physical Point("Corner", 29) = {4};
diff --git a/envs/cmake.cmd b/envs/cmake.cmd
new file mode 100644
index 0000000000000000000000000000000000000000..ff21a68200b13d9259b7cb708cbf0cc77b180db7
--- /dev/null
+++ b/envs/cmake.cmd
@@ -0,0 +1,3 @@
+@echo off
+::echo cmake.exe -G "MinGW Makefiles" %*
+cmake.exe -G "MinGW Makefiles" %*
\ No newline at end of file
diff --git a/envs/linux-macos.sh b/envs/linux-macos.sh
new file mode 100644
index 0000000000000000000000000000000000000000..9970673d782322100e11d4a020740a14a792be0c
--- /dev/null
+++ b/envs/linux-macos.sh
@@ -0,0 +1,32 @@
+# This file setup an environment which allows you to compile the code
+# on Linux or macOS using the default compiler (gcc or clang)
+#
+# HOW TO USE THIS FILE?
+#   open a terminal
+#   source ./envs/linux-macos.sh
+#   mkdir build
+#   cd build
+#   cmake ..
+#   make
+#   [executables are built in the bin/ folder]
+#   ctest
+
+# try to get the absolute path of root folder
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd && echo x)"; DIR="${DIR%x}"
+abspath=$(dirname $DIR )
+#echo "abspath=${abspath}"
+
+# set the location of gmsh SDK ( **MODIFY THIS LINE FOR YOUR SYSTEM** )
+GMSHSDK=${abspath}/lib/gmsh-sdk
+EIGEN=${abspath}/lib/eigen
+
+# where are gmsh and gmsh-**.so ?
+export PATH=${GMSHSDK}/bin:${GMSHSDK}/lib:${PATH}
+# where is gmsh.h ?
+export INCLUDE=${EIGEN}:${GMSHSDK}/include:${INCLUDE}
+# where is gmsh.lib ?
+export LIB=${GMSHSDK}/lib:${LIB}
+# where is gmsh.py ? (required only if you want to use the python API)
+export PYTHONPATH=${GMSHSDK}/lib:${PYTHONPATH}
+# the following command is only useful for macOS 
+export DYLD_LIBRARY_PATH=${GMSHSDK}/lib:${DYLD_LIBRARY_PATH}
diff --git a/envs/make.cmd b/envs/make.cmd
new file mode 100644
index 0000000000000000000000000000000000000000..cd6247a535b99591f68b00b81cc7d587239cb7ab
--- /dev/null
+++ b/envs/make.cmd
@@ -0,0 +1,2 @@
+@echo off
+mingw32-make.exe %*
\ No newline at end of file
diff --git a/envs/win-mingw64.cmd b/envs/win-mingw64.cmd
new file mode 100644
index 0000000000000000000000000000000000000000..b18a4a9309f17f9280ccefc73ab57bee485a3849
--- /dev/null
+++ b/envs/win-mingw64.cmd
@@ -0,0 +1,92 @@
+@echo off
+:: This file opens a terminal which allows you to compile the code with
+:: a 64-bit mingw compiler
+::     https://mingw-w64.org/
+::     https://www.msys2.org/
+::
+:: HOW TO USE THIS FILE?
+::   [check the PATHs below]
+::   [run this file]
+::   mkdir build
+::   cd build
+::   cmake ..
+::   make
+::   ctest
+::
+:: How to clean the "build" folder using cmd line?
+::   cd build
+::   rd /q /s .
+
+echo setting MinGW64 environment...
+
+:: set the location of gmsh SDK / mingw compiler
+set GMSHSDK=%~dp0..\lib\gmsh-sdk
+set EIGEN=%~dp0..\lib\eigen
+set COMPILERPATHS=C:\msys64\mingw64\bin;C:\mingw-w64\mingw64\bin
+
+:: perform some tests...
+IF NOT EXIST %GMSHSDK% (
+    ECHO   - gmsh-sdk NOT found in %GMSHSDK%!!
+    PAUSE
+    EXIT /B
+) ELSE (
+    ECHO   - gmsh-sdk found in %GMSHSDK%.
+)
+
+IF NOT EXIST %EIGEN% (
+    ECHO   - eigen NOT found in %EIGEN%!!
+    PAUSE
+    EXIT /B
+) ELSE (
+    ECHO   - eigen found in %EIGEN%.
+)
+
+FOR %%a IN (%COMPILERPATHS%) DO (
+    IF EXIST %%a (
+        ECHO   - compiler found in %%a.
+        SET COMPILERPATH=%%a
+    ) ELSE (
+        IF NOT DEFINED COMPILERPATH (
+            ECHO   - compiler not found in %%a.
+        )
+    )
+)
+IF NOT DEFINED COMPILERPATH (
+    ECHO   =^> compiler NOT found!
+    PAUSE
+    EXIT /B 
+)
+
+:: Intel MKL/TBB
+set INTELPATH="C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows"
+IF NOT EXIST %INTELPATH% (
+    ECHO   - INTEL libraries NOT found in %INTELPATH%!!
+) ELSE (
+    ECHO   - INTEL libraries found.
+    call %INTELPATH%\mkl\bin\mklvars.bat intel64 vs2019
+    call %INTELPATH%\tbb\bin\tbbvars.bat intel64 vs2019
+)
+
+:: where is gmsh.exe and gmsh-**.dll ?
+set PATH=%GMSHSDK%\bin;%GMSHSDK%\lib;%PATH%
+:: where is gmsh.h ?
+set INCLUDE=%EIGEN%;%GMSHSDK%\include;%INCLUDE%
+:: where is gmsh.lib ?
+set LIB=%GMSHSDK%\lib;%LIB%
+:: where is gmsh.py ? (required only if you want to use the python API)
+set PYTHONPATH=%GMSHSDK%\lib;%PYTHONPATH%
+:: add path to the compiler to the PATH
+set PATH=%COMPILERPATH%;%PATH%
+:: add current folder to PATH for cmake/make aliases
+set PATH=%~dp0;%PATH%
+
+:: clear local vars
+set GMSHSDK=
+set EIGEN=
+set COMPILERPATHS=
+set COMPILERPATH=
+
+:: open terminal
+CD /d "%~dp0"
+CD ..
+%comspec% /K
diff --git a/envs/win-msvc.cmd b/envs/win-msvc.cmd
new file mode 100644
index 0000000000000000000000000000000000000000..1aecc53ab4e981839516141e81cb53e434da0026
--- /dev/null
+++ b/envs/win-msvc.cmd
@@ -0,0 +1,68 @@
+@echo off
+:: This file opens a terminal which allows you to compile the code with
+:: Microsoft Visual Studio 2017 (x64) on Windows
+::
+:: HOW TO USE THIS FILE?
+::   [run this file]
+::   mkdir build
+::   cd build
+::   cmake -A x64 ..
+::   cmake --build . --config Release
+::   [executables are built in the bin/Release folder]
+::   ctest -C Release
+
+:: set the location of gmsh SDK / mingw compiler
+set "GMSHSDK=%~dp0..\lib\gmsh-sdk"
+set "EIGEN=%~dp0..\lib\eigen"
+set COMPILERPATH="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build"
+
+:: perform some tests...
+IF NOT EXIST "%GMSHSDK%" (
+    ECHO   - gmsh-sdk NOT found in %GMSHSDK%!!
+    PAUSE
+    EXIT /B
+) ELSE (
+    ECHO   - gmsh-sdk found.
+)
+
+IF NOT EXIST "%EIGEN%" (
+    ECHO   - eigen NOT found in %EIGEN%!!
+    PAUSE
+    EXIT /B
+) ELSE (
+    ECHO   - eigen found.
+)
+
+IF NOT EXIST %COMPILERPATH% (
+    ECHO   - compiler NOT found in %COMPILERPATH%!!
+    PAUSE
+    EXIT /B
+) ELSE (
+    ECHO   - compiler found.
+)
+
+:: Intel MKL/TBB
+set INTELPATH="C:\Program Files (x86)\IntelSWTools\compilers_and_libraries\windows"
+IF NOT EXIST %INTELPATH% (
+    ECHO   - INTEL libraries NOT found in %INTELPATH%!!
+) ELSE (
+    ECHO   - INTEL libraries found.
+    call %INTELPATH%\mkl\bin\mklvars.bat intel64 vs2019
+    @REM call %INTELPATH%\tbb\bin\tbbvars.bat intel64 vs2019
+)
+
+:: where is gmsh.exe and gmsh-**.dll ?
+set PATH=%GMSHSDK%\bin;%GMSHSDK%\lib;%PATH%
+:: where is gmsh.h ?
+set INCLUDE=%EIGEN%;%GMSHSDK%\include;%INCLUDE%
+:: where is gmsh.lib ?
+set LIB=%GMSHSDK%\lib;%LIB%
+:: where is gmsh.py ? (required only if you want to use the python API)
+set PYTHONPATH=%GMSHSDK%\lib;%PYTHONPATH%
+
+:: set the environment of the msvc compiler
+CD /d "%~dp0"
+CD ..
+:: problem with mkl_rt (mkl_intel_thread cannot be loaded)
+:: %comspec% /k "%COMPILERPATH%\vcvars64.bat"
+%comspec% /k 
diff --git a/lib/.gitignore b/lib/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..67a64465eb935decc500dbb91de7b25924e2fbca
--- /dev/null
+++ b/lib/.gitignore
@@ -0,0 +1,2 @@
+eigen/
+gmsh-sdk/
\ No newline at end of file
diff --git a/lib/README.md b/lib/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ed65683f8679789ff6fd7723c78a9217eb4bdf03
--- /dev/null
+++ b/lib/README.md
@@ -0,0 +1,3 @@
+# ./lib
+
+This folder contains scripts for downloading the 2 external libraries that are required for this project.
\ No newline at end of file
diff --git a/lib/get_eigen.cmd b/lib/get_eigen.cmd
new file mode 100644
index 0000000000000000000000000000000000000000..cf848c53f6846dd7f9f005c456875ef7568d1ef0
--- /dev/null
+++ b/lib/get_eigen.cmd
@@ -0,0 +1,18 @@
+::@echo off
+:: download eigen & extract to current folder
+
+set version=3.3.9
+set file=eigen-%version%.zip
+
+:: download file
+del %file%  >nul 2>&1
+bitsadmin /transfer get_eigen /dynamic /download /priority foreground "https://gitlab.com/libeigen/eigen/-/archive/%version%/%file%" "%CD%\%file%"
+
+:: unzip
+rd /Q/S eigen-%version%  >nul 2>&1
+powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::ExtractToDirectory('%CD%\%file%', '%CD%'); }"
+del %file%  >nul 2>&1
+
+:: rename folder
+rd /Q/S eigen >nul 2>&1
+ren eigen-%version% eigen
diff --git a/lib/get_eigen.sh b/lib/get_eigen.sh
new file mode 100644
index 0000000000000000000000000000000000000000..aea96329790dd7df0f5628c80ec231f3ea72b071
--- /dev/null
+++ b/lib/get_eigen.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+set -e
+
+VERSION=3.3.9
+
+wget -q https://gitlab.com/libeigen/eigen/-/archive/${VERSION}/eigen-${VERSION}.tar.gz
+tar xzf eigen-${VERSION}.tar.gz
+rm eigen-${VERSION}.tar.gz
+rm -rf eigen
+mv eigen-${VERSION} eigen
diff --git a/lib/get_gmsh.cmd b/lib/get_gmsh.cmd
new file mode 100644
index 0000000000000000000000000000000000000000..d9a71d6be81d90b6311bfdec9a43b89896f25931
--- /dev/null
+++ b/lib/get_gmsh.cmd
@@ -0,0 +1,28 @@
+::@echo off
+:: download gmsh-sdk & extract to current folder
+
+set version=4.10.2
+
+set gmsh=gmsh-%version%-Windows64-sdk
+set file=%gmsh%.zip
+
+:: download file
+del %file%  >nul 2>&1
+bitsadmin /transfer get_gmsh /dynamic /download /priority foreground "https://gmsh.info/bin/Windows/%file%" "%CD%\%file%"
+
+:: unzip
+rd /Q/S %gmsh%  >nul 2>&1
+powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::ExtractToDirectory('%CD%\%file%', '%CD%'); }"
+del %file%  >nul 2>&1
+
+:: rename folder
+rd /Q/S gmsh-sdk >nul 2>&1
+ren %gmsh% gmsh-sdk
+
+:: copy dll to bin folder so that double-clic on gmsh.exe works!
+move /Y gmsh-sdk\lib\gmsh-*.dll gmsh-sdk\bin\
+:: IMPORTANT NOTE: do not leave gmsh-*.dll next to *.py because gmsh.py 
+:: will try to load this one first.
+:: However any C++ program will prefer gmsh-*.dll in the bin folder
+:: => any combined python/C++ program will load both copies in memory
+:: => C++ calls and python call will not operate on the same memory. 
\ No newline at end of file
diff --git a/lib/get_gmsh.sh b/lib/get_gmsh.sh
new file mode 100644
index 0000000000000000000000000000000000000000..4b8cb825426bbfbfe05520be1e48f8f1f058ab0c
--- /dev/null
+++ b/lib/get_gmsh.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+set -e
+
+# VERSION=4.9.3
+VERSION=4.10.2
+
+if [[ "$OSTYPE" == "linux-gnu" ]]; then
+    wget -q https://gmsh.info/bin/Linux/gmsh-${VERSION}-Linux64-sdk.tgz
+    tar xzf gmsh-${VERSION}-Linux64-sdk.tgz
+    rm gmsh-${VERSION}-Linux64-sdk.tgz
+    rm -rf gmsh-sdk
+    mv gmsh-${VERSION}-Linux64-sdk gmsh-sdk
+elif [[ "$OSTYPE" == "darwin"* ]]; then
+    wget -q https://gmsh.info/bin/MacOSX/gmsh-${VERSION}-MacOSX-sdk.tgz
+    tar xzf gmsh-${VERSION}-MacOSX-sdk.tgz
+    rm gmsh-${VERSION}-MacOSX-sdk.tgz
+    rm -rf gmsh-sdk
+    mv gmsh-${VERSION}-MacOSX-sdk gmsh-sdk
+else
+    echo "unknown system"
+fi
diff --git a/models/bonemodel2.py b/models/bonemodel2.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f8e08fd56c0a8af11b4c43c84e3fdcf8998ac3c
--- /dev/null
+++ b/models/bonemodel2.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+# Bone FEA model (formely: "mandible model")
+#
+# main cxxfem script with default parameters.
+# R.Boman
+
+import cxxfem as fem
+from . import boneload
+import os
+import gmsh
+import numpy as np
+
+
+def parms(d={}):
+    """default parameters of the model
+    """
+    p = {}
+
+    # Geomagic/MeshLab input files
+    path = 'dolicorhynchops/10k'
+    # mandible mesh (.ply/.geo/.msh)
+    p['bone'] = f'{path}/mandible.ply'
+    # one or several vertices (.ply/.off) (used if p['food']=='fixteeth')
+    p['contact_pts'] = f'{path}/teeth.off'
+    # left temporomandibular joint - 1 vertex (.ply/.off/coordinates)
+    p['axis_pt1'] = [0.1458893, -73.13895, 227.3909]
+    # right temporomandibular joint - 1 vertex (.ply/.off/coordinates)
+    p['axis_pt2'] = f'{path}/RTMJ.off'
+
+    p['muscles'] = [
+        {
+            'file': f'{path}/Lmuscle.ply',
+            'force': 100.,
+            'focalpt': f'{path}/LmuscleF.off',
+            'method': 'T'                       # loading model: 'U', 'T', 'T+N'
+        },
+        {
+            'file': f'{path}/Rmuscle.off',
+            'force': 100.,
+            'focalpt': f'{path}/RmuscleF.off',
+            'method': 'T'
+        }
+    ]
+    p['food'] = 'fixteeth'  # type of food: 'fixteeth' or 'cylinder'
+    p['fixations'] = {
+        'contact_pts': ['x', 'y', 'z'],  # constrained degrees of freedom
+        'axis_pt1': ['x', 'y', 'z'],
+        'axis_pt2': ['x', 'y', 'z']
+    }
+    # rigid cylinder (used if p['food']=='cylinder')
+    R = 20
+    p['cylinder'] = {
+        'origin': [-R-8, 0, -150],
+        'radius': R,
+        'width': 100,
+        'friction': 0.1,
+        'penalty': 1e3
+    }
+
+    # material properties
+    p['density'] = 1.850e-9  # [T/mm³] - bone: 1.850 kg/l
+    p['Young'] = 17000.      # [MPa] elastic modulus - bone: 17-20 GPa
+    p['Poisson'] = 0.3       # [-] Poisson's ratio
+
+    # numerical parameters
+    p['tolNR'] = 1e-6        # [-] equilibrium tolerance
+    p['dt0'] = 1.0           # [s] time step size
+
+    # gmsh toolbox
+    p['use_gmshOld'] = False  # use old gmsh interface
+
+    p.update(d)
+    return p
+
+
+def solve(p={}):
+
+    p = parms(p)
+
+    import json
+    # print(f'parameters={json.dumps(p, indent=4, sort_keys=True)}')
+
+    pbl = fem.Problem()
+
+    gmsh.initialize()
+    gmsh.option.setNumber("General.Terminal", 1)
+    # load main mandible mesh (volume or surface - it will be meshed in 3D)
+    import_mesh(p['bone'])
+
+    # define groups
+
+    # group (3,1)='all' is the group containing all volume elements
+    groups = {}
+    groups['all'] = (3, 1)
+    nodeTags, coord = gmsh.model.mesh.getNodesForPhysicalGroup(*groups['all'])
+    print(f"the mandible mesh has {len(nodeTags)} nodes")
+
+    # extract external surface of the mesh => groups['surf']
+    # #   nods_no: tags of the nodes
+    # #   nods_pos: coordinates of these nodes
+    groups['surf'] = (2, 1)
+    nods_no, nods_pos = gmsh.model.mesh.getNodesForPhysicalGroup(
+        *groups['surf'])
+    print(f"the mandible surface has {len(nods_no)} nodes")
+    nods_pos = np.reshape(nods_pos, (len(nods_no), 3))
+    # print(f'nods_pos={nods_pos}')
+
+    # load other surface meshes  => groups['axis_pt1'], ...
+    for meshname in ['axis_pt1', 'axis_pt2']:
+        create_group(p[meshname], nods_no, nods_pos, groups, meshname)
+
+    # load muscle groups
+    mgroups = {}                    # stores muscle group data and loads
+    for muscle in p['muscles']:
+        # load focal point
+        if isinstance(muscle['focalpt'], str):
+            fullpath = os.path.join(
+                os.path.dirname(__file__), muscle['focalpt'])
+            focalnodes, _ = boneload.load_msh(fullpath)
+        else:  # coordinates in array or tuple
+            focalnodes = [muscle['focalpt']]
+
+        # load surface mesh => groups[name (from filename)]
+        name, nodes, tris, ntags = \
+            create_group(muscle['file'], nods_no, nods_pos, groups)
+        # create loads on the surface
+        loads = boneload.create_loads(nodes, tris,
+                                      total_force=muscle['force'], target=focalnodes[0], method=muscle['method'])
+        # store data in a structure
+        mgroups[name] = MuscleGroup(nodes, tris, ntags, loads)
+
+    if p['food'] == 'fixteeth':
+        # teeth are fixed along chosen directions
+        create_group(p['contact_pts'], nods_no,
+                     nods_pos, groups, 'contact_pts')
+
+
+    # --------------------------------------------------------------------------
+    print('== ADDING MEDIUM...')
+
+    do_not_delete = []
+
+    # material
+    do_not_delete.append( fem.Medium(pbl, "all", E=p['Young'], nu=p['Poisson']) )
+
+    print('== ADDING DIRICHLETs...')
+    # axis of rotation
+    for gname in ['axis_pt1', 'axis_pt2']:
+        for d in p['fixations'][gname]:
+            do_not_delete.append(fem.Dirichlet(pbl, gname, dof=d.upper(), val=0.0) )
+
+    print('== ADDING NEUMANNs...')
+    # mshpts = geometry.getMesh().getPointSet()
+    for name, gr in mgroups.items():
+        # print(f'len(gr.ntags)={len(gr.ntags)}')
+        for i,load in enumerate(gr.loads):
+            do_not_delete.append(fem.Neumann(pbl, int(gr.ntags[i]), 'X', load.x[0]) )  # TODO: regler pbl de cast "gr.ntags[i] => size_t"
+            do_not_delete.append(fem.Neumann(pbl, int(gr.ntags[i]), 'Y', load.x[1]) )  # TODO: regler "dof=" qui marche pas
+            do_not_delete.append(fem.Neumann(pbl, int(gr.ntags[i]), 'Z', load.x[2]) )
+
+    # type of food
+
+    if p['food'] == 'fixteeth':
+        # teeth are fixed along chosen directions
+        for d in p['fixations']['contact_pts']:
+            do_not_delete.append(fem.Dirichlet(pbl, 'contact_pts', dof=d.upper(), val=0.0) )
+    else:
+        raise Exception('food = "fixteeth" only supported')
+
+
+    # python only!
+    # do_not_delete.append(fem.Dirichlet(pbl, "all", dof='T', val=0.0) )
+
+    
+    # Time integration scheme    
+    # print (pbl)
+    print('SOLVE.......')
+
+    solver = fem.Solver(pbl)
+    solver.linear_solver = "pardiso"
+    solver.solve()
+
+    post = fem.Post(solver)
+    post.build_views()
+    post.show_views([ 'smooth_stress_tensor' ])
+    post.set_camera_3D()
+    post.write()
+    post.deform()
+
+    print('extracting results...')
+    print(f'\tinternal energy = {solver.int_energy:.3f} N.mm')
+    for grpname in ['axis_pt1', 'axis_pt2', 'contact_pts']:
+        v = np.array(post.probe('force_vector', grpname))
+        v = np.reshape(v, (v.size//3, 3))
+        v = np.sum(v, axis=0)
+        print(f'\t{grpname}_Fx = {v[0]:.3f} N')
+        print(f'\t{grpname}_Fy = {v[1]:.3f} N')
+        print(f'\t{grpname}_Fz = {v[2]:.3f} N')
+
+    for name, group in mgroups.items():
+        v=np.array( [0.0, 0.0, 0.0] )
+        for tag in group.ntags:
+            v += np.array(post.probe('force_vector', int(tag)))
+        print(f'\t{name}_Fx = {v[0]:.3f} N')
+        print(f'\t{name}_Fy = {v[1]:.3f} N')
+        print(f'\t{name}_Fz = {v[2]:.3f} N')
+
+    post.view()
+
+
+# ------------------------------------------------------------------------------
+# utility functions involing Gmsh objects
+
+
+def import_mesh(filename):
+    """import the volume mesh
+    input file type can be:
+    - .msh: the mesh is simply loaded
+    - .geo: the mesh is generated
+    - .ply/.stl: a .geo is written in the workspace and loaded into gmsh
+    """
+    # import gmsh file (accept .geo, .msh, .ply or .stl)
+    mandible_gmsh = filename
+    # make the path absolute
+    if not os.path.isabs(mandible_gmsh):
+        mandible_gmsh = os.path.join(os.path.dirname(__file__), mandible_gmsh)
+    # check whether file exists
+    if not os.path.isfile(mandible_gmsh):
+        raise Exception(f'{mandible_gmsh} not found!')
+    # extract extension
+    noext, ext = os.path.splitext(mandible_gmsh)
+    if ext in ['.ply', '.stl']:
+        plyfile = mandible_gmsh
+        print(f'loading file {plyfile} into Gmsh...')
+        # gmsh.model.add("fossil")
+        gmsh.merge(plyfile)
+        gmsh.model.geo.addSurfaceLoop(surfaceTags=[1], tag=1)
+        gmsh.model.geo.addVolume(shellTags=[1], tag=1)
+        gmsh.model.geo.addPhysicalGroup(dim=2, tags=[1], tag=1)
+        gmsh.model.setPhysicalName(dim=2, tag=1, name='surf')
+        gmsh.model.geo.addPhysicalGroup(dim=3, tags=[1], tag=1)
+        gmsh.model.setPhysicalName(dim=3, tag=1, name='all')
+    elif ext in ['.geo', '.msh']:
+        gmsh.merge(mandible_gmsh)
+    else:
+        raise Exception(
+            f'Unknown extension: {ext}, please use .ply, .stl, .msh or .geo')
+
+    # generate mesh
+    gmsh.model.geo.synchronize()
+    gmsh.model.mesh.generate(3)
+    # gmsh.fltk.run()
+
+
+def create_group(mesh_file_or_coord, all_no, all_coords, groups, gname=None):
+    """loads the mesh file "mesh_file_or_coord" and identify vertices among vertices
+    stored in all_coords. 
+    Then, the routine creates a group named "gname" (built from mesh_file_or_coord if None)
+
+    mesh_file_or_coord can be either:
+      a filename
+      an array of points: [ [x1,y1,z1], [x2,y2,z2], ... ]
+      one single point: [x1,y1,z1]
+    """
+    if isinstance(mesh_file_or_coord, str):
+        # load mesh file
+        print(f'\n* create_group {mesh_file_or_coord}...')
+        fullpath = os.path.join(os.path.dirname(__file__), mesh_file_or_coord)
+        nodes, tris = boneload.load_msh(fullpath)
+    elif type(mesh_file_or_coord) in [list, tuple]:
+        print(f'\n* create_group {gname}...')
+        if type(mesh_file_or_coord[0]) in [list, tuple]:
+            # array of points
+            nodes = mesh_file_or_coord
+        else:
+            # one single point
+            nodes = [mesh_file_or_coord]
+        print(f'nodes={nodes}')
+        print(f'mesh_file_or_coord={mesh_file_or_coord}')
+        tris = []
+    else:
+        raise Exception(f'Data is not understood: {mesh_file_or_coord}')
+    # identify nodes of the mesh among the given vertices of the main mesh
+    # ntags = boneload.identify_nodes_brutal(nodes, all_no, all_coords)
+    ntags = boneload.identify_nodes_SaP(nodes, all_no, all_coords)
+    # print(f'ntags={ntags}')
+
+    if gname is None:
+        gname, _ = os.path.splitext(os.path.basename(mesh_file_or_coord))
+
+    # create a Gmsh group
+    #   => a discrete entity with "points" elements
+    print(f'** adding gmsh group "{gname}"...')
+    etag = gmsh.model.addDiscreteEntity(dim=0)
+    point_type = gmsh.model.mesh.getElementType("Point", 1)
+    # print(f'point_type={point_type}')
+    gmsh.model.mesh.addElementsByType(etag, point_type, [], ntags)
+    ptag = gmsh.model.geo.addPhysicalGroup(dim=0, tags=[etag])
+    gmsh.model.setPhysicalName(dim=0, tag=ptag, name=gname)
+
+    groups[gname] = (0, ptag)
+
+    # required for the nodes to be linked to the physical group
+    gmsh.model.geo.synchronize()
+    nods, pos = gmsh.model.mesh.getNodesForPhysicalGroup(*groups[gname])
+    # print(f'nods={nods}')
+    # print(f'pos={pos}')
+
+    print(f'group {groups[gname]} ({gname}) '
+          f'created from {mesh_file_or_coord} ({len(nods)} nodes)')
+
+    return gname, nodes, tris, ntags
+
+
+class MuscleGroup:
+    """convenient structure for storing muscle data and loads
+    """
+
+    def __init__(self, nodes, tris, ntags, loads):
+        self.nodes = nodes
+        self.tris = tris
+        self.ntags = ntags
+        self.loads = loads
+
diff --git a/models/dolicorhynchops/dolicorhynchops_10k.py b/models/dolicorhynchops/dolicorhynchops_10k.py
index 6087c273a6ee9a90004106a5cb195006791338af..0ebd8da4ff458f3eb5fe53ccc87792cdf50011d5 100644
--- a/models/dolicorhynchops/dolicorhynchops_10k.py
+++ b/models/dolicorhynchops/dolicorhynchops_10k.py
@@ -9,9 +9,9 @@ def parms(d={}):
     path = 'dolicorhynchops/10k'
     p['bone'] = f'{path}/mandible.stl'
     p['contact_pts'] = [[-10.20362, -17.46838, -229.9061],
-                  [-11.92466, 26.3042, -229.5354]]
+                        [-11.92466, 26.3042, -229.5354]]
     p['axis_pt1'] = f'{path}/LTMJ.off'
-    p['axis_pt2'] = [ -8.716309, 79.13171, 233.8385 ]
+    p['axis_pt2'] = [-8.716309, 79.13171, 233.8385]
     p['muscles'] = [
         {
             'file': f'{path}/Lmuscle.stl',
@@ -45,3 +45,8 @@ def parms(d={}):
 def getMetafor(p={}):
     import bonemodel as model
     return model.getMetafor(parms(p))
+
+
+if __name__ == "__main__":
+    import models.bonemodel2 as model
+    model.solve(parms())
diff --git a/run.py b/run.py
new file mode 100644
index 0000000000000000000000000000000000000000..4bb930750261ec4f32ba0b32c8c1a60cd5f0da4d
--- /dev/null
+++ b/run.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# runs a test as if it was installed
+
+import os, platform
+
+if 'Windows' in platform.uname():
+    # remove tbb from the PATH
+    #   otherwise: "Intel MKL FATAL ERROR: Cannot load mkl_intel_thread.dll"
+    paths = os.environ['PATH'].split(';')
+    corr_path = [ p for p in paths if not 'tbb' in p]
+    os.environ['PATH'] = ';'.join(corr_path)
+    for p in corr_path:
+        print(p)    
+
+if __name__ == "__main__":
+    import sys
+    import os
+    # adds "." to the pythonpath
+    thisdir = os.path.split(os.path.abspath(__file__))[0]
+    thisdir = os.path.normcase(thisdir)
+    print(f'adding {thisdir} to pythonpath')
+    sys.path.append(thisdir)
+
+    # add binary dir to PYTHONPATH
+    pyexe = os.path.basename(sys.executable)
+    print(f'pyexe={pyexe}')
+    sys.path.append(os.path.join(thisdir, 'cxxfem',
+                    'build', 'bin'))  # gcc/mingw
+    sys.path.append(os.path.join(thisdir, 'cxxfem',
+                    'build', 'bin', 'Release'))  # msvc
+
+    # display env variables
+    try:
+        print(f"OMP_NUM_THREADS={os.environ['OMP_NUM_THREADS']}")
+    except:
+        print(f"OMP_NUM_THREADS=[not set]")
+
+    # parse args
+    import argparse
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-v", "--verb", help="increase output verbosity", action="count", default=0)
+    parser.add_argument("--nogui", help="disable any graphical output",
+                        action="store_true")
+    parser.add_argument('file', nargs='*', help='python files')
+    args = parser.parse_args()
+
+    # run all tests sequentially
+    for testname in args.file:
+        testname = os.path.abspath(testname)
+        testname = os.path.normcase(testname)  # F:/ => f:/ on Windows
+        # create workspace
+        common = os.path.commonprefix((testname, thisdir + os.sep))
+        resdir = testname[len(common):].replace(os.sep, "_")
+        resdir, ext = os.path.splitext(resdir)
+        wdir = os.path.join('workspace', resdir)
+        print('workspace=', wdir)
+        if not os.path.isdir(wdir):
+            os.makedirs(wdir)
+        os.chdir(wdir)
+        if ext == '.py':
+            # python script
+            __file__ = testname
+            exec(open(testname, encoding='utf-8').read())
diff --git a/run.spec b/run.spec
new file mode 100644
index 0000000000000000000000000000000000000000..d795a74140de6545cc5f3d3ba26e24df71112b4d
--- /dev/null
+++ b/run.spec
@@ -0,0 +1,60 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+# pyinstaller --noconfirm run.spec
+
+block_cipher = None
+
+
+a = Analysis(
+    ['run.py'],
+    pathex=['cxxfem\\build\\bin\\Release'],
+    binaries=[
+        (r'C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2019.5.281\windows\redist\intel64_win\mkl\mkl_vml_avx512.dll', '.'),
+        (r'C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2019.5.281\windows\redist\intel64_win\mkl\mkl_rt.dll', '.'),
+        (r'C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2019.5.281\windows\redist\intel64_win\mkl\mkl_intel_thread.dll', '.'),
+        (r'C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2019.5.281\windows\redist\intel64_win\mkl\mkl_core.dll', '.'),
+        (r'C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2019.5.281\windows\redist\intel64_win\mkl\mkl_avx512.dll', '.'),
+        (r'C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2019.5.281\windows\redist\intel64_win\mkl\mkl_avx.dll', '.'),
+        (r'C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2019.5.281\windows\redist\intel64_win\mkl\mkl_def.dll', '.'),
+        (r'C:\Program Files (x86)\IntelSWTools\compilers_and_libraries_2019.5.281\windows\redist\intel64_win\compiler\libiomp5md.dll', '.')
+    ],
+    datas=[],
+    hiddenimports=['cxxfem','femi','gmsh','bonemodel'],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    win_no_prefer_redirects=False,
+    win_private_assemblies=False,
+    cipher=block_cipher,
+    noarchive=False,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    [],
+    exclude_binaries=True,
+    name='run',
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    console=True,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+)
+coll = COLLECT(
+    exe,
+    a.binaries,
+    a.zipfiles,
+    a.datas,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    name='run',
+)