From e7e32cc2d671be385df76a13fbe2d88b234f17f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simone=20Bn=C3=A0?= <simone.bna@cineca.it>
Date: Sun, 23 Dec 2018 20:40:25 +0100
Subject: [PATCH] openFOAM-paraview-catalyst (#10129)

* paraview: adding variants to use external packages as internal do not compile

* paraview: add latest paraview version

* catalyst: fixed libvtkexpat undefined reference linking error in Catalyst 5.5

* catalyst: add latest catalyst version

* catalyst: added ParaView_DIR env variable to catalyst module

* add paraview, catalyst patches

- https://gitlab.kitware.com/vtk/vtk-m/merge_requests/1166
- https://gitlab.kitware.com/paraview/paraview/merge_requests/2433
- https://gitlab.kitware.com/paraview/paraview/merge_requests/2436

* - Handle updated library paths for catalyst.

  Versions 5.4 and old places libraries under a paraview subdirectory.
  Eg, "lib/paraview-5.4", they are now placed directly under "lib"

- Minor code style cleanup

* Handle update library and python paths for ParaView-5.5

* catalyst: added ParaView_DIR path to spack_env

* BUG: applied the patch to the extracted catalyst source files

* paraview: added missing self to a member variable

* paraview: added Paraview_DIR to env

* catalyst: added osmesa variant

* of-catalyst: added new package

* add (FOAM,WM)_PROJECT_DIR  also to spack_env environment

* depends on first openfoam release supporting catalyst

* openfoam-com: added missing env variables to module generation

* openfoam: fixed flake8 errors

* of-catalyst: added full variant and openfoam version dependency

* paraview: adding variants to use external packages as internal do not compile

* catalyst: fixed libvtkexpat undefined reference linking error in Catalyst 5.5

* catalyst: added ParaView_DIR env variable to catalyst module

* add paraview, catalyst patches

- https://gitlab.kitware.com/vtk/vtk-m/merge_requests/1166
- https://gitlab.kitware.com/paraview/paraview/merge_requests/2433
- https://gitlab.kitware.com/paraview/paraview/merge_requests/2436

* - Handle updated library paths for catalyst.

  Versions 5.4 and old places libraries under a paraview subdirectory.
  Eg, "lib/paraview-5.4", they are now placed directly under "lib"

- Minor code style cleanup

* Handle update library and python paths for ParaView-5.5

* catalyst: added ParaView_DIR path to spack_env

* BUG: applied the patch to the extracted catalyst source files

* of-catalyst: added new package

* add (FOAM,WM)_PROJECT_DIR  also to spack_env environment

* depends on first openfoam release supporting catalyst

* paraview: added missing self to a member variable

* openfoam-com: added missing env variables to module generation

* openfoam: fixed flake8 errors

* paraview: added Paraview_DIR to env

* catalyst: added osmesa variant

* of-catalyst: added full variant and openfoam version dependency

* paraview-catalyst: use always external expat and netcdf

* of-catalyst: reformatted package description

* paraview-catalyst: removed duplicated function

* catalyst: fixed flake8 error

* of-catalyst: fixed license header

* of-catalyst: minor changes

* of-catalyst: renamed gitrepo with git

* of-catalyst: removed useless gitrepo parameter
---
 .../builtin/packages/catalyst/package.py      | 122 +++--
 .../catalyst/vtkm-catalyst-pv551.patch        | 487 +++++++++++++++++
 .../builtin/packages/of-catalyst/package.py   |  48 ++
 .../builtin/packages/openfoam-com/package.py  |  16 +-
 .../builtin/packages/paraview/package.py      |  53 +-
 .../paraview/vtkm-catalyst-pv551.patch        | 510 ++++++++++++++++++
 6 files changed, 1176 insertions(+), 60 deletions(-)
 create mode 100644 var/spack/repos/builtin/packages/catalyst/vtkm-catalyst-pv551.patch
 create mode 100644 var/spack/repos/builtin/packages/of-catalyst/package.py
 create mode 100644 var/spack/repos/builtin/packages/paraview/vtkm-catalyst-pv551.patch

diff --git a/var/spack/repos/builtin/packages/catalyst/package.py b/var/spack/repos/builtin/packages/catalyst/package.py
index d9be7b17dd..03f9e4dbcf 100644
--- a/var/spack/repos/builtin/packages/catalyst/package.py
+++ b/var/spack/repos/builtin/packages/catalyst/package.py
@@ -7,6 +7,7 @@
 import os
 import subprocess
 import llnl.util.tty as tty
+from spack.patch import absolute_path_for_package
 
 
 class Catalyst(CMakePackage):
@@ -33,17 +34,31 @@ class Catalyst(CMakePackage):
     variant('python', default=False, description='Enable Python support')
     variant('essentials', default=False, description='Enable Essentials support')
     variant('extras', default=False, description='Enable Extras support')
-    variant('rendering', default=False, description='Enable Vtk Rendering support')
+    variant('rendering', default=False, description='Enable VTK Rendering support')
+    variant('osmesa', default=True, description='Use offscreen rendering')
 
-    depends_on('git')
+    depends_on('git', type='build')
     depends_on('mpi')
     depends_on('python@2:2.8', when='+python', type=("build", "link", "run"))
     depends_on('python', when='~python', type=("build"))
     depends_on('mesa', when='+rendering')
-    depends_on("libx11", when='+rendering')
-    depends_on("libxt", when='+rendering')
+    depends_on('libx11', when='+rendering')
+    depends_on('libxt', when='+rendering')
     depends_on('cmake@3.3:', type='build')
 
+    @when('@5.5.0:5.5.2')
+    def patch(self):
+        """Apply the patch (it should be fixed in Paraview 5.6)
+        at the package dir to the source code in
+        root_cmakelists_dir."""
+        patch_name = 'vtkm-catalyst-pv551.patch'
+        pkg_dir = os.path.dirname(absolute_path_for_package(self))
+        patch = which("patch", required=True)
+        with working_dir(self.root_cmakelists_dir):
+            patch('-s', '-p', '1', '-i',
+                  join_path(pkg_dir, patch_name),
+                  "-d", '.')
+
     def url_for_version(self, version):
         """Handle ParaView version-based custom URLs."""
         if version < Version('5.1.0'):
@@ -53,6 +68,30 @@ def url_for_version(self, version):
         else:
             return self._urlfmt_xz.format(version.up_to(2), version, '')
 
+    @property
+    def paraview_subdir(self):
+        """The paraview subdirectory name as paraview-major.minor"""
+        return 'paraview-{0}'.format(self.spec.version.up_to(2))
+
+    @property
+    def editions(self):
+        """Transcribe spack variants into names of Catalyst Editions"""
+        selected = ['Base']  # Always required
+
+        if '+python' in self.spec:
+            selected.append('Enable-Python')
+
+        if '+essentials' in self.spec:
+            selected.append('Essentials')
+
+        if '+extras' in self.spec:
+            selected.append('Extras')
+
+        if '+rendering' in self.spec:
+            selected.append('Rendering-Base')
+
+        return selected
+
     def do_stage(self, mirror_only=False):
         """Unpacks and expands the fetched tarball.
         Then, generate the catalyst source files."""
@@ -62,38 +101,15 @@ def do_stage(self, mirror_only=False):
         paraview_dir = os.path.join(self.stage.path,
                                     'ParaView-v' + str(self.version))
         catalyst_script = os.path.join(paraview_dir, 'Catalyst', 'catalyze.py')
+        editions_dir = os.path.join(paraview_dir, 'Catalyst', 'Editions')
         catalyst_source_dir = os.path.abspath(self.root_cmakelists_dir)
 
         command = ['python', catalyst_script,
-                   '-r', paraview_dir]
+                   '-r', paraview_dir,
+                   '-o', catalyst_source_dir]
 
-        catalyst_edition = os.path.join(paraview_dir, 'Catalyst',
-                                        'Editions', 'Base')
-        command.append('-i')
-        command.append(catalyst_edition)
-        if '+python' in self.spec:
-            catalyst_edition = os.path.join(paraview_dir, 'Catalyst',
-                                            'Editions', 'Enable-Python')
-            command.append('-i')
-            command.append(catalyst_edition)
-        if '+essentials' in self.spec:
-            catalyst_edition = os.path.join(paraview_dir, 'Catalyst',
-                                            'Editions', 'Essentials')
-            command.append('-i')
-            command.append(catalyst_edition)
-        if '+extras' in self.spec:
-            catalyst_edition = os.path.join(paraview_dir, 'Catalyst',
-                                            'Editions', 'Extras')
-            command.append('-i')
-            command.append(catalyst_edition)
-        if '+rendering' in self.spec:
-            catalyst_edition = os.path.join(paraview_dir, 'Catalyst',
-                                            'Editions', 'Rendering-Base')
-            command.append('-i')
-            command.append(catalyst_edition)
-
-        command.append('-o')
-        command.append(catalyst_source_dir)
+        for edition in self.editions:
+            command.extend(['-i', os.path.join(editions_dir, edition)])
 
         if not os.path.isdir(catalyst_source_dir):
             os.mkdir(catalyst_source_dir)
@@ -104,15 +120,29 @@ def do_stage(self, mirror_only=False):
                                                     self.stage.path))
 
     def setup_environment(self, spack_env, run_env):
+        # paraview 5.5 and later
+        # - cmake under lib/cmake/paraview-5.5
+        # - libs  under lib
+        # - python bits under lib/python2.8/site-packages
         if os.path.isdir(self.prefix.lib64):
             lib_dir = self.prefix.lib64
         else:
             lib_dir = self.prefix.lib
-        paraview_version = 'paraview-%s' % self.spec.version.up_to(2)
-        run_env.prepend_path('LIBRARY_PATH', join_path(lib_dir,
-                             paraview_version))
-        run_env.prepend_path('LD_LIBRARY_PATH', join_path(lib_dir,
-                             paraview_version))
+
+        if self.spec.version <= Version('5.4.1'):
+            lib_dir = join_path(lib_dir, paraview_subdir)
+        run_env.set('ParaView_DIR', self.prefix)
+        run_env.prepend_path('LIBRARY_PATH', lib_dir)
+        run_env.prepend_path('LD_LIBRARY_PATH', lib_dir)
+
+        if '+python' in self.spec:
+            python_version = self.spec['python'].version.up_to(2)
+            run_env.prepend_path('PYTHONPATH', join_path(lib_dir,
+                                 'python{0}'.format(python_version),
+                                 'site-packages'))
+
+    def setup_dependent_environment(self, spack_env, run_env, dependent_spec):
+        spack_env.set('ParaView_DIR', self.prefix)
 
     @property
     def root_cmakelists_dir(self):
@@ -136,8 +166,24 @@ def build_directory(self):
 
     def cmake_args(self):
         """Populate cmake arguments for Catalyst."""
+        spec = self.spec
+
+        def variant_bool(feature, on='ON', off='OFF'):
+            """Ternary for spec variant to ON/OFF string"""
+            if feature in spec:
+                return on
+            return off
+
+        def nvariant_bool(feature):
+            """Negated ternary for spec variant to OFF/ON string"""
+            return variant_bool(feature, on='OFF', off='ON')
+
         cmake_args = [
-            '-DPARAVIEW_GIT_DESCRIBE=v%s' % str(self.version)
+            '-DPARAVIEW_GIT_DESCRIBE=v%s' % str(self.version),
+            '-DVTK_USE_SYSTEM_EXPAT:BOOL=ON',
+            '-DVTK_USE_X:BOOL=%s' % nvariant_bool('+osmesa'),
+            '-DVTK_USE_OFFSCREEN:BOOL=%s' % variant_bool('+osmesa'),
+            '-DVTK_OPENGL_HAS_OSMESA:BOOL=%s' % variant_bool('+osmesa')
         ]
         return cmake_args
 
diff --git a/var/spack/repos/builtin/packages/catalyst/vtkm-catalyst-pv551.patch b/var/spack/repos/builtin/packages/catalyst/vtkm-catalyst-pv551.patch
new file mode 100644
index 0000000000..fa610722af
--- /dev/null
+++ b/var/spack/repos/builtin/packages/catalyst/vtkm-catalyst-pv551.patch
@@ -0,0 +1,487 @@
+# The catalyst changes (the working directory for output) are slated for
+# paraview-5.6.
+# They are API-compatible with paraview-5.5 but not ABI compatible.
+#
+# https://gitlab.kitware.com/paraview/paraview/merge_requests/2433
+# https://gitlab.kitware.com/paraview/paraview/merge_requests/2436
+#
+--- Catalyst-v5.5.0/CoProcessing/Catalyst/vtkCPProcessor.cxx.orig	2018-04-06 22:03:33.000000000 +0200
++++ Catalyst-v5.5.0/CoProcessing/Catalyst/vtkCPProcessor.cxx	2018-05-11 12:02:26.894772713 +0200
+@@ -38,6 +38,7 @@
+ #include "vtkStringArray.h"
+ 
+ #include <list>
++#include <vtksys/SystemTools.hxx>
+ 
+ struct vtkCPProcessorInternals
+ {
+@@ -47,12 +48,13 @@
+ };
+ 
+ vtkStandardNewMacro(vtkCPProcessor);
+-vtkMultiProcessController* vtkCPProcessor::Controller = NULL;
++vtkMultiProcessController* vtkCPProcessor::Controller = nullptr;
+ //----------------------------------------------------------------------------
+ vtkCPProcessor::vtkCPProcessor()
+ {
+   this->Internal = new vtkCPProcessorInternals;
+-  this->InitializationHelper = NULL;
++  this->InitializationHelper = nullptr;
++  this->WorkingDirectory = nullptr;
+ }
+ 
+ //----------------------------------------------------------------------------
+@@ -61,14 +63,15 @@
+   if (this->Internal)
+   {
+     delete this->Internal;
+-    this->Internal = NULL;
++    this->Internal = nullptr;
+   }
+ 
+   if (this->InitializationHelper)
+   {
+     this->InitializationHelper->Delete();
+-    this->InitializationHelper = NULL;
++    this->InitializationHelper = nullptr;
+   }
++  this->SetWorkingDirectory(nullptr);
+ }
+ 
+ //----------------------------------------------------------------------------
+@@ -95,7 +98,7 @@
+ {
+   if (which < 0 || which >= this->GetNumberOfPipelines())
+   {
+-    return NULL;
++    return nullptr;
+   }
+   int counter = 0;
+   vtkCPProcessorInternals::PipelineListIterator iter = this->Internal->Pipelines.begin();
+@@ -108,7 +111,7 @@
+     counter++;
+     iter++;
+   }
+-  return NULL;
++  return nullptr;
+ }
+ 
+ //----------------------------------------------------------------------------
+@@ -130,17 +133,41 @@
+ }
+ 
+ //----------------------------------------------------------------------------
+-int vtkCPProcessor::Initialize()
++int vtkCPProcessor::Initialize(const char* workingDirectory)
+ {
+-  if (this->InitializationHelper == NULL)
++  if (this->InitializationHelper == nullptr)
+   {
+     this->InitializationHelper = this->NewInitializationHelper();
+   }
++  // make sure the directory exists here so that we only do it once
++  if (workingDirectory)
++  {
++    vtkMultiProcessController* controller = vtkMultiProcessController::GetGlobalController();
++    int success = 1;
++    if (controller == nullptr || controller->GetLocalProcessId() == 0)
++    {
++      success = vtksys::SystemTools::MakeDirectory(workingDirectory) == true ? 1 : 0;
++      if (success == 0)
++      {
++        vtkWarningMacro("Could not make "
++          << workingDirectory << " directory. "
++          << "Results will be generated in current working directory instead.");
++      }
++    }
++    if (controller)
++    {
++      controller->Broadcast(&success, 1, 0);
++    }
++    if (success)
++    {
++      this->SetWorkingDirectory(workingDirectory);
++    }
++  }
+   return 1;
+ }
+ 
+ //----------------------------------------------------------------------------
+-int vtkCPProcessor::Initialize(vtkMPICommunicatorOpaqueComm& comm)
++int vtkCPProcessor::Initialize(vtkMPICommunicatorOpaqueComm& comm, const char* workingDirectory)
+ {
+ #ifdef PARAVIEW_USE_MPI
+   if (vtkCPProcessor::Controller)
+@@ -148,7 +175,7 @@
+     vtkErrorMacro("Can only initialize with a communicator once per process.");
+     return 0;
+   }
+-  if (this->InitializationHelper == NULL)
++  if (this->InitializationHelper == nullptr)
+   {
+     vtkMPICommunicator* communicator = vtkMPICommunicator::New();
+     communicator->InitializeExternal(&comm);
+@@ -157,12 +184,12 @@
+     this->Controller = controller;
+     this->Controller->SetGlobalController(controller);
+     communicator->Delete();
+-    return this->Initialize();
++    return this->Initialize(workingDirectory);
+   }
+   return 1;
+ #else
+   static_cast<void>(&comm); // get rid of variable not used warning
+-  return this->Initialize();
++  return this->Initialize(workingDirectory);
+ #endif
+ }
+ 
+@@ -225,6 +252,13 @@
+       input->GetFieldData()->AddArray(catalystChannel);
+     }
+   }
++
++  std::string originalWorkingDirectory;
++  if (this->WorkingDirectory)
++  {
++    originalWorkingDirectory = vtksys::SystemTools::GetCurrentWorkingDirectory();
++    vtksys::SystemTools::ChangeDirectory(this->WorkingDirectory);
++  }
+   for (vtkCPProcessorInternals::PipelineListIterator iter = this->Internal->Pipelines.begin();
+        iter != this->Internal->Pipelines.end(); iter++)
+   {
+@@ -248,6 +282,10 @@
+       }
+     }
+   }
++  if (originalWorkingDirectory.empty() == false)
++  {
++    vtksys::SystemTools::ChangeDirectory(originalWorkingDirectory);
++  }
+   // we want to reset everything here to make sure that new information
+   // is properly passed in the next time.
+   dataDescription->ResetAll();
+@@ -259,7 +297,7 @@
+ {
+   if (this->Controller)
+   {
+-    this->Controller->SetGlobalController(NULL);
++    this->Controller->SetGlobalController(nullptr);
+     this->Controller->Finalize(1);
+     this->Controller->Delete();
+   }
+--- Catalyst-v5.5.0/CoProcessing/Catalyst/vtkCPProcessor.h.orig	2018-04-06 22:03:33.000000000 +0200
++++ Catalyst-v5.5.0/CoProcessing/Catalyst/vtkCPProcessor.h	2018-05-11 12:02:26.894772713 +0200
+@@ -76,14 +76,16 @@
+   virtual void RemoveAllPipelines();
+ 
+   /// Initialize the co-processor. Returns 1 if successful and 0
+-  /// otherwise.
+   /// otherwise. If Catalyst is built with MPI then Initialize()
+   /// can also be called with a specific MPI communicator if
+   /// MPI_COMM_WORLD isn't the proper one. Catalyst is initialized
+-  /// to use MPI_COMM_WORLD by default.
+-  virtual int Initialize();
++  /// to use MPI_COMM_WORLD by default. Both methods have an optional
++  /// workingDirectory argument which will set *WorkingDirectory* so
++  /// that files will be put relative to this directory.
++  virtual int Initialize(const char* workingDirectory = nullptr);
+ #ifndef __WRAP__
+-  virtual int Initialize(vtkMPICommunicatorOpaqueComm& comm);
++  virtual int Initialize(
++    vtkMPICommunicatorOpaqueComm& comm, const char* workingDirectory = nullptr);
+ #endif
+ 
+   /// The Catalyst input field data string array name. This array will
+@@ -111,6 +113,13 @@
+   /// implementation an opportunity to clean up, before it is destroyed.
+   virtual int Finalize();
+ 
++  /// Get the current working directory for outputting Catalyst files.
++  /// If not set then Catalyst output files will be relative to the
++  /// current working directory. This will not affect where Catalyst
++  /// looks for Python scripts. *WorkingDirectory* gets set through
++  /// the *Initialize()* methods.
++  vtkGetStringMacro(WorkingDirectory);
++
+ protected:
+   vtkCPProcessor();
+   virtual ~vtkCPProcessor();
+@@ -118,6 +127,11 @@
+   /// Create a new instance of the InitializationHelper.
+   virtual vtkObject* NewInitializationHelper();
+ 
++  /// Set the current working directory for outputting Catalyst files.
++  /// This is a protected method since simulation code adaptors should
++  /// set this through the *Initialize()* methods.
++  vtkSetStringMacro(WorkingDirectory);
++
+ private:
+   vtkCPProcessor(const vtkCPProcessor&) = delete;
+   void operator=(const vtkCPProcessor&) = delete;
+@@ -125,6 +139,7 @@
+   vtkCPProcessorInternals* Internal;
+   vtkObject* InitializationHelper;
+   static vtkMultiProcessController* Controller;
++  char* WorkingDirectory;
+ };
+ 
+ #endif
+--- Catalyst-v5.5.0/CoProcessing/Catalyst/vtkCPXMLPWriterPipeline.cxx.orig	2018-04-06 22:03:33.000000000 +0200
++++ Catalyst-v5.5.0/CoProcessing/Catalyst/vtkCPXMLPWriterPipeline.cxx	2018-05-11 12:02:26.894772713 +0200
+@@ -31,6 +31,7 @@
+ #include <vtkSmartPointer.h>
+ #include <vtkUnstructuredGrid.h>
+ 
++#include <algorithm>
+ #include <sstream>
+ #include <string>
+ 
+@@ -174,7 +175,7 @@
+ 
+   for (unsigned int i = 0; i < dataDescription->GetNumberOfInputDescriptions(); i++)
+   {
+-    const char* inputName = dataDescription->GetInputDescriptionName(i);
++    std::string inputName = dataDescription->GetInputDescriptionName(i);
+     vtkCPInputDataDescription* idd = dataDescription->GetInputDescription(i);
+     vtkDataObject* grid = idd->GetGrid();
+     if (grid == nullptr)
+@@ -206,6 +207,8 @@
+         vtkSMStringVectorProperty* fileName =
+           vtkSMStringVectorProperty::SafeDownCast(writer->GetProperty("FileName"));
+ 
++        // If we have a / in the channel name we take it out of the filename we're going to write to
++        inputName.erase(std::remove(inputName.begin(), inputName.end(), '/'), inputName.end());
+         std::ostringstream o;
+         if (this->Path.empty() == false)
+         {
+--- Catalyst-v5.5.0/Wrapping/Python/paraview/coprocessing.py.orig	2018-04-06 22:03:33.000000000 +0200
++++ Catalyst-v5.5.0/Wrapping/Python/paraview/coprocessing.py	2018-05-11 12:02:27.038772408 +0200
+@@ -11,22 +11,12 @@
+ from paraview.vtk.vtkPVVTKExtensionsCore import *
+ import math
+ 
+-# -----------------------------------------------------------------------------
+-def IsInModulo(timestep, frequencyArray):
+-    """
+-    Return True if the given timestep is in one of the provided frequency.
+-    This can be interpreted as follow::
+-
+-        isFM = IsInModulo(timestep, [2,3,7])
+-
+-    is similar to::
++# If the user created a filename in a location that doesn't exist by default we'll
++# make the directory for them. This can be changed though by setting createDirectoriesIfNeeded
++# to False.
++createDirectoriesIfNeeded = True
+ 
+-        isFM = (timestep % 2 == 0) or (timestep % 3 == 0) or (timestep % 7 == 0)
+-    """
+-    for frequency in frequencyArray:
+-        if frequency > 0 and (timestep % frequency == 0):
+-            return True
+-    return False
++# -----------------------------------------------------------------------------
+ 
+ class CoProcessor(object):
+     """Base class for co-processing Pipelines.
+@@ -68,6 +58,9 @@
+         self.__CinemaTracks = {}
+         self.__InitialFrequencies = {}
+         self.__PrintEnsightFormatString = False
++        self.__TimeStepToStartOutputAt=0
++        self.__ForceOutputAtFirstCall=False
++        self.__FirstTimeStepIndex = None
+ 
+     def SetPrintEnsightFormatString(self, enable):
+         """If outputting ExodusII files with the purpose of reading them into
+@@ -87,6 +80,17 @@
+                  "Incorrect argument type: %s, must be a dict" % type(frequencies))
+         self.__InitialFrequencies = frequencies
+ 
++    def SetInitialOutputOptions(self, timeStepToStartOutputAt, forceOutputAtFirstCall):
++        """Set the frequencies at which the pipeline needs to be updated.
++           Typically, this is called by the subclass once it has determined what
++           timesteps co-processing will be needed to be done.
++           frequencies is a map, with key->string name of for the simulation
++           input, and value is a list of frequencies.
++           """
++
++        self.__TimeStepToStartOutputAt=timeStepToStartOutputAt
++        self.__ForceOutputAtFirstCall=forceOutputAtFirstCall
++
+     def EnableLiveVisualization(self, enable, frequency = 1):
+         """Call this method to enable live-visualization. When enabled,
+         DoLiveVisualization() will communicate with ParaView server if possible
+@@ -115,7 +119,7 @@
+         # if this is a time step to do live then all of the inputs
+         # must be made available. note that we want the pipeline built
+         # before we do the actual first live connection.
+-        if self.__EnableLiveVisualization and timestep % self.__LiveVisualizationFrequency == 0 \
++        if self.__EnableLiveVisualization and self.NeedToOutput(timestep, self.__LiveVisualizationFrequency) \
+            and self.__LiveVisualizationLink:
+             if self.__LiveVisualizationLink.Initialize(servermanager.ActiveConnection.Session.GetSessionProxyManager()):
+                 num_inputs = datadescription.GetNumberOfInputDescriptions()
+@@ -132,13 +136,13 @@
+         # hasn't been set up yet). If we don't have live enabled
+         # we know that the output frequencies aren't changed and can
+         # just use the initial frequencies.
+-        if self.__InitialFrequencies or not self.__EnableLiveVisualization:
++        if self.__ForceOutputAtFirstCall or self.__InitialFrequencies or not self.__EnableLiveVisualization:
+             num_inputs = datadescription.GetNumberOfInputDescriptions()
+             for cc in range(num_inputs):
+                 input_name = datadescription.GetInputDescriptionName(cc)
+ 
+                 freqs = self.__InitialFrequencies.get(input_name, [])
+-                if self.__EnableLiveVisualization or ( self and IsInModulo(timestep, freqs) ):
++                if self.__EnableLiveVisualization or ( self and self.IsInModulo(timestep, freqs) ):
+                         datadescription.GetInputDescription(cc).AllFieldsOn()
+                         datadescription.GetInputDescription(cc).GenerateMeshOn()
+         else:
+@@ -149,15 +153,14 @@
+             for writer in self.__WritersList:
+                 frequency = writer.parameters.GetProperty(
+                     "WriteFrequency").GetElement(0)
+-                if (timestep % frequency) == 0 or \
+-                   datadescription.GetForceOutput() == True:
++                if self.NeedToOutput(timestep, frequency) or datadescription.GetForceOutput() == True:
+                     writerinputs = cpstate.locate_simulation_inputs(writer)
+                     for writerinput in writerinputs:
+                         datadescription.GetInputDescriptionByName(writerinput).AllFieldsOn()
+                         datadescription.GetInputDescriptionByName(writerinput).GenerateMeshOn()
+ 
+             for view in self.__ViewsList:
+-                if (view.cpFrequency and timestep % view.cpFrequency == 0) or \
++                if (view.cpFrequency and self.NeedToOutput(timestep, view.cpFrequency)) or \
+                    datadescription.GetForceOutput() == True:
+                     viewinputs = cpstate.locate_simulation_inputs_for_view(view)
+                     for viewinput in viewinputs:
+@@ -192,8 +195,7 @@
+         for writer in self.__WritersList:
+             frequency = writer.parameters.GetProperty(
+                 "WriteFrequency").GetElement(0)
+-            if (timestep % frequency) == 0 or \
+-                    datadescription.GetForceOutput() == True:
++            if self.NeedToOutput(timestep, frequency) or datadescription.GetForceOutput() == True:
+                 fileName = writer.parameters.GetProperty("FileName").GetElement(0)
+                 paddingamount = writer.parameters.GetProperty("PaddingAmount").GetElement(0)
+                 helperName = writer.GetXMLName()
+@@ -203,6 +205,23 @@
+                 else:
+                     ts = str(timestep).rjust(paddingamount, '0')
+                     writer.FileName = fileName.replace("%t", ts)
++                if '/' in writer.FileName and createDirectoriesIfNeeded:
++                    oktowrite = [1.]
++                    import vtk
++                    comm = vtk.vtkMultiProcessController.GetGlobalController()
++                    if comm.GetLocalProcessId() == 0:
++                        import os
++                        newDir = writer.FileName[0:writer.FileName.rfind('/')]
++                        try:
++                            os.makedirs(newDir)
++                        except OSError:
++                            if not os.path.isdir(newDir):
++                                print ("ERROR: Cannot make directory for", writer.FileName, ". No data will be written.")
++                                oktowrite[0] = 0.
++                    comm.Broadcast(oktowrite, 1, 0)
++                    if oktowrite[0] == 0:
++                        # we can't make the directory so no reason to update the pipeline
++                        return
+                 writer.UpdatePipeline(datadescription.GetTime())
+ 
+     def WriteImages(self, datadescription, rescale_lookuptable=False,
+@@ -240,7 +259,7 @@
+ 
+         cinema_dirs = []
+         for view in self.__ViewsList:
+-            if (view.cpFrequency and timestep % view.cpFrequency == 0) or \
++            if (view.cpFrequency and self.NeedToOutput(timestep, view.cpFrequency)) or \
+                datadescription.GetForceOutput() == True:
+                 fname = view.cpFileName
+                 ts = str(timestep).rjust(padding_amount, '0')
+@@ -267,6 +286,24 @@
+                     if dirname:
+                         cinema_dirs.append(dirname)
+                 else:
++                    if '/' in fname and createDirectoriesIfNeeded:
++                        oktowrite = [1.]
++                        import vtk
++                        comm = vtk.vtkMultiProcessController.GetGlobalController()
++                        if comm.GetLocalProcessId() == 0:
++                            import os
++                            newDir = fname[0:fname.rfind('/')]
++                            try:
++                                os.makedirs(newDir)
++                            except OSError:
++                                if not os.path.isdir(newDir):
++                                    print ("ERROR: Cannot make directory for", fname, ". No image will be output.")
++                                    oktowrite[0] = 0.
++                        comm.Broadcast(oktowrite, 1, 0)
++                        if oktowrite[0] == 0:
++                            # we can't make the directory so no reason to update the pipeline
++                            return
++
+                     if image_quality is None and fname.endswith('png'):
+                         # for png quality = 0 means no compression. compression can be a potentially
+                         # very costly serial operation on process 0
+@@ -307,7 +344,7 @@
+ 
+ 
+         timeStep = datadescription.GetTimeStep()
+-        if self.__EnableLiveVisualization and timeStep % self.__LiveVisualizationFrequency == 0:
++        if self.__EnableLiveVisualization and self.NeedToOutput(timeStep, self.__LiveVisualizationFrequency):
+             if not self.__LiveVisualizationLink.Initialize(servermanager.ActiveConnection.Session.GetSessionProxyManager()):
+                 return
+ 
+@@ -412,7 +449,7 @@
+         """
+         controller = servermanager.ParaViewPipelineController()
+         # assume that a client only proxy with the same name as a writer
+-        # is available in "insitu_writer_paramters"
++        # is available in "insitu_writer_parameters"
+ 
+         # Since coprocessor sometimes pass writer as a custom object and not
+         # a proxy, we need to handle that. Just creating any arbitrary writer
+@@ -666,3 +703,42 @@
+         #restore what we showed
+         pv_introspect.restore_visibility(pxystate)
+         return os.path.basename(vfname)
++
++    def IsInModulo(self, timestep, frequencies):
++        """
++        Return True if the given timestep is in one of the provided frequency.
++        This can be interpreted as follow::
++
++        isFM = IsInModulo(timestep-timeStepToStartOutputAt, [2,3,7])
++
++        is similar to::
++
++        isFM = (timestep-timeStepToStartOutputAt % 2 == 0) or (timestep-timeStepToStartOutputAt % 3 == 0) or (timestep-timeStepToStartOutputAt % 7 == 0)
++
++        The timeStepToStartOutputAt is the first timestep that will potentially be output.
++        """
++        if timestep < self.__TimeStepToStartOutputAt and not self.__ForceOutputAtFirstCall:
++            return False
++        for frequency in frequencies:
++            if frequency > 0 and self.NeedToOutput(timestep, frequency):
++                return True
++
++        return False
++
++
++    def NeedToOutput(self, timestep, frequency):
++        """
++        Return True if we need to output based on the input timestep and frequency. Checks based
++        __FirstTimeStepIndex, __FirstTimeStepIndex, __ForceOutputAtFirstCall and __TimeStepToStartOutputAt
++        member variables.
++        """
++        if self.__FirstTimeStepIndex == None:
++            self.__FirstTimeStepIndex = timestep
++
++        if self.__ForceOutputAtFirstCall and self.__FirstTimeStepIndex == timestep:
++            return True
++
++        if self.__TimeStepToStartOutputAt <= timestep and (timestep-self.__TimeStepToStartOutputAt) % frequency == 0:
++            return True
++
++        return False
diff --git a/var/spack/repos/builtin/packages/of-catalyst/package.py b/var/spack/repos/builtin/packages/of-catalyst/package.py
new file mode 100644
index 0000000000..ac1caf654e
--- /dev/null
+++ b/var/spack/repos/builtin/packages/of-catalyst/package.py
@@ -0,0 +1,48 @@
+# Copyright 2013-2018 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+from spack import *
+
+
+class OfCatalyst(CMakePackage):
+    """Of-catalyst is a library for OpenFOAM that provides a runtime-selectable
+    function object for embedding ParaView Catalyst in-situ visualization
+    into arbitrary OpenFOAM simulations.
+    Supports in-situ conversion of the following types:
+      1) finite volume meshes and fields, single or multi-region;
+      2) finite area meshes and fields, single region;
+      3) lagrangian (clouds), single or multiple clouds.
+    This offering is part of the community repository supported by OpenCFD Ltd,
+    producer and distributor of the OpenFOAM software via www.openfoam.com,
+    and owner of the OPENFOAM trademark.
+    OpenCFD Ltd has been developing and releasing OpenFOAM since its debut
+    in 2004.
+    """
+
+    # Currently only via git
+    homepage = "https://develop.openfoam.com/Community/catalyst"
+    git = "https://develop.openfoam.com/Community/catalyst.git"
+
+    version('develop', branch='develop')
+    version('1806', tag='v1806')
+
+    variant('full', default=False, description='Build against paraview (full) or catalyst (light)')
+
+    depends_on('openfoam-com@1806', when='@1806', type=('build', 'link', 'run'))
+    depends_on('openfoam-com@develop', when='@develop', type=('build', 'link', 'run'))
+    depends_on('catalyst@5.5:', when='~full')
+    depends_on('paraview@5.5:+osmesa~qt', when='+full')
+
+    root_cmakelists_dir = 'src/catalyst'
+
+    def cmake_args(self):
+        """Populate cmake arguments for ParaView."""
+        cmake_args = [
+            '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=%s' % join_path(
+                self.stage.source_path,
+                'spack-build')
+        ]
+
+        return cmake_args
diff --git a/var/spack/repos/builtin/packages/openfoam-com/package.py b/var/spack/repos/builtin/packages/openfoam-com/package.py
index 33b9daf405..1cd5722003 100644
--- a/var/spack/repos/builtin/packages/openfoam-com/package.py
+++ b/var/spack/repos/builtin/packages/openfoam-com/package.py
@@ -405,16 +405,16 @@ def setup_environment(self, spack_env, run_env):
 
                         # Unneeded bits
                         # -------------
-                        'FOAM_SETTINGS',  # Do not use with modules
-                        'FOAM_INST_DIR',  # Old
-                        'FOAM_(APP|ETC|SRC|SOLVERS|UTILITIES)',
+                        # 'FOAM_SETTINGS',  # Do not use with modules
+                        # 'FOAM_INST_DIR',  # Old
+                        # 'FOAM_(APP|ETC|SRC|SOLVERS|UTILITIES)',
                         # 'FOAM_TUTORIALS',  # can be useful
-                        'WM_OSTYPE',      # Purely optional value
+                        # 'WM_OSTYPE',      # Purely optional value
 
                         # Third-party cruft - only used for orig compilation
                         # -----------------
                         '[A-Z].*_ARCH_PATH',
-                        '(KAHIP|METIS|SCOTCH)_VERSION',
+                        # '(KAHIP|METIS|SCOTCH)_VERSION',
 
                         # User-specific
                         # -------------
@@ -426,6 +426,7 @@ def setup_environment(self, spack_env, run_env):
                     ])
 
                 run_env.extend(mods)
+                spack_env.extend(mods)
                 minimal = False
                 tty.info('OpenFOAM bashrc env: {0}'.format(bashrc))
             except Exception:
@@ -436,8 +437,11 @@ def setup_environment(self, spack_env, run_env):
             tty.info('OpenFOAM minimal env {0}'.format(self.prefix))
             run_env.set('FOAM_PROJECT_DIR', self.projectdir)
             run_env.set('WM_PROJECT_DIR', self.projectdir)
+            spack_env.set('FOAM_PROJECT_DIR', self.projectdir)
+            spack_env.set('WM_PROJECT_DIR', self.projectdir)
             for d in ['wmake', self.archbin]:  # bin added automatically
                 run_env.prepend_path('PATH', join_path(self.projectdir, d))
+                spack_env.prepend_path('PATH', join_path(self.projectdir, d))
 
     def setup_dependent_environment(self, spack_env, run_env, dependent_spec):
         """Location of the OpenFOAM project directory.
@@ -445,7 +449,7 @@ def setup_dependent_environment(self, spack_env, run_env, dependent_spec):
         variable since it would mask the normal OpenFOAM cleanup of
         previous versions.
         """
-        spack_env.set('FOAM_PROJECT_DIR', self.projectdir)
+        self.setup_environment(spack_env, run_env)
 
     @property
     def projectdir(self):
diff --git a/var/spack/repos/builtin/packages/paraview/package.py b/var/spack/repos/builtin/packages/paraview/package.py
index e1c36a063f..aca00f3cde 100644
--- a/var/spack/repos/builtin/packages/paraview/package.py
+++ b/var/spack/repos/builtin/packages/paraview/package.py
@@ -59,7 +59,8 @@ class Paraview(CMakePackage):
     depends_on('libpng')
     depends_on('libtiff')
     depends_on('libxml2')
-    # depends_on('netcdf')
+    depends_on('netcdf')
+    depends_on('expat')
     # depends_on('netcdf-cxx')
     # depends_on('protobuf') # version mismatches?
     # depends_on('sqlite') # external version not supported
@@ -74,6 +75,9 @@ class Paraview(CMakePackage):
     # Broken installation (ui_pqExportStateWizard.h) - fixed in 5.2.0
     patch('ui_pqExportStateWizard.patch', when='@:5.1.2')
 
+    # Broken vtk-m config. Upstream catalyst changes
+    patch('vtkm-catalyst-pv551.patch', when='@5.5.0:5.5.2')
+
     def url_for_version(self, version):
         """Handle ParaView version-based custom URLs."""
         if version < Version('5.1.0'):
@@ -81,34 +85,50 @@ def url_for_version(self, version):
         else:
             return self._urlfmt.format(version.up_to(2), version, '')
 
+    @property
+    def paraview_subdir(self):
+        """The paraview subdirectory name as paraview-major.minor"""
+        return 'paraview-{0}'.format(self.spec.version.up_to(2))
+
     def setup_dependent_environment(self, spack_env, run_env, dependent_spec):
         if os.path.isdir(self.prefix.lib64):
             lib_dir = self.prefix.lib64
         else:
             lib_dir = self.prefix.lib
-        paraview_version = 'paraview-%s' % self.spec.version.up_to(2)
+        spack_env.set('ParaView_DIR', self.prefix)
         spack_env.set('PARAVIEW_VTK_DIR',
-                      join_path(lib_dir, 'cmake', paraview_version))
+                      join_path(lib_dir, 'cmake', self.paraview_subdir))
 
     def setup_environment(self, spack_env, run_env):
+        # paraview 5.5 and later
+        # - cmake under lib/cmake/paraview-5.5
+        # - libs  under lib
+        # - python bits under lib/python2.8/site-packages
         if os.path.isdir(self.prefix.lib64):
             lib_dir = self.prefix.lib64
         else:
             lib_dir = self.prefix.lib
-        paraview_version = 'paraview-%s' % self.spec.version.up_to(2)
-        run_env.prepend_path('LIBRARY_PATH', join_path(lib_dir,
-                             paraview_version))
-        run_env.prepend_path('LD_LIBRARY_PATH', join_path(lib_dir,
-                             paraview_version))
+
+        run_env.set('ParaView_DIR', self.prefix)
         run_env.set('PARAVIEW_VTK_DIR',
-                    join_path(lib_dir, 'cmake', paraview_version))
+                    join_path(lib_dir, 'cmake', self.paraview_subdir))
+
+        if self.spec.version <= Version('5.4.1'):
+            lib_dir = join_path(lib_dir, self.paraview_subdir)
+
+        run_env.prepend_path('LIBRARY_PATH', lib_dir)
+        run_env.prepend_path('LD_LIBRARY_PATH', lib_dir)
+
         if '+python' in self.spec:
-            run_env.prepend_path('PYTHONPATH', join_path(lib_dir,
-                                 paraview_version))
-            run_env.prepend_path('PYTHONPATH', join_path(lib_dir,
-                                 paraview_version, 'site-packages'))
-            run_env.prepend_path('PYTHONPATH', join_path(lib_dir,
-                                 paraview_version, 'site-packages', 'vtk'))
+            if self.spec.version <= Version('5.4.1'):
+                pv_pydir = join_path(lib_dir, 'site-packages')
+                run_env.prepend_path('PYTHONPATH', pv_pydir)
+                run_env.prepend_path('PYTHONPATH', join_path(pv_pydir, 'vtk'))
+            else:
+                python_version = self.spec['python'].version.up_to(2)
+                run_env.prepend_path('PYTHONPATH', join_path(lib_dir,
+                                     'python{0}'.format(python_version),
+                                     'site-packages'))
 
     def cmake_args(self):
         """Populate cmake arguments for ParaView."""
@@ -139,7 +159,8 @@ def nvariant_bool(feature):
             '-DVTK_USE_SYSTEM_HDF5:BOOL=%s' % variant_bool('+hdf5'),
             '-DVTK_USE_SYSTEM_JPEG:BOOL=ON',
             '-DVTK_USE_SYSTEM_LIBXML2:BOOL=ON',
-            '-DVTK_USE_SYSTEM_NETCDF:BOOL=OFF',
+            '-DVTK_USE_SYSTEM_NETCDF:BOOL=ON',
+            '-DVTK_USE_SYSTEM_EXPAT:BOOL=ON',
             '-DVTK_USE_SYSTEM_TIFF:BOOL=ON',
             '-DVTK_USE_SYSTEM_ZLIB:BOOL=ON',
         ]
diff --git a/var/spack/repos/builtin/packages/paraview/vtkm-catalyst-pv551.patch b/var/spack/repos/builtin/packages/paraview/vtkm-catalyst-pv551.patch
new file mode 100644
index 0000000000..8c8883740d
--- /dev/null
+++ b/var/spack/repos/builtin/packages/paraview/vtkm-catalyst-pv551.patch
@@ -0,0 +1,510 @@
+# The VTK-m changes are slated for paraview-5.5.x
+#
+# The catalyst changes (the working directory for output) are slated for
+# paraview-5.6.
+# They are API-compatible with paraview-5.5 but not ABI compatible.
+#
+# https://gitlab.kitware.com/vtk/vtk-m/merge_requests/1166
+# https://gitlab.kitware.com/paraview/paraview/merge_requests/2433
+# https://gitlab.kitware.com/paraview/paraview/merge_requests/2436
+#
+--- ParaView-v5.5.0/VTK/ThirdParty/vtkm/vtk-m/CMake/VTKmDetermineVersion.cmake.orig	2018-04-06 22:03:33.000000000 +0200
++++ ParaView-v5.5.0/VTK/ThirdParty/vtkm/vtk-m/CMake/VTKmDetermineVersion.cmake	2018-04-23 12:00:23.708544206 +0200
+@@ -51,6 +51,8 @@
+         ERROR_QUIET
+         OUTPUT_STRIP_TRAILING_WHITESPACE
+         ERROR_STRIP_TRAILING_WHITESPACE)
++    else()
++      set(output)
+     endif()
+   else()
+     set(result 0)
+@@ -75,7 +77,7 @@
+ 
+ # Extracts components from a version string. See determine_version() for usage.
+ function(extract_version_components version_string var_prefix)
+-  string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)[-]*(.*)"
++  string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)[-]*(.*)$"
+     version_matches "${version_string}")
+   if(CMAKE_MATCH_0)
+     set(full ${CMAKE_MATCH_0})
+--- ParaView-v5.5.0/CoProcessing/Catalyst/vtkCPProcessor.cxx.orig	2018-04-06 22:03:33.000000000 +0200
++++ ParaView-v5.5.0/CoProcessing/Catalyst/vtkCPProcessor.cxx	2018-05-11 12:02:26.894772713 +0200
+@@ -38,6 +38,7 @@
+ #include "vtkStringArray.h"
+ 
+ #include <list>
++#include <vtksys/SystemTools.hxx>
+ 
+ struct vtkCPProcessorInternals
+ {
+@@ -47,12 +48,13 @@
+ };
+ 
+ vtkStandardNewMacro(vtkCPProcessor);
+-vtkMultiProcessController* vtkCPProcessor::Controller = NULL;
++vtkMultiProcessController* vtkCPProcessor::Controller = nullptr;
+ //----------------------------------------------------------------------------
+ vtkCPProcessor::vtkCPProcessor()
+ {
+   this->Internal = new vtkCPProcessorInternals;
+-  this->InitializationHelper = NULL;
++  this->InitializationHelper = nullptr;
++  this->WorkingDirectory = nullptr;
+ }
+ 
+ //----------------------------------------------------------------------------
+@@ -61,14 +63,15 @@
+   if (this->Internal)
+   {
+     delete this->Internal;
+-    this->Internal = NULL;
++    this->Internal = nullptr;
+   }
+ 
+   if (this->InitializationHelper)
+   {
+     this->InitializationHelper->Delete();
+-    this->InitializationHelper = NULL;
++    this->InitializationHelper = nullptr;
+   }
++  this->SetWorkingDirectory(nullptr);
+ }
+ 
+ //----------------------------------------------------------------------------
+@@ -95,7 +98,7 @@
+ {
+   if (which < 0 || which >= this->GetNumberOfPipelines())
+   {
+-    return NULL;
++    return nullptr;
+   }
+   int counter = 0;
+   vtkCPProcessorInternals::PipelineListIterator iter = this->Internal->Pipelines.begin();
+@@ -108,7 +111,7 @@
+     counter++;
+     iter++;
+   }
+-  return NULL;
++  return nullptr;
+ }
+ 
+ //----------------------------------------------------------------------------
+@@ -130,17 +133,41 @@
+ }
+ 
+ //----------------------------------------------------------------------------
+-int vtkCPProcessor::Initialize()
++int vtkCPProcessor::Initialize(const char* workingDirectory)
+ {
+-  if (this->InitializationHelper == NULL)
++  if (this->InitializationHelper == nullptr)
+   {
+     this->InitializationHelper = this->NewInitializationHelper();
+   }
++  // make sure the directory exists here so that we only do it once
++  if (workingDirectory)
++  {
++    vtkMultiProcessController* controller = vtkMultiProcessController::GetGlobalController();
++    int success = 1;
++    if (controller == nullptr || controller->GetLocalProcessId() == 0)
++    {
++      success = vtksys::SystemTools::MakeDirectory(workingDirectory) == true ? 1 : 0;
++      if (success == 0)
++      {
++        vtkWarningMacro("Could not make "
++          << workingDirectory << " directory. "
++          << "Results will be generated in current working directory instead.");
++      }
++    }
++    if (controller)
++    {
++      controller->Broadcast(&success, 1, 0);
++    }
++    if (success)
++    {
++      this->SetWorkingDirectory(workingDirectory);
++    }
++  }
+   return 1;
+ }
+ 
+ //----------------------------------------------------------------------------
+-int vtkCPProcessor::Initialize(vtkMPICommunicatorOpaqueComm& comm)
++int vtkCPProcessor::Initialize(vtkMPICommunicatorOpaqueComm& comm, const char* workingDirectory)
+ {
+ #ifdef PARAVIEW_USE_MPI
+   if (vtkCPProcessor::Controller)
+@@ -148,7 +175,7 @@
+     vtkErrorMacro("Can only initialize with a communicator once per process.");
+     return 0;
+   }
+-  if (this->InitializationHelper == NULL)
++  if (this->InitializationHelper == nullptr)
+   {
+     vtkMPICommunicator* communicator = vtkMPICommunicator::New();
+     communicator->InitializeExternal(&comm);
+@@ -157,12 +184,12 @@
+     this->Controller = controller;
+     this->Controller->SetGlobalController(controller);
+     communicator->Delete();
+-    return this->Initialize();
++    return this->Initialize(workingDirectory);
+   }
+   return 1;
+ #else
+   static_cast<void>(&comm); // get rid of variable not used warning
+-  return this->Initialize();
++  return this->Initialize(workingDirectory);
+ #endif
+ }
+ 
+@@ -225,6 +252,13 @@
+       input->GetFieldData()->AddArray(catalystChannel);
+     }
+   }
++
++  std::string originalWorkingDirectory;
++  if (this->WorkingDirectory)
++  {
++    originalWorkingDirectory = vtksys::SystemTools::GetCurrentWorkingDirectory();
++    vtksys::SystemTools::ChangeDirectory(this->WorkingDirectory);
++  }
+   for (vtkCPProcessorInternals::PipelineListIterator iter = this->Internal->Pipelines.begin();
+        iter != this->Internal->Pipelines.end(); iter++)
+   {
+@@ -248,6 +282,10 @@
+       }
+     }
+   }
++  if (originalWorkingDirectory.empty() == false)
++  {
++    vtksys::SystemTools::ChangeDirectory(originalWorkingDirectory);
++  }
+   // we want to reset everything here to make sure that new information
+   // is properly passed in the next time.
+   dataDescription->ResetAll();
+@@ -259,7 +297,7 @@
+ {
+   if (this->Controller)
+   {
+-    this->Controller->SetGlobalController(NULL);
++    this->Controller->SetGlobalController(nullptr);
+     this->Controller->Finalize(1);
+     this->Controller->Delete();
+   }
+--- ParaView-v5.5.0/CoProcessing/Catalyst/vtkCPProcessor.h.orig	2018-04-06 22:03:33.000000000 +0200
++++ ParaView-v5.5.0/CoProcessing/Catalyst/vtkCPProcessor.h	2018-05-11 12:02:26.894772713 +0200
+@@ -76,14 +76,16 @@
+   virtual void RemoveAllPipelines();
+ 
+   /// Initialize the co-processor. Returns 1 if successful and 0
+-  /// otherwise.
+   /// otherwise. If Catalyst is built with MPI then Initialize()
+   /// can also be called with a specific MPI communicator if
+   /// MPI_COMM_WORLD isn't the proper one. Catalyst is initialized
+-  /// to use MPI_COMM_WORLD by default.
+-  virtual int Initialize();
++  /// to use MPI_COMM_WORLD by default. Both methods have an optional
++  /// workingDirectory argument which will set *WorkingDirectory* so
++  /// that files will be put relative to this directory.
++  virtual int Initialize(const char* workingDirectory = nullptr);
+ #ifndef __WRAP__
+-  virtual int Initialize(vtkMPICommunicatorOpaqueComm& comm);
++  virtual int Initialize(
++    vtkMPICommunicatorOpaqueComm& comm, const char* workingDirectory = nullptr);
+ #endif
+ 
+   /// The Catalyst input field data string array name. This array will
+@@ -111,6 +113,13 @@
+   /// implementation an opportunity to clean up, before it is destroyed.
+   virtual int Finalize();
+ 
++  /// Get the current working directory for outputting Catalyst files.
++  /// If not set then Catalyst output files will be relative to the
++  /// current working directory. This will not affect where Catalyst
++  /// looks for Python scripts. *WorkingDirectory* gets set through
++  /// the *Initialize()* methods.
++  vtkGetStringMacro(WorkingDirectory);
++
+ protected:
+   vtkCPProcessor();
+   virtual ~vtkCPProcessor();
+@@ -118,6 +127,11 @@
+   /// Create a new instance of the InitializationHelper.
+   virtual vtkObject* NewInitializationHelper();
+ 
++  /// Set the current working directory for outputting Catalyst files.
++  /// This is a protected method since simulation code adaptors should
++  /// set this through the *Initialize()* methods.
++  vtkSetStringMacro(WorkingDirectory);
++
+ private:
+   vtkCPProcessor(const vtkCPProcessor&) = delete;
+   void operator=(const vtkCPProcessor&) = delete;
+@@ -125,6 +139,7 @@
+   vtkCPProcessorInternals* Internal;
+   vtkObject* InitializationHelper;
+   static vtkMultiProcessController* Controller;
++  char* WorkingDirectory;
+ };
+ 
+ #endif
+--- ParaView-v5.5.0/CoProcessing/Catalyst/vtkCPXMLPWriterPipeline.cxx.orig	2018-04-06 22:03:33.000000000 +0200
++++ ParaView-v5.5.0/CoProcessing/Catalyst/vtkCPXMLPWriterPipeline.cxx	2018-05-11 12:02:26.894772713 +0200
+@@ -31,6 +31,7 @@
+ #include <vtkSmartPointer.h>
+ #include <vtkUnstructuredGrid.h>
+ 
++#include <algorithm>
+ #include <sstream>
+ #include <string>
+ 
+@@ -174,7 +175,7 @@
+ 
+   for (unsigned int i = 0; i < dataDescription->GetNumberOfInputDescriptions(); i++)
+   {
+-    const char* inputName = dataDescription->GetInputDescriptionName(i);
++    std::string inputName = dataDescription->GetInputDescriptionName(i);
+     vtkCPInputDataDescription* idd = dataDescription->GetInputDescription(i);
+     vtkDataObject* grid = idd->GetGrid();
+     if (grid == nullptr)
+@@ -206,6 +207,8 @@
+         vtkSMStringVectorProperty* fileName =
+           vtkSMStringVectorProperty::SafeDownCast(writer->GetProperty("FileName"));
+ 
++        // If we have a / in the channel name we take it out of the filename we're going to write to
++        inputName.erase(std::remove(inputName.begin(), inputName.end(), '/'), inputName.end());
+         std::ostringstream o;
+         if (this->Path.empty() == false)
+         {
+--- ParaView-v5.5.0/Wrapping/Python/paraview/coprocessing.py.orig	2018-04-06 22:03:33.000000000 +0200
++++ ParaView-v5.5.0/Wrapping/Python/paraview/coprocessing.py	2018-05-11 12:02:27.038772408 +0200
+@@ -11,22 +11,12 @@
+ from paraview.vtk.vtkPVVTKExtensionsCore import *
+ import math
+ 
+-# -----------------------------------------------------------------------------
+-def IsInModulo(timestep, frequencyArray):
+-    """
+-    Return True if the given timestep is in one of the provided frequency.
+-    This can be interpreted as follow::
+-
+-        isFM = IsInModulo(timestep, [2,3,7])
+-
+-    is similar to::
++# If the user created a filename in a location that doesn't exist by default we'll
++# make the directory for them. This can be changed though by setting createDirectoriesIfNeeded
++# to False.
++createDirectoriesIfNeeded = True
+ 
+-        isFM = (timestep % 2 == 0) or (timestep % 3 == 0) or (timestep % 7 == 0)
+-    """
+-    for frequency in frequencyArray:
+-        if frequency > 0 and (timestep % frequency == 0):
+-            return True
+-    return False
++# -----------------------------------------------------------------------------
+ 
+ class CoProcessor(object):
+     """Base class for co-processing Pipelines.
+@@ -68,6 +58,9 @@
+         self.__CinemaTracks = {}
+         self.__InitialFrequencies = {}
+         self.__PrintEnsightFormatString = False
++        self.__TimeStepToStartOutputAt=0
++        self.__ForceOutputAtFirstCall=False
++        self.__FirstTimeStepIndex = None
+ 
+     def SetPrintEnsightFormatString(self, enable):
+         """If outputting ExodusII files with the purpose of reading them into
+@@ -87,6 +80,17 @@
+                  "Incorrect argument type: %s, must be a dict" % type(frequencies))
+         self.__InitialFrequencies = frequencies
+ 
++    def SetInitialOutputOptions(self, timeStepToStartOutputAt, forceOutputAtFirstCall):
++        """Set the frequencies at which the pipeline needs to be updated.
++           Typically, this is called by the subclass once it has determined what
++           timesteps co-processing will be needed to be done.
++           frequencies is a map, with key->string name of for the simulation
++           input, and value is a list of frequencies.
++           """
++
++        self.__TimeStepToStartOutputAt=timeStepToStartOutputAt
++        self.__ForceOutputAtFirstCall=forceOutputAtFirstCall
++
+     def EnableLiveVisualization(self, enable, frequency = 1):
+         """Call this method to enable live-visualization. When enabled,
+         DoLiveVisualization() will communicate with ParaView server if possible
+@@ -115,7 +119,7 @@
+         # if this is a time step to do live then all of the inputs
+         # must be made available. note that we want the pipeline built
+         # before we do the actual first live connection.
+-        if self.__EnableLiveVisualization and timestep % self.__LiveVisualizationFrequency == 0 \
++        if self.__EnableLiveVisualization and self.NeedToOutput(timestep, self.__LiveVisualizationFrequency) \
+            and self.__LiveVisualizationLink:
+             if self.__LiveVisualizationLink.Initialize(servermanager.ActiveConnection.Session.GetSessionProxyManager()):
+                 num_inputs = datadescription.GetNumberOfInputDescriptions()
+@@ -132,13 +136,13 @@
+         # hasn't been set up yet). If we don't have live enabled
+         # we know that the output frequencies aren't changed and can
+         # just use the initial frequencies.
+-        if self.__InitialFrequencies or not self.__EnableLiveVisualization:
++        if self.__ForceOutputAtFirstCall or self.__InitialFrequencies or not self.__EnableLiveVisualization:
+             num_inputs = datadescription.GetNumberOfInputDescriptions()
+             for cc in range(num_inputs):
+                 input_name = datadescription.GetInputDescriptionName(cc)
+ 
+                 freqs = self.__InitialFrequencies.get(input_name, [])
+-                if self.__EnableLiveVisualization or ( self and IsInModulo(timestep, freqs) ):
++                if self.__EnableLiveVisualization or ( self and self.IsInModulo(timestep, freqs) ):
+                         datadescription.GetInputDescription(cc).AllFieldsOn()
+                         datadescription.GetInputDescription(cc).GenerateMeshOn()
+         else:
+@@ -149,15 +153,14 @@
+             for writer in self.__WritersList:
+                 frequency = writer.parameters.GetProperty(
+                     "WriteFrequency").GetElement(0)
+-                if (timestep % frequency) == 0 or \
+-                   datadescription.GetForceOutput() == True:
++                if self.NeedToOutput(timestep, frequency) or datadescription.GetForceOutput() == True:
+                     writerinputs = cpstate.locate_simulation_inputs(writer)
+                     for writerinput in writerinputs:
+                         datadescription.GetInputDescriptionByName(writerinput).AllFieldsOn()
+                         datadescription.GetInputDescriptionByName(writerinput).GenerateMeshOn()
+ 
+             for view in self.__ViewsList:
+-                if (view.cpFrequency and timestep % view.cpFrequency == 0) or \
++                if (view.cpFrequency and self.NeedToOutput(timestep, view.cpFrequency)) or \
+                    datadescription.GetForceOutput() == True:
+                     viewinputs = cpstate.locate_simulation_inputs_for_view(view)
+                     for viewinput in viewinputs:
+@@ -192,8 +195,7 @@
+         for writer in self.__WritersList:
+             frequency = writer.parameters.GetProperty(
+                 "WriteFrequency").GetElement(0)
+-            if (timestep % frequency) == 0 or \
+-                    datadescription.GetForceOutput() == True:
++            if self.NeedToOutput(timestep, frequency) or datadescription.GetForceOutput() == True:
+                 fileName = writer.parameters.GetProperty("FileName").GetElement(0)
+                 paddingamount = writer.parameters.GetProperty("PaddingAmount").GetElement(0)
+                 helperName = writer.GetXMLName()
+@@ -203,6 +205,23 @@
+                 else:
+                     ts = str(timestep).rjust(paddingamount, '0')
+                     writer.FileName = fileName.replace("%t", ts)
++                if '/' in writer.FileName and createDirectoriesIfNeeded:
++                    oktowrite = [1.]
++                    import vtk
++                    comm = vtk.vtkMultiProcessController.GetGlobalController()
++                    if comm.GetLocalProcessId() == 0:
++                        import os
++                        newDir = writer.FileName[0:writer.FileName.rfind('/')]
++                        try:
++                            os.makedirs(newDir)
++                        except OSError:
++                            if not os.path.isdir(newDir):
++                                print ("ERROR: Cannot make directory for", writer.FileName, ". No data will be written.")
++                                oktowrite[0] = 0.
++                    comm.Broadcast(oktowrite, 1, 0)
++                    if oktowrite[0] == 0:
++                        # we can't make the directory so no reason to update the pipeline
++                        return
+                 writer.UpdatePipeline(datadescription.GetTime())
+ 
+     def WriteImages(self, datadescription, rescale_lookuptable=False,
+@@ -240,7 +259,7 @@
+ 
+         cinema_dirs = []
+         for view in self.__ViewsList:
+-            if (view.cpFrequency and timestep % view.cpFrequency == 0) or \
++            if (view.cpFrequency and self.NeedToOutput(timestep, view.cpFrequency)) or \
+                datadescription.GetForceOutput() == True:
+                 fname = view.cpFileName
+                 ts = str(timestep).rjust(padding_amount, '0')
+@@ -267,6 +286,24 @@
+                     if dirname:
+                         cinema_dirs.append(dirname)
+                 else:
++                    if '/' in fname and createDirectoriesIfNeeded:
++                        oktowrite = [1.]
++                        import vtk
++                        comm = vtk.vtkMultiProcessController.GetGlobalController()
++                        if comm.GetLocalProcessId() == 0:
++                            import os
++                            newDir = fname[0:fname.rfind('/')]
++                            try:
++                                os.makedirs(newDir)
++                            except OSError:
++                                if not os.path.isdir(newDir):
++                                    print ("ERROR: Cannot make directory for", fname, ". No image will be output.")
++                                    oktowrite[0] = 0.
++                        comm.Broadcast(oktowrite, 1, 0)
++                        if oktowrite[0] == 0:
++                            # we can't make the directory so no reason to update the pipeline
++                            return
++
+                     if image_quality is None and fname.endswith('png'):
+                         # for png quality = 0 means no compression. compression can be a potentially
+                         # very costly serial operation on process 0
+@@ -307,7 +344,7 @@
+ 
+ 
+         timeStep = datadescription.GetTimeStep()
+-        if self.__EnableLiveVisualization and timeStep % self.__LiveVisualizationFrequency == 0:
++        if self.__EnableLiveVisualization and self.NeedToOutput(timeStep, self.__LiveVisualizationFrequency):
+             if not self.__LiveVisualizationLink.Initialize(servermanager.ActiveConnection.Session.GetSessionProxyManager()):
+                 return
+ 
+@@ -412,7 +449,7 @@
+         """
+         controller = servermanager.ParaViewPipelineController()
+         # assume that a client only proxy with the same name as a writer
+-        # is available in "insitu_writer_paramters"
++        # is available in "insitu_writer_parameters"
+ 
+         # Since coprocessor sometimes pass writer as a custom object and not
+         # a proxy, we need to handle that. Just creating any arbitrary writer
+@@ -666,3 +703,42 @@
+         #restore what we showed
+         pv_introspect.restore_visibility(pxystate)
+         return os.path.basename(vfname)
++
++    def IsInModulo(self, timestep, frequencies):
++        """
++        Return True if the given timestep is in one of the provided frequency.
++        This can be interpreted as follow::
++
++        isFM = IsInModulo(timestep-timeStepToStartOutputAt, [2,3,7])
++
++        is similar to::
++
++        isFM = (timestep-timeStepToStartOutputAt % 2 == 0) or (timestep-timeStepToStartOutputAt % 3 == 0) or (timestep-timeStepToStartOutputAt % 7 == 0)
++
++        The timeStepToStartOutputAt is the first timestep that will potentially be output.
++        """
++        if timestep < self.__TimeStepToStartOutputAt and not self.__ForceOutputAtFirstCall:
++            return False
++        for frequency in frequencies:
++            if frequency > 0 and self.NeedToOutput(timestep, frequency):
++                return True
++
++        return False
++
++
++    def NeedToOutput(self, timestep, frequency):
++        """
++        Return True if we need to output based on the input timestep and frequency. Checks based
++        __FirstTimeStepIndex, __FirstTimeStepIndex, __ForceOutputAtFirstCall and __TimeStepToStartOutputAt
++        member variables.
++        """
++        if self.__FirstTimeStepIndex == None:
++            self.__FirstTimeStepIndex = timestep
++
++        if self.__ForceOutputAtFirstCall and self.__FirstTimeStepIndex == timestep:
++            return True
++
++        if self.__TimeStepToStartOutputAt <= timestep and (timestep-self.__TimeStepToStartOutputAt) % frequency == 0:
++            return True
++
++        return False
-- 
GitLab