Tutorial 1: 2D flow past a cylinder

Outline

  1. Loading the data

  2. Computing a metric and adding geometries

  3. Creating a new mesh

  4. Exporting the data

  5. Optional: performing an SVD

  6. Using the DataLoader

  7. More about geometry objects

  8. References and further material

In this introductory tutorial, we will use \(S^3\) for a flow past a cylinder at \(Re = 100\). You can find the numerical setup in the flow_data repository. This tutorial uses OpenFOAM as CFD tool to generate the data, however, \(S^3\) works for other data formats as well.

In general, \(S^3\) expects three things:

  1. a point cloud representing the coordinates of the cell centers

  2. a value (metric) associated with each cell center, indicating the importance of that cell

  3. a stopping criterion, e.g. the max. number of cells or a min. threshold for approximating the metric

1. Loading the data

First we load and inspect our simulation data. Therefore, \(S^3\) implicitly uses the flowTorch package, which provides a variety of APIs for loading CFD data. Depending on your data structure, you can use the best-suited dataloader within flowTorch.data directly.

[7]:
import sys
import torch as pt

from os import environ
from os.path import join

environ["sparseSpatialSampling"] = join("..", "..", "..")
sys.path.insert(0, environ["sparseSpatialSampling"])

from sparseSpatialSampling.export import ExportData
from sparseSpatialSampling.geometry import CubeGeometry, SphereGeometry
from sparseSpatialSampling.sparse_spatial_sampling import SparseSpatialSampling
from sparseSpatialSampling.utils import load_foam_data, export_openfoam_fields
[8]:
# define load paths to the CFD data, assuming they are in the top-level of the repository
load_path = join("..", "..", "..", "flowTorch_Workshop_2025", "cylinder_2D_Re100")

# here we use the approximated metric field as stopping criterion
# how much of the metric within the original grid should be captured at least
min_metric = 0.75

# define the path to where we want to save the results and the name of the file
save_path = join("..","..", "..", "run", "tutorials", "tutorial_1")
save_name = "cylinder2D_metric_{:.2f}".format(min_metric)
[9]:
# now we can load the data. Since we used OpenFOAM, we can use the load_foam_data function provided by S^3.
# Otherwise, we have to use flowtorch dataloaders directly, refer to the flowtorch documentation

# define boundaries of the masked domain for the cylinder, here we want to load the full domain
bounds = [[0, 0], [2.2, 0.41]]  # [[xmin, ymin], [xmax, ymax]]

# load the CFD data, we want to compute the metric based on the velocity in the quasi-steady state, so omit the first 4 seconds
field, coord, _, write_times = load_foam_data(load_path, bounds, field_name="U", t_start=4, scalar=False)

# display some information about the grid
print(f"Grid size: {field.shape[0]} cells.")
print(f"Found {field.shape[-1]} snapshots.")
[2026-02-19 15:35:08] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2026-02-19 15:35:08] INFO     Loading precomputed cell centers and volumes from processor1/constant
Grid size: 9800 cells.
Found 301 snapshots.

we can visualize the flow field and the grid in paraview:

U_field_t10s_cell_centered.png grid.png

Since the grid is already quite coarse, we don’t expect a significant data reduction when using \(S^3\).

2. Computing a metric and adding geometries

[10]:
# now we compute a metric. in this case, we just use the temporal mean of the abs. velocity vector
metric = pt.mean(field.abs().sum(1), 1)

# create geometry objects for the domain and the cylinder, we will learn in section 7 more about these geometry objects
# we don't want to refine the domain boundaries, so keep all the optional arguments as default
domain = CubeGeometry("domain", True, bounds[0], bounds[1])

# define the properties of the cylinder
position = [0.2, 0.2]
radius = 0.05

# we want to refine the cylinder surface, so we set refine=True
# by default this refines the cylinder with the max. level encountered at the geometry. However, we can also increase the
# resolution of the cylinder by passing a min_refinement_level
geometry = SphereGeometry("cylinder", False, position, radius, refine=True, min_refinement_level=9)

# create a S^3 instance, since this is a quite small case, we set the number of CPUs (n_jobs) to 4.
# Further, we want to stop the refinement process based on the approximation of the metric, so we have to pass the threshold min_metric as well
s_cube = SparseSpatialSampling(coord, metric, [domain, geometry], save_path, save_name, "cylinder2D", min_metric=min_metric, n_jobs=4)
[2026-02-19 15:35:09] INFO     Selecting min. approximation of the metric as stopping criterion.
[2026-02-19 15:35:09] INFO
        Selected settings:
                pre_select           :  False
                n_jobs               :  4
                max_delta_level      :  False
                geometry             :  ['domain', 'cylinder']
                min_metric           :  0.75
                min_level            :  5
                cells_per_iter_start :  9
                cells_per_iter_end   :  9
                cells_per_iter       :  9
                cells_per_iter_last  :  1000000000.0
                reach_at_least       :  0.75
                n_dimensions         :  2
                n_cells_orig         :  9800
                relTol               :  0.001

3. Creating a new mesh

Now we are ready to create a new grid using \(S^3\). This can be achieved by simply calling the execute_grid_generation() method:

[11]:
# execute the mesh generation with S^3
s_cube.execute_grid_generation()
[2026-02-19 15:35:10] INFO     Starting grid generation.
[2026-02-19 15:35:10] INFO     Starting uniform refinement.
        Starting iteration no. 0, N_cells = 1
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
        Starting iteration no. 1, N_cells = 4
        Starting iteration no. 2, N_cells = 8
        Starting iteration no. 3, N_cells = 16
        Starting iteration no. 4, N_cells = 64
[2026-02-19 15:35:17] INFO     Finished uniform refinement.
[2026-02-19 15:35:17] INFO     Starting metric-based refinement.
        Starting iteration no. 0, captured metric: 13.55 %, N_cells = 192
        Starting iteration no. 1, captured metric: 14.41 %, N_cells = 217
        Starting iteration no. 2, captured metric: 14.97 %, N_cells = 244
        Starting iteration no. 3, captured metric: 15.32 %, N_cells = 271
        Starting iteration no. 4, captured metric: 16.18 %, N_cells = 298
        Starting iteration no. 5, captured metric: 16.33 %, N_cells = 325
        Starting iteration no. 6, captured metric: 16.48 %, N_cells = 352
        Starting iteration no. 7, captured metric: 16.68 %, N_cells = 379
        Starting iteration no. 8, captured metric: 16.82 %, N_cells = 406
        Starting iteration no. 9, captured metric: 17.43 %, N_cells = 433
        Starting iteration no. 10, captured metric: 18.54 %, N_cells = 459
        Starting iteration no. 11, captured metric: 19.3 %, N_cells = 486
        Starting iteration no. 12, captured metric: 19.89 %, N_cells = 513
        Starting iteration no. 13, captured metric: 20.48 %, N_cells = 540
        Starting iteration no. 14, captured metric: 21.05 %, N_cells = 567
        Starting iteration no. 15, captured metric: 21.75 %, N_cells = 594
        Starting iteration no. 16, captured metric: 22.61 %, N_cells = 621
        Starting iteration no. 17, captured metric: 23.45 %, N_cells = 647
        Starting iteration no. 18, captured metric: 24.08 %, N_cells = 674
        Starting iteration no. 19, captured metric: 24.72 %, N_cells = 701
        Starting iteration no. 20, captured metric: 25.02 %, N_cells = 728
        Starting iteration no. 21, captured metric: 25.69 %, N_cells = 755
        Starting iteration no. 22, captured metric: 26.15 %, N_cells = 782
        Starting iteration no. 23, captured metric: 26.83 %, N_cells = 809
        Starting iteration no. 24, captured metric: 27.34 %, N_cells = 836
        Starting iteration no. 25, captured metric: 27.91 %, N_cells = 863
        Starting iteration no. 26, captured metric: 28.29 %, N_cells = 890
        Starting iteration no. 27, captured metric: 28.83 %, N_cells = 917
        Starting iteration no. 28, captured metric: 29.22 %, N_cells = 944
        Starting iteration no. 29, captured metric: 29.59 %, N_cells = 971
        Starting iteration no. 30, captured metric: 30.03 %, N_cells = 998
        Starting iteration no. 31, captured metric: 30.51 %, N_cells = 1025
        Starting iteration no. 32, captured metric: 30.89 %, N_cells = 1052
        Starting iteration no. 33, captured metric: 31.09 %, N_cells = 1079
        Starting iteration no. 34, captured metric: 31.47 %, N_cells = 1106
        Starting iteration no. 35, captured metric: 31.61 %, N_cells = 1133
        Starting iteration no. 36, captured metric: 31.96 %, N_cells = 1160
        Starting iteration no. 37, captured metric: 32.29 %, N_cells = 1187
        Starting iteration no. 38, captured metric: 32.52 %, N_cells = 1214
        Starting iteration no. 39, captured metric: 32.75 %, N_cells = 1241
        Starting iteration no. 40, captured metric: 32.93 %, N_cells = 1268
        Starting iteration no. 41, captured metric: 32.97 %, N_cells = 1295
        Starting iteration no. 42, captured metric: 33.17 %, N_cells = 1322
        Starting iteration no. 43, captured metric: 33.43 %, N_cells = 1349
        Starting iteration no. 44, captured metric: 33.53 %, N_cells = 1376
        Starting iteration no. 45, captured metric: 33.89 %, N_cells = 1403
        Starting iteration no. 46, captured metric: 34.09 %, N_cells = 1430
        Starting iteration no. 47, captured metric: 34.18 %, N_cells = 1457
        Starting iteration no. 48, captured metric: 34.32 %, N_cells = 1484
        Starting iteration no. 49, captured metric: 34.42 %, N_cells = 1511
        Starting iteration no. 50, captured metric: 34.71 %, N_cells = 1538
        Starting iteration no. 51, captured metric: 34.92 %, N_cells = 1565
        Starting iteration no. 52, captured metric: 35.2 %, N_cells = 1592
        Starting iteration no. 53, captured metric: 35.44 %, N_cells = 1619
        Starting iteration no. 54, captured metric: 35.78 %, N_cells = 1646
        Starting iteration no. 55, captured metric: 36.04 %, N_cells = 1673
        Starting iteration no. 56, captured metric: 36.25 %, N_cells = 1700
        Starting iteration no. 57, captured metric: 36.57 %, N_cells = 1727
        Starting iteration no. 58, captured metric: 36.8 %, N_cells = 1754
        Starting iteration no. 59, captured metric: 37.12 %, N_cells = 1781
        Starting iteration no. 60, captured metric: 37.6 %, N_cells = 1808
        Starting iteration no. 61, captured metric: 37.94 %, N_cells = 1835
        Starting iteration no. 62, captured metric: 38.15 %, N_cells = 1862
        Starting iteration no. 63, captured metric: 38.39 %, N_cells = 1889
        Starting iteration no. 64, captured metric: 38.76 %, N_cells = 1916
        Starting iteration no. 65, captured metric: 39.09 %, N_cells = 1943
        Starting iteration no. 66, captured metric: 39.33 %, N_cells = 1970
        Starting iteration no. 67, captured metric: 39.57 %, N_cells = 1997
        Starting iteration no. 68, captured metric: 39.98 %, N_cells = 2024
        Starting iteration no. 69, captured metric: 40.17 %, N_cells = 2051
        Starting iteration no. 70, captured metric: 40.45 %, N_cells = 2078
        Starting iteration no. 71, captured metric: 40.78 %, N_cells = 2105
        Starting iteration no. 72, captured metric: 41.05 %, N_cells = 2132
        Starting iteration no. 73, captured metric: 41.33 %, N_cells = 2158
        Starting iteration no. 74, captured metric: 41.68 %, N_cells = 2185
        Starting iteration no. 75, captured metric: 42.06 %, N_cells = 2212
        Starting iteration no. 76, captured metric: 42.42 %, N_cells = 2239
        Starting iteration no. 77, captured metric: 42.75 %, N_cells = 2266
        Starting iteration no. 78, captured metric: 43.09 %, N_cells = 2293
        Starting iteration no. 79, captured metric: 43.43 %, N_cells = 2320
        Starting iteration no. 80, captured metric: 43.77 %, N_cells = 2347
        Starting iteration no. 81, captured metric: 44.15 %, N_cells = 2374
        Starting iteration no. 82, captured metric: 44.52 %, N_cells = 2401
        Starting iteration no. 83, captured metric: 44.9 %, N_cells = 2428
        Starting iteration no. 84, captured metric: 45.29 %, N_cells = 2455
        Starting iteration no. 85, captured metric: 45.6 %, N_cells = 2482
        Starting iteration no. 86, captured metric: 45.96 %, N_cells = 2509
        Starting iteration no. 87, captured metric: 46.34 %, N_cells = 2536
        Starting iteration no. 88, captured metric: 46.66 %, N_cells = 2563
        Starting iteration no. 89, captured metric: 47.02 %, N_cells = 2590
        Starting iteration no. 90, captured metric: 47.45 %, N_cells = 2617
        Starting iteration no. 91, captured metric: 47.8 %, N_cells = 2644
        Starting iteration no. 92, captured metric: 48.15 %, N_cells = 2671
        Starting iteration no. 93, captured metric: 48.42 %, N_cells = 2698
        Starting iteration no. 94, captured metric: 48.82 %, N_cells = 2725
        Starting iteration no. 95, captured metric: 49.09 %, N_cells = 2752
        Starting iteration no. 96, captured metric: 49.29 %, N_cells = 2779
        Starting iteration no. 97, captured metric: 49.49 %, N_cells = 2806
        Starting iteration no. 98, captured metric: 49.68 %, N_cells = 2831
        Starting iteration no. 99, captured metric: 49.95 %, N_cells = 2858
        Starting iteration no. 100, captured metric: 50.12 %, N_cells = 2885
        Starting iteration no. 101, captured metric: 50.28 %, N_cells = 2912
        Starting iteration no. 102, captured metric: 50.43 %, N_cells = 2939
        Starting iteration no. 103, captured metric: 50.69 %, N_cells = 2966
        Starting iteration no. 104, captured metric: 50.93 %, N_cells = 2993
        Starting iteration no. 105, captured metric: 51.12 %, N_cells = 3020
        Starting iteration no. 106, captured metric: 51.35 %, N_cells = 3047
        Starting iteration no. 107, captured metric: 51.55 %, N_cells = 3074
        Starting iteration no. 108, captured metric: 51.8 %, N_cells = 3101
        Starting iteration no. 109, captured metric: 51.98 %, N_cells = 3128
        Starting iteration no. 110, captured metric: 52.12 %, N_cells = 3155
        Starting iteration no. 111, captured metric: 52.28 %, N_cells = 3182
        Starting iteration no. 112, captured metric: 52.47 %, N_cells = 3209
        Starting iteration no. 113, captured metric: 52.86 %, N_cells = 3236
        Starting iteration no. 114, captured metric: 53.15 %, N_cells = 3263
        Starting iteration no. 115, captured metric: 53.39 %, N_cells = 3290
        Starting iteration no. 116, captured metric: 53.64 %, N_cells = 3317
        Starting iteration no. 117, captured metric: 53.77 %, N_cells = 3344
        Starting iteration no. 118, captured metric: 54.02 %, N_cells = 3371
        Starting iteration no. 119, captured metric: 54.34 %, N_cells = 3398
        Starting iteration no. 120, captured metric: 54.61 %, N_cells = 3425
        Starting iteration no. 121, captured metric: 54.82 %, N_cells = 3452
        Starting iteration no. 122, captured metric: 55.09 %, N_cells = 3479
        Starting iteration no. 123, captured metric: 55.32 %, N_cells = 3504
        Starting iteration no. 124, captured metric: 55.71 %, N_cells = 3531
        Starting iteration no. 125, captured metric: 55.92 %, N_cells = 3558
        Starting iteration no. 126, captured metric: 56.21 %, N_cells = 3585
        Starting iteration no. 127, captured metric: 56.46 %, N_cells = 3612
[2026-02-19 15:35:24] INFO     Finished metric-based refinement.
[2026-02-19 15:35:24] INFO     Starting geometry refinement.
[2026-02-19 15:35:24] INFO     Starting refining geometry cylinder.
[2026-02-19 15:35:24] INFO     Found a minimum cell level of 6. Target level is 9.
                                                                        Refining level 7 / 9.
                                                                        Refining level 8 / 9.
                                                                        Refining level 9 / 9.
[2026-02-19 15:35:24] INFO     Finished geometry refinement.
[2026-02-19 15:35:24] INFO     Starting renumbering final mesh.
[2026-02-19 15:35:28] INFO     Finished refinement in 18.3238 s
                                                                (128 iterations).
                                                                Time for uniform refinement: 7.9080 s
                                                                Time for metric-based refinement: 6.6841 s
                                                                Time for geometry refinement: 0.3106 s
                                                                Time for renumbering the final mesh: 3.4060 s

                                    Number of cells: 3734
                                    Minimum ref. level: 6
                                    Maximum ref. level: 9
                                    Captured metric of original grid: 56.56 %

As we notice from the output, we don’t reach the specified metric of min_metric = 0.75, but the refinement process stops at around \(56.56\%\). The reason for that is another stopping criterion, which aborts the refinement process based on the relative improvement between two consecutive iterations. This parameter relTol is by default set to relTol = 0.001. We can disable it either by setting it to zero, or by setting the parameter reach_at_least to one. reach_at_least sets the threshold when to activate the relTol stopping criterion and defaults to reach_at_least=0.75, meaning that we have to approximate the metric by \(75\%\) before the relTol stopping criterion is activated. However, this will increase the required runtime:

[12]:
# omit the relTol stopping criterion, change cell type if you want to test this
# s_cube = SparseSpatialSampling(coord, metric, [domain, geometry], save_path, save_name, "cylinder2D", min_metric=min_metric, n_jobs=4,
#                                relTol=0)
# s_cube.execute_grid_generation()

Output files

\(S^3\) will create two output files:

  1. mesh_info_<save_name>.pt

  2. s_cube_<save_name>.pt

The first file contains statistics about the mesh generation and the final mesh while the latter contains the s_cube object itself. This is useful in case we want to carry out the grid generation process independently from the export of the flow fields. In this case, instead of executing the grid generation again, we can simply load the s_cube object as:

s_cube = pt.load("s_cube_<save_name>.pt", weights_only=False)

and then continue with the next section.

4. Exporting the data

We can now interpolate the original flow fields from CFD onto the new grid generated by \(S^3\) and export it to HDF5. Therefore, \(S^3\) provides the ExportData class which takes care of the interpolation and export. The procedure is shown in the following.

[13]:
# create export instance, export all fields into the same HFD5 file and create single XDMF from it
export = ExportData(s_cube)

# by default, solutions are only exported at the cells center. We can set the parameter interpolate_at_vertices to True to
# export the solution at the cell vertices as well
export = ExportData(s_cube, interpolate_at_vertices=True)

# export all available write times for fields available
export_openfoam_fields(export, load_path, bounds)
[2026-02-19 15:35:28] WARNING  Argument ``write_times`` is ``None``. Make sure to set the ``write_times`` before calling the ``export()`` method.
[2026-02-19 15:35:28] WARNING  Argument ``write_times`` is ``None``. Make sure to set the ``write_times`` before calling the ``export()`` method.
[2026-02-19 15:35:28] INFO     Exporting batch 1 / 1
[2026-02-19 15:35:28] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2026-02-19 15:35:28] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2026-02-19 15:35:30] INFO     Initializing KNN and computing interpolation weights.
[2026-02-19 15:35:30] INFO     Starting interpolation and export of field U.
[2026-02-19 15:35:31] INFO     Writing HDF5 file for field U.
[2026-02-19 15:35:31] INFO     Writing XDMF file for file cylinder2D_metric_0.75.h5
[2026-02-19 15:35:32] INFO     Finished export of field U in 3.649s.
[2026-02-19 15:35:32] INFO     Exporting batch 1 / 1
[2026-02-19 15:35:32] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2026-02-19 15:35:32] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2026-02-19 15:35:32] INFO     Starting interpolation and export of field p.
[2026-02-19 15:35:33] INFO     Writing XDMF file for file cylinder2D_metric_0.75.h5
[2026-02-19 15:35:33] INFO     Finished export of field p in 1.766s.
[14]:
# Note: executing this cell will overwrite the files containing the exported fields in the previous cell
# we have to instantiate another export object since otherwise this leads to issues with HDF5
export = ExportData(s_cube)

# alternatively, we can export data available at only certain time steps, but we need to assure that we don't get any round-off issues.
# so we specify the precision first
export.write_times = ["{:.3f}".format(i.item()) for i in pt.arange(4, 10, 0.001)]

# now export the velocity field, since we used that to compute our metric, we don't need to re-load it
export.export(coord, field, "U")

# now we can load and export the pressure field
field, _, _, _ = load_foam_data(load_path, bounds, t_start=4)

# export() expects a field of [N_cells, N_dimensions, N_snapshots], so for a scalar field we have to add a dimension
export.export(coord, field.unsqueeze(1), "p")
[2026-02-19 15:35:33] WARNING  Argument ``write_times`` is ``None``. Make sure to set the ``write_times`` before calling the ``export()`` method.
[2026-02-19 15:35:33] INFO     Initializing KNN and computing interpolation weights.
[2026-02-19 15:35:34] INFO     Starting interpolation and export of field U.
[2026-02-19 15:35:34] INFO     Writing HDF5 file for field U.
[2026-02-19 15:35:34] INFO     Writing XDMF file for file cylinder2D_metric_0.75.h5
[2026-02-19 15:35:34] INFO     Finished export of field U in 0.972s.
[2026-02-19 15:35:34] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2026-02-19 15:35:34] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2026-02-19 15:35:35] INFO     Starting interpolation and export of field p.
[2026-02-19 15:35:35] INFO     Writing XDMF file for file cylinder2D_metric_0.75.h5
[2026-02-19 15:35:35] INFO     Finished export of field p in 0.878s.

This creates two new files:

  1. <save_name>.h5 which contains all the exported flow fields along with the metric and cell levels

  2. <save_name>.xdmf which is a markdown file for loading the results into paraview

we can now take a look at the grid, metric and flow field by loading the cylinder2D_metric_0.75.xdmf into paraview. When opening the XDMF file in Paraview, it is important to select the ``Xdmf3ReaderS``, all other readers will lead to an incorrect assignment of the values to the respective cells. Once we have loaded the file, we can visualize the different quantities:

metric_metric_0.75.png grid_metric_0.75.png U_field_10s_cell_centered.png

The metric and cells levels are saved in the first time step folder.

5. Optional: performing an SVD

We can further perform an SVD for analysing flow patterns. The results of the SVD are saved in separate HDF5and XDMFfiles. It is important to note that the SVD is computed separately for each specified field.

Also, the data matrix is weighted with the square-root of the cell volume when computing the SVD using the utility write_svd_s_cube_to_file(). This is an important step when comparing the results of the SVD for different grid topologies, i.e. the results on the original grid with the results of \(S^3\). The weighing has to be reversed after computing the SVD.

The general procedure is:

data_matrix *= cell_volumes.sqrt()
svd = SVD(data_matrix, rank=rank)

# reverse the weighting for the modes
svd.U /= cell_area.sqrt()

which has to be applied when computing the corresponding SVD on the original data from CFD.

[15]:
from sparseSpatialSampling.utils import write_svd_s_cube_to_file

# compute the SVD on grid generated by S^3 separately for each field and export the results to HDF5 & XDMF
# make sure to skip the initial transient phase
write_svd_s_cube_to_file(["p", "U"], save_path, save_name, False, 50, rank=int(1e5), t_start=4)
[2026-02-19 15:35:35] INFO     Performing SVD for field p.
[2026-02-19 15:35:36] INFO     Writing XDMF file for file cylinder2D_metric_0.75_p_svd.h5
[2026-02-19 15:35:36] INFO     Performing SVD for field U.
[2026-02-19 15:35:36] INFO     Writing XDMF file for file cylinder2D_metric_0.75_U_svd.h5

6. Using the Dataloader (flowtorch.SCUBEDataloader)

We can load flow fields or other results from files generated by \(S^3\) using the Dataloaderclass:

[16]:
from flowtorch.data import SCUBEDataloader
from sparseSpatialSampling.data import Dataloader

# we can use the dataloder to load and post-process Scube data
dataloader = Dataloader(save_path, f"{save_name}.h5")

# alternatively, we can use the SCUBEDataloader within flowTorch
dataloader = SCUBEDataloader(save_path, f"{save_name}.h5")

# we can now use the dataloader for post-processing etc.
print(f"Number of cells: {dataloader.vertices.size(0)}")
print(f"Field names at t = {dataloader.write_times[0]}s: {dataloader.field_names[dataloader.write_times[0]]}")
Number of cells: 3734
Field names at t = 4.000s: ['U', 'p']

Note that the results from SVD can’t be loaded with the Dataloader. For now, you can find an example on how to do that in:

post_processing/compare_svd_results_cylinder3D_Re3900.py

We will present a more detailed explanation on how to use the \(S^3\) Dataloader and Datawriter classes in tutorial 5.

7. More about geometry objects

So far, we have used geometry objects without an explanation about what they are doing. This section will give a brief overview about geometry objects, their purpose and limitations.

Geometry objects can be used to mask out an area in the flow field. Since \(S^3\) creates the grid solely based on the metric field, it also interpolates the metric into regions where (in CFD) a geometry is present. In case there is a gradient within the metric field across the geometry, e.g. between the lower and upper side of an airfoil (compare tutorial 2), this will generate a large number of cells ‘inside’ the geometry. Geometry objects can be used to avoid this behavior by masking out these regions, so that there is no grid generated.

By default, :math:`S^3` need at least a single geometry object which represents the domain. All of the available geometry objects can be used either as domain (keep_inside=True) or as geometry (keep_inside=False). When used as domain, this means all cells outside of this geometry object will be removed. This is required since \(S^3\) starts the grid generation by creating a single cell based on the main dimension of the flow field. If the numerical domain is not quadratic (2D) or cubic (3D), this will lead to cells being generated outside the actual (numerical) domain. The domain geometry object avoids this behavior. When setting keep_inside=False this means this object is regarded as geometry and all cells inside this geometry are removed.

Currently, we can only pass exactly one geometry object representing the domain to \(S^3\). For geometry objects representing geometries or areas to mask out in the flow field, there is no limitation with respect to the number of objects passed to \(S^3\).

We can check out the available geometries for masking using the list_geometry function:

[17]:
# we can check out the available geometry object classes currently implemented in S^3
from sparseSpatialSampling.sparse_spatial_sampling import list_geometries
list_geometries()
[2026-02-19 15:35:36] INFO
        Available geometry objects:
        ---------------------------
                - CubeGeometry          : rectangles (2D) or cubes (3D)
                - CylinderGeometry3D    : cylinders, conical objects and cones (3D)
                - GeometryCoordinates2D : 2D coordinates for geometries
                - GeometrySTL3D         : usage of STL files for geometries (3D)
                - PrismGeometry3D       : prisms (3D)
                - PyramidGeometry3D     : square pyramids (3D)
                - SphereGeometry        : circles (2D) or spheres (3D)
                - TetrahedronGeometry3D : tetrahedrons (3D)
                - TriangleGeometry      : triangles (2D)

        For a more detailed description check out the documentation.

All of these geometry objects can be used either as domains or geometries, but so far we only used the CubeGeometry as domain since all of our CFD domains had a rectangular shape.

To illustrate the usage of other geometry objects, we will now use the same simulation (flow past a cylinder), but replace the cylinder with a triangle. Further, we add some polygons to mask out specific areas within the flow field.

[18]:
from sparseSpatialSampling.geometry import TriangleGeometry, GeometryCoordinates2D

# initialize everything
field, coord, _, write_times = load_foam_data(load_path, bounds, field_name="U", t_start=4, scalar=False)
save_name = "cylinder2D_withMore_geometries"

# create some geometry objects
domain = CubeGeometry("domain", True, bounds[0], bounds[1])
t1 = TriangleGeometry("front triangle", False, [(0.1, 0.2), (0.25, 0.1), (0.25, 0.3)], refine=True, min_refinement_level=9)

# make up some coordinates for the two polygons
coord_s = [
        (0.48, 0.29),
        (0.4, 0.29),
        (0.4, 0.19),
        (0.46, 0.19),
        (0.46, 0.13),
        (0.42, 0.13),
        (0.42, 0.15),
        (0.4, 0.15),
        (0.4, 0.11),
        (0.48, 0.11),
        (0.48, 0.21),
        (0.42, 0.21),
        (0.42, 0.27),
        (0.46, 0.27),
        (0.46, 0.24),
        (0.48, 0.24),
        (0.48, 0.29)
    ]

coord_3 = [
        (0.5, 0.24),
        (0.5, 0.27),
        (0.52, 0.29),
        (0.56, 0.29),
        (0.58, 0.27),
        (0.58, 0.22),
        (0.56, 0.20),
        (0.58, 0.18),
        (0.58, 0.13),
        (0.56, 0.11),
        (0.52, 0.11),
        (0.5, 0.13),
        (0.5, 0.16),
        (0.52, 0.16),
        (0.52, 0.14),
        (0.53, 0.13),
        (0.55, 0.13),
        (0.56, 0.14),
        (0.56, 0.17),
        (0.55, 0.18),
        (0.53, 0.2),
        (0.56, 0.23),
        (0.56, 0.258),
        (0.55, 0.268),
        (0.53, 0.268),
        (0.52, 0.258),
        (0.52, 0.24),
        (0.5, 0.24)
    ]
[2026-02-19 15:35:36] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2026-02-19 15:35:36] INFO     Loading precomputed cell centers and volumes from processor1/constant

It is important to note that the coordinates for the GeometryCoordinates2D class have to be in 2D. Further they have to form a closed line. For 3D coordinates, the GeometrySTL3D can be used. Therefore, the coordinates have to be converted into an STL file, e.g., by using PyVista.

[19]:
# initialize all geometries
g1 = GeometryCoordinates2D("S", False, coord_s, refine=True, min_refinement_level=10)
g2 = GeometryCoordinates2D("3", False, coord_3, refine=True, min_refinement_level=10)
t2 = TriangleGeometry("rear triangle", False, [(0.88, 0.2), (0.73, 0.1), (0.73, 0.3)],
                                refine=True, min_refinement_level=9)

# create a S^3 instance and execute
s_cube = SparseSpatialSampling(coord, pt.mean(field.abs().sum(1), 1), [domain, t1, g1, g2, t2], save_path, save_name,
                               "cylinder2D", min_metric=min_metric, n_jobs=4)
s_cube.execute_grid_generation()
[2026-02-19 15:35:37] INFO     Selecting min. approximation of the metric as stopping criterion.
[2026-02-19 15:35:37] INFO
        Selected settings:
                pre_select           :  False
                n_jobs               :  4
                max_delta_level      :  False
                geometry             :  ['domain', 'front triangle', 'S', '3', 'rear triangle']
                min_metric           :  0.75
                min_level            :  5
                cells_per_iter_start :  9
                cells_per_iter_end   :  9
                cells_per_iter       :  9
                cells_per_iter_last  :  1000000000.0
                reach_at_least       :  0.75
                n_dimensions         :  2
                n_cells_orig         :  9800
                relTol               :  0.001
[2026-02-19 15:35:37] INFO     Starting grid generation.
[2026-02-19 15:35:37] INFO     Starting uniform refinement.
        Starting iteration no. 0, N_cells = 1
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
        Starting iteration no. 1, N_cells = 4
        Starting iteration no. 2, N_cells = 8
        Starting iteration no. 3, N_cells = 16
        Starting iteration no. 4, N_cells = 64
[2026-02-19 15:35:43] INFO     Finished uniform refinement.
[2026-02-19 15:35:43] INFO     Starting metric-based refinement.
        Starting iteration no. 0, captured metric: 13.55 %, N_cells = 192
        Starting iteration no. 1, captured metric: 14.41 %, N_cells = 217
        Starting iteration no. 2, captured metric: 14.97 %, N_cells = 244
        Starting iteration no. 3, captured metric: 15.32 %, N_cells = 271
        Starting iteration no. 4, captured metric: 16.18 %, N_cells = 297
        Starting iteration no. 5, captured metric: 16.33 %, N_cells = 323
        Starting iteration no. 6, captured metric: 16.48 %, N_cells = 350
        Starting iteration no. 7, captured metric: 16.68 %, N_cells = 377
        Starting iteration no. 8, captured metric: 16.82 %, N_cells = 404
        Starting iteration no. 9, captured metric: 17.43 %, N_cells = 431
        Starting iteration no. 10, captured metric: 18.52 %, N_cells = 453
        Starting iteration no. 11, captured metric: 19.27 %, N_cells = 480
        Starting iteration no. 12, captured metric: 19.87 %, N_cells = 507
        Starting iteration no. 13, captured metric: 20.45 %, N_cells = 534
        Starting iteration no. 14, captured metric: 21.02 %, N_cells = 559
        Starting iteration no. 15, captured metric: 21.72 %, N_cells = 586
        Starting iteration no. 16, captured metric: 22.58 %, N_cells = 613
        Starting iteration no. 17, captured metric: 23.42 %, N_cells = 637
        Starting iteration no. 18, captured metric: 24.05 %, N_cells = 664
        Starting iteration no. 19, captured metric: 24.69 %, N_cells = 691
        Starting iteration no. 20, captured metric: 24.99 %, N_cells = 716
        Starting iteration no. 21, captured metric: 25.66 %, N_cells = 743
        Starting iteration no. 22, captured metric: 26.12 %, N_cells = 770
        Starting iteration no. 23, captured metric: 26.81 %, N_cells = 797
        Starting iteration no. 24, captured metric: 27.31 %, N_cells = 824
        Starting iteration no. 25, captured metric: 27.88 %, N_cells = 850
        Starting iteration no. 26, captured metric: 28.36 %, N_cells = 876
        Starting iteration no. 27, captured metric: 28.71 %, N_cells = 901
        Starting iteration no. 28, captured metric: 29.13 %, N_cells = 925
        Starting iteration no. 29, captured metric: 29.45 %, N_cells = 951
        Starting iteration no. 30, captured metric: 29.89 %, N_cells = 977
        Starting iteration no. 31, captured metric: 30.38 %, N_cells = 1004
        Starting iteration no. 32, captured metric: 30.79 %, N_cells = 1031
        Starting iteration no. 33, captured metric: 30.93 %, N_cells = 1058
        Starting iteration no. 34, captured metric: 31.31 %, N_cells = 1085
        Starting iteration no. 35, captured metric: 31.5 %, N_cells = 1112
        Starting iteration no. 36, captured metric: 31.89 %, N_cells = 1139
        Starting iteration no. 37, captured metric: 32.14 %, N_cells = 1166
        Starting iteration no. 38, captured metric: 32.36 %, N_cells = 1192
        Starting iteration no. 39, captured metric: 32.59 %, N_cells = 1219
        Starting iteration no. 40, captured metric: 32.78 %, N_cells = 1246
        Starting iteration no. 41, captured metric: 32.83 %, N_cells = 1273
        Starting iteration no. 42, captured metric: 32.96 %, N_cells = 1297
        Starting iteration no. 43, captured metric: 33.22 %, N_cells = 1324
        Starting iteration no. 44, captured metric: 33.43 %, N_cells = 1351
        Starting iteration no. 45, captured metric: 33.69 %, N_cells = 1378
        Starting iteration no. 46, captured metric: 33.89 %, N_cells = 1405
        Starting iteration no. 47, captured metric: 33.99 %, N_cells = 1432
        Starting iteration no. 48, captured metric: 34.13 %, N_cells = 1459
        Starting iteration no. 49, captured metric: 34.29 %, N_cells = 1486
        Starting iteration no. 50, captured metric: 34.51 %, N_cells = 1513
        Starting iteration no. 51, captured metric: 34.83 %, N_cells = 1540
        Starting iteration no. 52, captured metric: 35.01 %, N_cells = 1567
        Starting iteration no. 53, captured metric: 35.34 %, N_cells = 1594
        Starting iteration no. 54, captured metric: 35.69 %, N_cells = 1621
        Starting iteration no. 55, captured metric: 35.81 %, N_cells = 1648
        Starting iteration no. 56, captured metric: 36.11 %, N_cells = 1675
        Starting iteration no. 57, captured metric: 36.39 %, N_cells = 1702
        Starting iteration no. 58, captured metric: 36.73 %, N_cells = 1729
        Starting iteration no. 59, captured metric: 37.01 %, N_cells = 1756
        Starting iteration no. 60, captured metric: 37.47 %, N_cells = 1783
        Starting iteration no. 61, captured metric: 37.81 %, N_cells = 1810
        Starting iteration no. 62, captured metric: 38.01 %, N_cells = 1837
        Starting iteration no. 63, captured metric: 38.25 %, N_cells = 1864
        Starting iteration no. 64, captured metric: 38.6 %, N_cells = 1888
        Starting iteration no. 65, captured metric: 38.93 %, N_cells = 1915
        Starting iteration no. 66, captured metric: 39.16 %, N_cells = 1942
        Starting iteration no. 67, captured metric: 39.53 %, N_cells = 1969
        Starting iteration no. 68, captured metric: 39.8 %, N_cells = 1996
        Starting iteration no. 69, captured metric: 40.0 %, N_cells = 2021
        Starting iteration no. 70, captured metric: 40.33 %, N_cells = 2048
        Starting iteration no. 71, captured metric: 40.6 %, N_cells = 2075
        Starting iteration no. 72, captured metric: 40.77 %, N_cells = 2097
        Starting iteration no. 73, captured metric: 41.17 %, N_cells = 2123
        Starting iteration no. 74, captured metric: 41.54 %, N_cells = 2150
        Starting iteration no. 75, captured metric: 41.89 %, N_cells = 2177
        Starting iteration no. 76, captured metric: 42.24 %, N_cells = 2204
        Starting iteration no. 77, captured metric: 42.58 %, N_cells = 2231
        Starting iteration no. 78, captured metric: 42.92 %, N_cells = 2258
        Starting iteration no. 79, captured metric: 43.27 %, N_cells = 2285
        Starting iteration no. 80, captured metric: 43.64 %, N_cells = 2312
        Starting iteration no. 81, captured metric: 43.99 %, N_cells = 2339
        Starting iteration no. 82, captured metric: 44.39 %, N_cells = 2366
        Starting iteration no. 83, captured metric: 44.79 %, N_cells = 2393
        Starting iteration no. 84, captured metric: 45.13 %, N_cells = 2420
        Starting iteration no. 85, captured metric: 45.45 %, N_cells = 2447
        Starting iteration no. 86, captured metric: 45.87 %, N_cells = 2474
        Starting iteration no. 87, captured metric: 46.2 %, N_cells = 2499
        Starting iteration no. 88, captured metric: 46.6 %, N_cells = 2526
        Starting iteration no. 89, captured metric: 46.99 %, N_cells = 2553
        Starting iteration no. 90, captured metric: 47.35 %, N_cells = 2580
        Starting iteration no. 91, captured metric: 47.7 %, N_cells = 2607
        Starting iteration no. 92, captured metric: 47.99 %, N_cells = 2634
        Starting iteration no. 93, captured metric: 48.38 %, N_cells = 2661
        Starting iteration no. 94, captured metric: 48.66 %, N_cells = 2688
        Starting iteration no. 95, captured metric: 48.86 %, N_cells = 2715
        Starting iteration no. 96, captured metric: 49.11 %, N_cells = 2742
        Starting iteration no. 97, captured metric: 49.29 %, N_cells = 2768
        Starting iteration no. 98, captured metric: 49.57 %, N_cells = 2794
        Starting iteration no. 99, captured metric: 49.75 %, N_cells = 2821
        Starting iteration no. 100, captured metric: 49.91 %, N_cells = 2848
        Starting iteration no. 101, captured metric: 50.07 %, N_cells = 2875
        Starting iteration no. 102, captured metric: 50.25 %, N_cells = 2900
        Starting iteration no. 103, captured metric: 50.5 %, N_cells = 2926
        Starting iteration no. 104, captured metric: 50.66 %, N_cells = 2949
        Starting iteration no. 105, captured metric: 50.85 %, N_cells = 2971
        Starting iteration no. 106, captured metric: 51.02 %, N_cells = 2995
        Starting iteration no. 107, captured metric: 51.27 %, N_cells = 3022
        Starting iteration no. 108, captured metric: 51.41 %, N_cells = 3046
        Starting iteration no. 109, captured metric: 51.55 %, N_cells = 3073
        Starting iteration no. 110, captured metric: 51.71 %, N_cells = 3099
        Starting iteration no. 111, captured metric: 51.9 %, N_cells = 3126
        Starting iteration no. 112, captured metric: 52.3 %, N_cells = 3153
        Starting iteration no. 113, captured metric: 52.53 %, N_cells = 3177
        Starting iteration no. 114, captured metric: 52.77 %, N_cells = 3201
        Starting iteration no. 115, captured metric: 53.06 %, N_cells = 3226
        Starting iteration no. 116, captured metric: 53.17 %, N_cells = 3253
        Starting iteration no. 117, captured metric: 53.43 %, N_cells = 3278
        Starting iteration no. 118, captured metric: 53.74 %, N_cells = 3302
        Starting iteration no. 119, captured metric: 54.05 %, N_cells = 3329
        Starting iteration no. 120, captured metric: 54.21 %, N_cells = 3354
        Starting iteration no. 121, captured metric: 54.43 %, N_cells = 3379
        Starting iteration no. 122, captured metric: 54.79 %, N_cells = 3406
        Starting iteration no. 123, captured metric: 55.05 %, N_cells = 3429
        Starting iteration no. 124, captured metric: 55.26 %, N_cells = 3456
        Starting iteration no. 125, captured metric: 55.58 %, N_cells = 3483
        Starting iteration no. 126, captured metric: 55.74 %, N_cells = 3508
        Starting iteration no. 127, captured metric: 55.87 %, N_cells = 3535
        Starting iteration no. 128, captured metric: 56.05 %, N_cells = 3560
        Starting iteration no. 129, captured metric: 56.27 %, N_cells = 3587
        Starting iteration no. 130, captured metric: 56.42 %, N_cells = 3614
        Starting iteration no. 131, captured metric: 56.76 %, N_cells = 3641
        Starting iteration no. 132, captured metric: 57.0 %, N_cells = 3665
        Starting iteration no. 133, captured metric: 57.16 %, N_cells = 3692
        Starting iteration no. 134, captured metric: 57.4 %, N_cells = 3716
        Starting iteration no. 135, captured metric: 57.65 %, N_cells = 3743
        Starting iteration no. 136, captured metric: 57.81 %, N_cells = 3770
        Starting iteration no. 137, captured metric: 57.93 %, N_cells = 3795
        Starting iteration no. 138, captured metric: 58.21 %, N_cells = 3820
        Starting iteration no. 139, captured metric: 58.45 %, N_cells = 3847
        Starting iteration no. 140, captured metric: 58.71 %, N_cells = 3874
        Starting iteration no. 141, captured metric: 58.95 %, N_cells = 3901
        Starting iteration no. 142, captured metric: 59.22 %, N_cells = 3928
        Starting iteration no. 143, captured metric: 59.38 %, N_cells = 3955
        Starting iteration no. 144, captured metric: 59.53 %, N_cells = 3982
        Starting iteration no. 145, captured metric: 59.65 %, N_cells = 4009
        Starting iteration no. 146, captured metric: 59.91 %, N_cells = 4036
[2026-02-19 15:35:55] INFO     Finished metric-based refinement.
[2026-02-19 15:35:55] INFO     Starting geometry refinement.
[2026-02-19 15:35:55] INFO     Starting refining geometry front triangle.
[2026-02-19 15:35:55] INFO     Found a minimum cell level of 7. Target level is 9.
                                                                        Refining level 8 / 9.
                                                                        Refining level 9 / 9.
[2026-02-19 15:35:56] INFO     Starting refining geometry S.
[2026-02-19 15:35:56] INFO     Found a minimum cell level of 7. Target level is 10.
                                                                        Refining level 8 / 10.
                                                                        Refining level 9 / 10.
                                                                        Refining level 10 / 10.
[2026-02-19 15:35:56] INFO     Starting refining geometry 3.
[2026-02-19 15:35:57] INFO     Found a minimum cell level of 7. Target level is 10.
                                                                        Refining level 8 / 10.
                                                                        Refining level 9 / 10.
                                                                        Refining level 10 / 10.
[2026-02-19 15:35:57] INFO     Starting refining geometry rear triangle.
[2026-02-19 15:35:58] INFO     Found a minimum cell level of 6. Target level is 9.
                                                                        Refining level 7 / 9.
                                                                        Refining level 8 / 9.
                                                                        Refining level 9 / 9.
[2026-02-19 15:35:58] INFO     Finished geometry refinement.
[2026-02-19 15:35:58] INFO     Starting renumbering final mesh.
[2026-02-19 15:35:58] INFO     Finished refinement in 20.9226 s
                                                                (147 iterations).
                                                                Time for uniform refinement: 5.4298 s
                                                                Time for metric-based refinement: 12.1724 s
                                                                Time for geometry refinement: 3.1586 s
                                                                Time for renumbering the final mesh: 0.1460 s

                                    Number of cells: 5666
                                    Minimum ref. level: 6
                                    Maximum ref. level: 10
                                    Captured metric of original grid: 60.00 %

[20]:
# export the data
export = ExportData(s_cube)
export_openfoam_fields(export, load_path, bounds)
[2026-02-19 15:35:58] WARNING  Argument ``write_times`` is ``None``. Make sure to set the ``write_times`` before calling the ``export()`` method.
[2026-02-19 15:35:59] INFO     Exporting batch 1 / 1
[2026-02-19 15:35:59] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2026-02-19 15:35:59] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2026-02-19 15:36:01] INFO     Initializing KNN and computing interpolation weights.
[2026-02-19 15:36:01] INFO     Starting interpolation and export of field U.
[2026-02-19 15:36:01] INFO     Writing HDF5 file for field U.
[2026-02-19 15:36:01] INFO     Writing XDMF file for file cylinder2D_withMore_geometries.h5
[2026-02-19 15:36:02] INFO     Finished export of field U in 3.013s.
[2026-02-19 15:36:02] INFO     Exporting batch 1 / 1
[2026-02-19 15:36:02] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2026-02-19 15:36:02] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2026-02-19 15:36:02] INFO     Starting interpolation and export of field p.
[2026-02-19 15:36:03] INFO     Writing XDMF file for file cylinder2D_withMore_geometries.h5
[2026-02-19 15:36:03] INFO     Finished export of field p in 1.308s.

As for the previous cases, we can load the results into paraview and visualize the grid and flow field:

grid_sign_s3.png sign_s3_U_field_t10s.png

8. References and further material

This completes the first tutorial. In the next tutorial we will learn how to deal with other data formats, such as HDF5, and other geometry representations.

[20]: