diff --git a/fossils.py b/fossils.py
index fc77b59d7185d0767a885099fb1228a1f251eb3b..cb2cf1719ab25935c526d7dc116579386820306f 100755
--- a/fossils.py
+++ b/fossils.py
@@ -7,7 +7,7 @@
 # if starting Qt fails with
 #   qt.qpa.plugin: Could not find the Qt platform plugin "windows" in ""
 # this means that an application in the PATH provides another version of Qt
-# which is incompatible (MikTeX) 
+# which is incompatible (MikTeX)
 #   => move MikTeX to the bottom of the user PATH.
 
 from PyQt5.QtCore import *
@@ -17,10 +17,16 @@ from gui.ui_fossils import Ui_Form
 import sys
 import os
 import gui.resources
+import numpy as np
+import gmsh
+if not gmsh.isInitialized():
+    gmsh.initialize()
+
 
 class Window(QWidget, Ui_Form):
     """Minimal GUI asking for a file and running it
     """
+
     def __init__(self, parent=None):
         super(Window, self).__init__(parent)
         self.setupUi(self)
@@ -40,28 +46,30 @@ class Window(QWidget, Ui_Form):
         # read Qt settings for the application
         settings = QSettings()
         # self.restoreGeometry(settings.value("Geometry", self.saveGeometry()))
-        self.inpFileLineEdit.setText(settings.value("inpFile", self.default_inputfile()))
-        self.wrkspLineEdit.setText(settings.value("workspace", self.default_wrksp()))
+        self.inpFileLineEdit.setText(settings.value(
+            "inpFile", self.default_inputfile()))
+        self.wrkspLineEdit.setText(settings.value(
+            "workspace", self.default_wrksp()))
 
         self.action = 'cancelled'
 
-        #iconfile = os.path.join('fossils.png')
+        # iconfile = os.path.join('fossils.png')
         self.setWindowIcon(QIcon(":/fossils.png"))
 
-
     def default_inputfile(self):
-        testnames = [ 
-            os.path.join(os.path.dirname(__file__),'models','others','dolicorhynchops','dolicorhynchops_10k.py'),
-            os.path.join(os.path.dirname(__file__),'models','dolicorhynchops','dolicorhynchops_10k.py'),
+        testnames = [
+            os.path.join(os.path.dirname(__file__), 'models', 'others',
+                         'dolicorhynchops', 'dolicorhynchops_10k.py'),
+            os.path.join(os.path.dirname(__file__), 'models',
+                         'dolicorhynchops', 'dolicorhynchops_10k.py'),
         ]
         for name in testnames:
             if os.path.isfile(name):
                 return name
         return ""
 
-
     def default_wrksp(self):
-        for key in ['USERPROFILE', 'HOME' ]:
+        for key in ['USERPROFILE', 'HOME']:
             try:
                 folder = os.environ[key]
                 if os.path.isdir(folder):
@@ -70,7 +78,6 @@ class Window(QWidget, Ui_Form):
                 pass
         return ""
 
-
     def on_runPushButton_pressed(self):
         if not os.path.isfile(self.inpFileLineEdit.text()):
             QMessageBox.critical(self, "Error", "Input file does not exist!")
@@ -78,8 +85,8 @@ class Window(QWidget, Ui_Form):
 
         if not os.path.isdir(self.wrkspLineEdit.text()):
             reply = QMessageBox.question(self, 'Message',
-                                        "Workspace does not exist - do you want to continue?", 
-                                        QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
+                                         "Workspace does not exist - do you want to continue?",
+                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
             if reply == QMessageBox.No:
                 return
         # wdir = build_workspace_name(self.wrkspLineEdit.text(), self.inpFileLineEdit.text())
@@ -87,15 +94,23 @@ class Window(QWidget, Ui_Form):
         self.action = 'run'
         self.close()
 
-
     def on_viewPushButton_pressed(self):
-        wdir = build_workspace_name(self.wrkspLineEdit.text(), self.inpFileLineEdit.text())
+        wdir = build_workspace_name(
+            self.wrkspLineEdit.text(), self.inpFileLineEdit.text())
         if not os.path.isdir(wdir):
             QMessageBox.critical(self, "Error", f"No results in {wdir}!")
             return
         self.action = 'post'
         self.close()
 
+    def on_toCSVPushButton_pressed(self):
+        wdir = build_workspace_name(
+            self.wrkspLineEdit.text(), self.inpFileLineEdit.text())
+        if not os.path.isdir(wdir):
+            QMessageBox.critical(self, "Error", f"No results in {wdir}!")
+            return
+        self.action = 'csv'
+        self.close()
 
     def on_inpFilePushButton_pressed(self):
         # try to set a relevant starting folder
@@ -107,11 +122,10 @@ class Window(QWidget, Ui_Form):
                 default = curfolder
 
         fname = QFileDialog.getOpenFileName(
-            self, 'Select input file', default, filter='Python File (*.py)')    
+            self, 'Select input file', default, filter='Python File (*.py)')
         pyfile = fname[0]
         if pyfile:
-            self.inpFileLineEdit.setText(QDir.toNativeSeparators(pyfile))    
-
+            self.inpFileLineEdit.setText(QDir.toNativeSeparators(pyfile))
 
     def on_inpFileEditPushButton_pressed(self):
         """Open input file with a text editor
@@ -119,35 +133,30 @@ class Window(QWidget, Ui_Form):
         editors = [
             "C:/Program Files/Just Great Software/EditPad Pro 8/EditPadPro8.exe",
             "C:/Windows/notepad.exe"
-            ]
+        ]
         for editor in editors:
             if os.path.isfile(editor):
                 import subprocess
-                subprocess.Popen([ editor, self.inpFileLineEdit.text() ])
+                subprocess.Popen([editor, self.inpFileLineEdit.text()])
                 break
 
-
     def on_inpFileResetPushButton_pressed(self):
         self.inpFileLineEdit.setText(self.default_inputfile())
 
-
     def on_wrkspExplorePushButton_pressed(self):
         """Browse workspace folder with system explorer
         """
         os.startfile(self.wrkspLineEdit.text())
 
-
     def on_wrkspPushButton_pressed(self):
         dir = QFileDialog.getExistingDirectory(
             self, "Select output folder", self.wrkspLineEdit.text())
         if dir:
-            self.wrkspLineEdit.setText(QDir.toNativeSeparators(dir))  
-
+            self.wrkspLineEdit.setText(QDir.toNativeSeparators(dir))
 
     def on_wrkspResetPushButton_pressed(self):
         self.wrkspLineEdit.setText(self.default_wrksp())
 
-
     def closeEvent(self, event):
         """save settings to registry and quit
         """
@@ -163,20 +172,18 @@ def run_simulation(testname):
     """
     # thanks to the "compile" command, the filename appears in the stack
     # trace in case of errors
-    script = open(testname, encoding='utf-8').read()   
-    exec(compile(script, testname, 'exec'), 
-        {'__file__': testname, '__name__':'__main__'})    
+    script = open(testname, encoding='utf-8').read()
+    exec(compile(script, testname, 'exec'),
+         {'__file__': testname, '__name__': '__main__'})
 
 
-def view_results():
-    """Load/display results in the current folder
+def load_previous_results():
+    """Load previously computed results from the current folder
     """
-    import gmsh
-    if not gmsh.isInitialized():
-        gmsh.initialize()
     # load empty mesh
     gmsh.merge('mesh.msh')
     gmsh.option.setNumber("General.Verbosity", 3)
+
     # load views from the option file
     views = []
     with open('post.opt') as f:
@@ -192,12 +199,57 @@ def view_results():
         gmsh.merge(v[1])
     print('loading options...')
     gmsh.merge('post.opt')
+    return views
+
+def view_results():
+    """Load/display results in the current folder
+    """
+    views = load_previous_results()
     print('starting Gmsh GUI, please wait...')
     gmsh.fltk.run()
 
 
+def convert_results_to_csv():
+    """Converts results to CSV files
+    """
+    views = load_previous_results()
+
+    # export results to csv
+    for v in views:
+        csvfile = v[1].replace('.msh', '.csv')
+        dataType, tags, data, time, numComp = gmsh.view.getModelData(tag=int(v[0])+1, step=0) # assume tag=index+1       
+        print(f'exporting "{csvfile}" ({dataType})')
+        
+        # export raw "data" to csv
+        # np.savetxt(csvfile, data, delimiter=',')
+
+        # if tensor => compute j2 for each node
+        if numComp == 9:
+            
+            ndata = np.array(data)
+            data = np.array([])
+            ncols = ndata.shape[1]
+            nnods = ncols/9
+            for i in range(int(nnods)):
+                xx, xy, xz, yx, yy, yz, zx, zy, zz = ndata[:,i*9:(i+1)*9].T
+                j2 = np.sqrt( ((xx-yy)**2 + (yy-zz)**2 + (zz-xx)**2 )/2 + 3*(xy*xy+yz*yz+zx*zx) )
+                if data.any():
+                    data = np.vstack([data,j2])
+                else:
+                    data = j2
+            if nnods == 1:
+                data = data.reshape(1,-1)            
+            data = data.T
+
+        # export "data" to csv with tags
+        f = open(csvfile, "w")
+        for x,y in zip(tags, data):
+            f.write("{}, {}\n".format(x, ', '.join(map(str, y))))
+        f.close()
+
+
 def rm_folder_from_pypath(folder):
-    sys.path = [ p for p in sys.path if not folder in p]
+    sys.path = [p for p in sys.path if not folder in p]
 
 
 def add_folder2pypath(folder):
@@ -209,10 +261,10 @@ def add_folder2pypath(folder):
 def rm_folder_from_path(folder):
     import platform
     if 'Windows' in platform.uname():
-        path = [ p for p in os.environ['PATH'].split(';') if not folder in p]
+        path = [p for p in os.environ['PATH'].split(';') if not folder in p]
         os.environ['PATH'] = ';'.join(path)
         # print(f'{folder} added to PATH')
-        # print(f"os.environ['PATH']={os.environ['PATH']}")    
+        # print(f"os.environ['PATH']={os.environ['PATH']}")
 
 
 def add_folder2path(folder):
@@ -240,15 +292,15 @@ def setup_pythonpath():
     pyexe = os.path.basename(sys.executable)
     print(f'pyexe = {pyexe}')
     add_folder2pypath(os.path.join(this_script_dir, 'cxxfem',
-                    'build', 'bin'))  # gcc/mingw
+                                   'build', 'bin'))  # gcc/mingw
     add_folder2pypath(os.path.join(this_script_dir, 'cxxfem',
-                    'build', 'bin', 'Release'))  # msvc
+                                   'build', 'bin', 'Release'))  # msvc
 
     # allows this script to be run without setting env
     # rm_folder_from_path('gmsh')
     # rm_folder_from_pypath('gmsh')
     # add_folder2pypath(os.path.join(this_script_dir, 'lib',
-    #                 'gmsh-sdk', 'lib'))  # msvc 
+    #                 'gmsh-sdk', 'lib'))  # msvc
     # add_folder2path(os.path.join(this_script_dir, 'lib',
     #                 'gmsh-sdk', 'bin'))  # gmsh
     # print(f'sys.path={sys.path}')
@@ -260,6 +312,7 @@ def setup_pythonpath():
             if os.path.exists(v):
                 os.add_dll_directory(v)
 
+
 def build_workspace_name(workspace, testname):
     """create workspace folder and chdir into it
     """
@@ -268,7 +321,7 @@ def build_workspace_name(workspace, testname):
         #   => workspace + testname
         common = os.path.basename(testname)
         resdir = common.replace(os.sep, "_")
-        resdir, ext = os.path.splitext(resdir)            
+        resdir, ext = os.path.splitext(resdir)
         wdir = os.path.join(workspace, resdir)
     else:
         # workspace is not given:
@@ -283,7 +336,6 @@ def build_workspace_name(workspace, testname):
         wdir = os.path.abspath(os.path.join('workspace', resdir))
     return wdir
 
-    
 
 if __name__ == "__main__":
 
@@ -302,7 +354,8 @@ if __name__ == "__main__":
     os.environ['OMP_NUM_THREADS'] = str(args.k)
     cxxfem.set_num_threads(args.k)
 
-    __file__ = os.path.abspath(__file__) # relative path on Linux with python <=3.8
+    # relative path on Linux with python <=3.8
+    __file__ = os.path.abspath(__file__)
 
     # display env variables
     try:
@@ -310,7 +363,6 @@ if __name__ == "__main__":
     except:
         print(f"OMP_NUM_THREADS=[not set]")
 
-    
     # ask for a file if not given => starts the GUI
     if not args.file:
         # QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
@@ -320,7 +372,8 @@ if __name__ == "__main__":
         win = Window()
         win.setWindowTitle("Fossils")
         win.show()
-        win.setWindowState(Qt.WindowActive) # try to make it appear above any other window (and particularly the console window)
+        # try to make it appear above any other window (and particularly the console window)
+        win.setWindowState(Qt.WindowActive)
         app.lastWindowClosed.connect(app.quit)
         app.exec_()
 
@@ -329,7 +382,7 @@ if __name__ == "__main__":
         testname = win.inpFileLineEdit.text()
         gui = True
     else:
-        workspace = None # use default
+        workspace = None  # use default
         if args.post:
             action = 'post'
         else:
@@ -340,10 +393,10 @@ if __name__ == "__main__":
     # run the simulation or display results
 
     print(f'action = {action}')
-    if action=='run' or action=='post':
+    if action in ['run', 'post', 'csv']:
         testname = os.path.normcase(testname)  # F:/ => f:/ on Windows
         print(f'testname = {testname}')
-        
+
         wdir = build_workspace_name(workspace, testname)
         print('workspace =', wdir)
         # sys.exit()
@@ -353,11 +406,15 @@ if __name__ == "__main__":
 
         try:
             if action == 'run':
-                tee = cxxfem.Tee('stdout.txt')  # split streams (stdout + logfile)
+                # split streams (stdout + logfile)
+                tee = cxxfem.Tee('stdout.txt')
                 run_simulation(testname)
             elif action == 'post':
                 tee = cxxfem.Tee('post.txt')
                 view_results()
+            elif action == 'csv':
+                tee = cxxfem.Tee('tocsv.txt')
+                convert_results_to_csv()
         except Exception as err:
             print(f'\n** ERROR: {err}\n')
             import traceback
diff --git a/gui/fossils.ui b/gui/fossils.ui
index c849628d3a4238a57c201c7a3514688e12a3d98f..60672be53959fc2eb329bc1a8c22188785369cf3 100644
--- a/gui/fossils.ui
+++ b/gui/fossils.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>745</width>
-    <height>154</height>
+    <width>756</width>
+    <height>116</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -129,7 +129,7 @@
     </layout>
    </item>
    <item>
-    <layout class="QHBoxLayout" name="horizontalLayout_2">
+    <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <spacer name="horizontalSpacer_2">
        <property name="orientation">
@@ -169,6 +169,19 @@
        </property>
       </widget>
      </item>
+     <item>
+      <widget class="QPushButton" name="toCSVPushButton">
+       <property name="toolTip">
+        <string>read/display the results stored in the folder of the workspace</string>
+       </property>
+       <property name="statusTip">
+        <string/>
+       </property>
+       <property name="text">
+        <string>to CSV</string>
+       </property>
+      </widget>
+     </item>
      <item>
       <spacer name="horizontalSpacer">
        <property name="orientation">
diff --git a/gui/ui_fossils.py b/gui/ui_fossils.py
index c13b08da698f32da6e46a06b08e2b93c555d40ce..9f53310a0c573fb23ca440e8d4807c1f7e51cba0 100644
--- a/gui/ui_fossils.py
+++ b/gui/ui_fossils.py
@@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
 class Ui_Form(object):
     def setupUi(self, Form):
         Form.setObjectName("Form")
-        Form.resize(745, 154)
+        Form.resize(756, 116)
         self.verticalLayout = QtWidgets.QVBoxLayout(Form)
         self.verticalLayout.setObjectName("verticalLayout")
         self.gridLayout = QtWidgets.QGridLayout()
@@ -58,21 +58,25 @@ class Ui_Form(object):
         self.wrkspResetPushButton.setObjectName("wrkspResetPushButton")
         self.gridLayout.addWidget(self.wrkspResetPushButton, 1, 4, 1, 1)
         self.verticalLayout.addLayout(self.gridLayout)
-        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
-        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+        self.horizontalLayout = QtWidgets.QHBoxLayout()
+        self.horizontalLayout.setObjectName("horizontalLayout")
         spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
-        self.horizontalLayout_2.addItem(spacerItem)
+        self.horizontalLayout.addItem(spacerItem)
         self.runPushButton = QtWidgets.QPushButton(Form)
         self.runPushButton.setStatusTip("")
         self.runPushButton.setObjectName("runPushButton")
-        self.horizontalLayout_2.addWidget(self.runPushButton)
+        self.horizontalLayout.addWidget(self.runPushButton)
         self.viewPushButton = QtWidgets.QPushButton(Form)
         self.viewPushButton.setStatusTip("")
         self.viewPushButton.setObjectName("viewPushButton")
-        self.horizontalLayout_2.addWidget(self.viewPushButton)
+        self.horizontalLayout.addWidget(self.viewPushButton)
+        self.toCSVPushButton = QtWidgets.QPushButton(Form)
+        self.toCSVPushButton.setStatusTip("")
+        self.toCSVPushButton.setObjectName("toCSVPushButton")
+        self.horizontalLayout.addWidget(self.toCSVPushButton)
         spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
-        self.horizontalLayout_2.addItem(spacerItem1)
-        self.verticalLayout.addLayout(self.horizontalLayout_2)
+        self.horizontalLayout.addItem(spacerItem1)
+        self.verticalLayout.addLayout(self.horizontalLayout)
 
         self.retranslateUi(Form)
         QtCore.QMetaObject.connectSlotsByName(Form)
@@ -100,3 +104,5 @@ class Ui_Form(object):
         self.runPushButton.setText(_translate("Form", "Run"))
         self.viewPushButton.setToolTip(_translate("Form", "read/display the results stored in the folder of the workspace"))
         self.viewPushButton.setText(_translate("Form", "View"))
+        self.toCSVPushButton.setToolTip(_translate("Form", "read/display the results stored in the folder of the workspace"))
+        self.toCSVPushButton.setText(_translate("Form", "to CSV"))