Tutorial 1: 2D flow past a cylinder

flowTorch workshop 29.09.2025 - 02.10.2025

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.

[1]:
import sys
import torch as pt
from os.path import join
from os import environ, system

environ["sparseSpatialSampling"] = "../../.."
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
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.
[2]:
# 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)
[3]:
# 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.")
[2025-08-21 09:39:06] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2025-08-21 09:39:06] 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

[4]:
# 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)
[2025-08-21 09:39:07] INFO
        Selected settings:
                _pre_select          :  False
                _n_jobs              :  4
                _max_delta_level     :  False
                _geometry            :  ['domain', 'cylinder']
                _min_metric          :  0.75
                _n_cells_max         :  None
                _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:

[5]:
# execute the mesh generation with S^3
s_cube.execute_grid_generation()
[2025-08-21 09:39:07] INFO     Starting refinement:
        Starting iteration no. 0, N_cells = 1
        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
[2025-08-21 09:39:12] INFO     Finished uniform refinement.
[2025-08-21 09:39:12] INFO     Starting adaptive refinement.
        Starting iteration no. 0, captured metric: 13.66 %, N_cells = 256
        Starting iteration no. 1, captured metric: 14.53 %, N_cells = 281
        Starting iteration no. 2, captured metric: 15.1 %, N_cells = 308
        Starting iteration no. 3, captured metric: 15.4 %, N_cells = 335
        Starting iteration no. 4, captured metric: 16.34 %, N_cells = 362
        Starting iteration no. 5, captured metric: 16.51 %, N_cells = 389
        Starting iteration no. 6, captured metric: 16.67 %, N_cells = 416
        Starting iteration no. 7, captured metric: 16.92 %, N_cells = 443
        Starting iteration no. 8, captured metric: 17.13 %, N_cells = 470
        Starting iteration no. 9, captured metric: 17.62 %, N_cells = 497
        Starting iteration no. 10, captured metric: 18.84 %, N_cells = 523
        Starting iteration no. 11, captured metric: 19.45 %, N_cells = 550
        Starting iteration no. 12, captured metric: 20.05 %, N_cells = 577
        Starting iteration no. 13, captured metric: 20.65 %, N_cells = 604
        Starting iteration no. 14, captured metric: 21.24 %, N_cells = 631
        Starting iteration no. 15, captured metric: 21.86 %, N_cells = 657
        Starting iteration no. 16, captured metric: 22.7 %, N_cells = 684
        Starting iteration no. 17, captured metric: 23.53 %, N_cells = 710
        Starting iteration no. 18, captured metric: 24.31 %, N_cells = 737
        Starting iteration no. 19, captured metric: 24.93 %, N_cells = 764
        Starting iteration no. 20, captured metric: 25.36 %, N_cells = 791
        Starting iteration no. 21, captured metric: 25.86 %, N_cells = 818
        Starting iteration no. 22, captured metric: 26.45 %, N_cells = 845
        Starting iteration no. 23, captured metric: 27.03 %, N_cells = 872
        Starting iteration no. 24, captured metric: 27.58 %, N_cells = 899
        Starting iteration no. 25, captured metric: 28.11 %, N_cells = 926
        Starting iteration no. 26, captured metric: 28.73 %, N_cells = 953
        Starting iteration no. 27, captured metric: 29.05 %, N_cells = 980
        Starting iteration no. 28, captured metric: 29.46 %, N_cells = 1007
        Starting iteration no. 29, captured metric: 29.93 %, N_cells = 1034
        Starting iteration no. 30, captured metric: 30.19 %, N_cells = 1061
        Starting iteration no. 31, captured metric: 30.5 %, N_cells = 1088
        Starting iteration no. 32, captured metric: 30.81 %, N_cells = 1115
        Starting iteration no. 33, captured metric: 31.28 %, N_cells = 1142
        Starting iteration no. 34, captured metric: 31.55 %, N_cells = 1169
        Starting iteration no. 35, captured metric: 31.97 %, N_cells = 1196
        Starting iteration no. 36, captured metric: 32.34 %, N_cells = 1223
        Starting iteration no. 37, captured metric: 32.62 %, N_cells = 1250
        Starting iteration no. 38, captured metric: 32.91 %, N_cells = 1277
        Starting iteration no. 39, captured metric: 33.04 %, N_cells = 1304
        Starting iteration no. 40, captured metric: 33.06 %, N_cells = 1325
        Starting iteration no. 41, captured metric: 33.32 %, N_cells = 1352
        Starting iteration no. 42, captured metric: 33.64 %, N_cells = 1379
        Starting iteration no. 43, captured metric: 33.98 %, N_cells = 1406
        Starting iteration no. 44, captured metric: 34.07 %, N_cells = 1433
        Starting iteration no. 45, captured metric: 34.22 %, N_cells = 1456
        Starting iteration no. 46, captured metric: 34.43 %, N_cells = 1481
        Starting iteration no. 47, captured metric: 34.58 %, N_cells = 1508
        Starting iteration no. 48, captured metric: 34.8 %, N_cells = 1535
        Starting iteration no. 49, captured metric: 34.95 %, N_cells = 1562
        Starting iteration no. 50, captured metric: 35.1 %, N_cells = 1589
        Starting iteration no. 51, captured metric: 35.31 %, N_cells = 1616
        Starting iteration no. 52, captured metric: 35.46 %, N_cells = 1643
        Starting iteration no. 53, captured metric: 35.65 %, N_cells = 1670
        Starting iteration no. 54, captured metric: 35.89 %, N_cells = 1695
        Starting iteration no. 55, captured metric: 36.04 %, N_cells = 1722
        Starting iteration no. 56, captured metric: 36.25 %, N_cells = 1749
        Starting iteration no. 57, captured metric: 36.58 %, N_cells = 1776
        Starting iteration no. 58, captured metric: 36.83 %, N_cells = 1803
        Starting iteration no. 59, captured metric: 37.19 %, N_cells = 1830
        Starting iteration no. 60, captured metric: 37.51 %, N_cells = 1855
        Starting iteration no. 61, captured metric: 37.91 %, N_cells = 1880
        Starting iteration no. 62, captured metric: 38.34 %, N_cells = 1907
        Starting iteration no. 63, captured metric: 38.62 %, N_cells = 1934
        Starting iteration no. 64, captured metric: 38.79 %, N_cells = 1957
        Starting iteration no. 65, captured metric: 39.1 %, N_cells = 1984
        Starting iteration no. 66, captured metric: 39.39 %, N_cells = 2011
        Starting iteration no. 67, captured metric: 39.71 %, N_cells = 2038
        Starting iteration no. 68, captured metric: 40.01 %, N_cells = 2065
        Starting iteration no. 69, captured metric: 40.23 %, N_cells = 2092
        Starting iteration no. 70, captured metric: 40.44 %, N_cells = 2117
        Starting iteration no. 71, captured metric: 40.66 %, N_cells = 2144
        Starting iteration no. 72, captured metric: 40.89 %, N_cells = 2167
        Starting iteration no. 73, captured metric: 41.23 %, N_cells = 2194
        Starting iteration no. 74, captured metric: 41.45 %, N_cells = 2217
        Starting iteration no. 75, captured metric: 41.7 %, N_cells = 2238
        Starting iteration no. 76, captured metric: 41.94 %, N_cells = 2263
        Starting iteration no. 77, captured metric: 42.23 %, N_cells = 2286
        Starting iteration no. 78, captured metric: 42.47 %, N_cells = 2307
        Starting iteration no. 79, captured metric: 42.81 %, N_cells = 2334
        Starting iteration no. 80, captured metric: 43.13 %, N_cells = 2359
        Starting iteration no. 81, captured metric: 43.41 %, N_cells = 2384
        Starting iteration no. 82, captured metric: 43.74 %, N_cells = 2411
        Starting iteration no. 83, captured metric: 44.08 %, N_cells = 2438
        Starting iteration no. 84, captured metric: 44.42 %, N_cells = 2465
        Starting iteration no. 85, captured metric: 44.72 %, N_cells = 2488
        Starting iteration no. 86, captured metric: 45.11 %, N_cells = 2515
        Starting iteration no. 87, captured metric: 45.43 %, N_cells = 2542
        Starting iteration no. 88, captured metric: 45.78 %, N_cells = 2565
        Starting iteration no. 89, captured metric: 46.12 %, N_cells = 2590
        Starting iteration no. 90, captured metric: 46.47 %, N_cells = 2615
        Starting iteration no. 91, captured metric: 46.73 %, N_cells = 2638
        Starting iteration no. 92, captured metric: 47.01 %, N_cells = 2663
        Starting iteration no. 93, captured metric: 47.2 %, N_cells = 2684
        Starting iteration no. 94, captured metric: 47.56 %, N_cells = 2711
        Starting iteration no. 95, captured metric: 47.85 %, N_cells = 2734
        Starting iteration no. 96, captured metric: 48.18 %, N_cells = 2761
        Starting iteration no. 97, captured metric: 48.51 %, N_cells = 2788
        Starting iteration no. 98, captured metric: 48.85 %, N_cells = 2813
        Starting iteration no. 99, captured metric: 49.15 %, N_cells = 2838
        Starting iteration no. 100, captured metric: 49.43 %, N_cells = 2861
        Starting iteration no. 101, captured metric: 49.64 %, N_cells = 2882
        Starting iteration no. 102, captured metric: 49.82 %, N_cells = 2907
        Starting iteration no. 103, captured metric: 50.11 %, N_cells = 2934
        Starting iteration no. 104, captured metric: 50.27 %, N_cells = 2957
        Starting iteration no. 105, captured metric: 50.49 %, N_cells = 2982
        Starting iteration no. 106, captured metric: 50.56 %, N_cells = 3005
        Starting iteration no. 107, captured metric: 50.81 %, N_cells = 3030
        Starting iteration no. 108, captured metric: 51.02 %, N_cells = 3053
        Starting iteration no. 109, captured metric: 51.19 %, N_cells = 3080
        Starting iteration no. 110, captured metric: 51.42 %, N_cells = 3105
        Starting iteration no. 111, captured metric: 51.55 %, N_cells = 3130
        Starting iteration no. 112, captured metric: 51.78 %, N_cells = 3153
        Starting iteration no. 113, captured metric: 51.89 %, N_cells = 3174
        Starting iteration no. 114, captured metric: 52.12 %, N_cells = 3200
        Starting iteration no. 115, captured metric: 52.4 %, N_cells = 3225
        Starting iteration no. 116, captured metric: 52.5 %, N_cells = 3246
        Starting iteration no. 117, captured metric: 52.77 %, N_cells = 3273
        Starting iteration no. 118, captured metric: 52.97 %, N_cells = 3300
        Starting iteration no. 119, captured metric: 53.2 %, N_cells = 3327
        Starting iteration no. 120, captured metric: 53.36 %, N_cells = 3354
        Starting iteration no. 121, captured metric: 53.44 %, N_cells = 3377
        Starting iteration no. 122, captured metric: 53.56 %, N_cells = 3404
        Starting iteration no. 123, captured metric: 53.79 %, N_cells = 3429
        Starting iteration no. 124, captured metric: 53.87 %, N_cells = 3450
        Starting iteration no. 125, captured metric: 54.08 %, N_cells = 3473
        Starting iteration no. 126, captured metric: 54.36 %, N_cells = 3500
        Starting iteration no. 127, captured metric: 54.62 %, N_cells = 3527
        Starting iteration no. 128, captured metric: 54.87 %, N_cells = 3554
        Starting iteration no. 129, captured metric: 55.13 %, N_cells = 3577
        Starting iteration no. 130, captured metric: 55.31 %, N_cells = 3600
        Starting iteration no. 131, captured metric: 55.5 %, N_cells = 3625
        Starting iteration no. 132, captured metric: 55.83 %, N_cells = 3649
        Starting iteration no. 133, captured metric: 56.06 %, N_cells = 3676
        Starting iteration no. 134, captured metric: 56.27 %, N_cells = 3699
        Starting iteration no. 135, captured metric: 56.51 %, N_cells = 3724
        Starting iteration no. 136, captured metric: 56.76 %, N_cells = 3749
        Starting iteration no. 137, captured metric: 56.98 %, N_cells = 3776
        Starting iteration no. 138, captured metric: 57.11 %, N_cells = 3801
        Starting iteration no. 139, captured metric: 57.29 %, N_cells = 3826
        Starting iteration no. 140, captured metric: 57.44 %, N_cells = 3853
        Starting iteration no. 141, captured metric: 57.75 %, N_cells = 3880
        Starting iteration no. 142, captured metric: 58.06 %, N_cells = 3907
        Starting iteration no. 143, captured metric: 58.27 %, N_cells = 3934
        Starting iteration no. 144, captured metric: 58.45 %, N_cells = 3959
        Starting iteration no. 145, captured metric: 58.69 %, N_cells = 3984
        Starting iteration no. 146, captured metric: 58.9 %, N_cells = 4009
        Starting iteration no. 147, captured metric: 59.07 %, N_cells = 4034
        Starting iteration no. 148, captured metric: 59.33 %, N_cells = 4061
        Starting iteration no. 149, captured metric: 59.54 %, N_cells = 4088
        Starting iteration no. 150, captured metric: 59.73 %, N_cells = 4115
        Starting iteration no. 151, captured metric: 59.9 %, N_cells = 4140
        Starting iteration no. 152, captured metric: 60.05 %, N_cells = 4165
[2025-08-21 09:39:19] INFO     Finished adaptive refinement.
[2025-08-21 09:39:19] INFO     Starting geometry refinement.
[2025-08-21 09:39:19] INFO     Starting refining geometry cylinder.
[2025-08-21 09:39:19] INFO     Finished geometry refinement.
[2025-08-21 09:39:19] INFO     Starting renumbering final mesh.
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.
[2025-08-21 09:39:24] INFO     Finished refinement in 16.6574 s
                                                                (153 iterations).
                                                                Time for uniform refinement: 4.9402 s
                                                                Time for adaptive refinement: 7.0549 s
                                                                Time for geometry refinement: 0.2899 s
                                                                Time for renumbering the final mesh: 4.3574 s

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

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 \(61.46\%\). 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:

[6]:
# 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.

[7]:
# 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)
[2025-08-21 09:39:24] INFO     Exporting batch 1 / 1
[2025-08-21 09:39:24] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2025-08-21 09:39:24] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2025-08-21 09:39:26] INFO     Starting interpolation and export of field U.
[2025-08-21 09:39:27] INFO     Writing HDF5 file for field U.
[2025-08-21 09:39:29] INFO     Writing XDMF file for file cylinder2D_metric_0.75.h5
[2025-08-21 09:39:30] INFO     Finished export of field U in 5.76s.
[2025-08-21 09:39:30] INFO     Exporting batch 1 / 1
[2025-08-21 09:39:30] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2025-08-21 09:39:30] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2025-08-21 09:39:31] INFO     Writing XDMF file for file cylinder2D_metric_0.75.h5
[2025-08-21 09:39:31] INFO     Finished export of field p in 1.899s.
[8]:
# 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")
[2025-08-21 09:39:32] INFO     Starting interpolation and export of field U.
[2025-08-21 09:39:32] INFO     Writing HDF5 file for field U.
[2025-08-21 09:39:32] INFO     Writing XDMF file for file cylinder2D_metric_0.75.h5
[2025-08-21 09:39:32] INFO     Finished export of field U in 0.743s.
[2025-08-21 09:39:32] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2025-08-21 09:39:32] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2025-08-21 09:39:33] INFO     Writing XDMF file for file cylinder2D_metric_0.75.h5
[2025-08-21 09:39:33] INFO     Finished export of field p in 0.797s.

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.

[9]:
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 pahse
write_svd_s_cube_to_file(["p", "U"], save_path, save_name, False, 50, rank=int(1e5), t_start=4)
[2025-08-21 09:39:33] INFO     Performing SVD for field p.
[2025-08-21 09:39:33] INFO     Writing XDMF file for file cylinder2D_metric_0.75_p_svd.h5
[2025-08-21 09:39:33] INFO     Performing SVD for field U.
[2025-08-21 09:39:33] 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:

[10]:
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: 4270
Field names at t = 4.000s: ['U', 'p']

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

post_processing/compare_svd_results_cylinder3D_Re3900.py

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:

[11]:
# we can check out the available geometry object classes currently implemented in S^3
from sparseSpatialSampling.sparse_spatial_sampling import list_geometries
list_geometries()
[2025-10-06 12:00:51] 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     : 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.

[12]:
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)
    ]
[2025-08-21 09:39:33] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2025-08-21 09:39:33] 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.

[13]:
# 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()
[2025-08-21 09:39:35] 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
                _n_cells_max         :  None
                _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
[2025-08-21 09:39:35] INFO     Starting refinement:
        Starting iteration no. 0, N_cells = 1
        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
[2025-08-21 09:39:40] INFO     Finished uniform refinement.
[2025-08-21 09:39:40] INFO     Starting adaptive refinement.
        Starting iteration no. 0, captured metric: 13.66 %, N_cells = 256
        Starting iteration no. 1, captured metric: 14.53 %, N_cells = 281
        Starting iteration no. 2, captured metric: 15.1 %, N_cells = 308
        Starting iteration no. 3, captured metric: 15.4 %, N_cells = 335
        Starting iteration no. 4, captured metric: 16.34 %, N_cells = 361
        Starting iteration no. 5, captured metric: 16.51 %, N_cells = 388
        Starting iteration no. 6, captured metric: 16.67 %, N_cells = 415
        Starting iteration no. 7, captured metric: 16.92 %, N_cells = 441
        Starting iteration no. 8, captured metric: 17.13 %, N_cells = 468
        Starting iteration no. 9, captured metric: 17.62 %, N_cells = 495
        Starting iteration no. 10, captured metric: 18.81 %, N_cells = 517
        Starting iteration no. 11, captured metric: 19.43 %, N_cells = 544
        Starting iteration no. 12, captured metric: 20.03 %, N_cells = 571
        Starting iteration no. 13, captured metric: 20.62 %, N_cells = 598
        Starting iteration no. 14, captured metric: 21.22 %, N_cells = 625
        Starting iteration no. 15, captured metric: 21.83 %, N_cells = 650
        Starting iteration no. 16, captured metric: 22.68 %, N_cells = 677
        Starting iteration no. 17, captured metric: 23.51 %, N_cells = 701
        Starting iteration no. 18, captured metric: 24.29 %, N_cells = 728
        Starting iteration no. 19, captured metric: 24.91 %, N_cells = 753
        Starting iteration no. 20, captured metric: 25.33 %, N_cells = 780
        Starting iteration no. 21, captured metric: 25.84 %, N_cells = 807
        Starting iteration no. 22, captured metric: 26.42 %, N_cells = 834
        Starting iteration no. 23, captured metric: 26.97 %, N_cells = 859
        Starting iteration no. 24, captured metric: 27.52 %, N_cells = 886
        Starting iteration no. 25, captured metric: 28.05 %, N_cells = 913
        Starting iteration no. 26, captured metric: 28.68 %, N_cells = 940
        Starting iteration no. 27, captured metric: 29.05 %, N_cells = 965
        Starting iteration no. 28, captured metric: 29.39 %, N_cells = 992
        Starting iteration no. 29, captured metric: 29.81 %, N_cells = 1018
        Starting iteration no. 30, captured metric: 30.06 %, N_cells = 1042
        Starting iteration no. 31, captured metric: 30.32 %, N_cells = 1067
        Starting iteration no. 32, captured metric: 30.65 %, N_cells = 1094
        Starting iteration no. 33, captured metric: 31.15 %, N_cells = 1121
        Starting iteration no. 34, captured metric: 31.46 %, N_cells = 1148
        Starting iteration no. 35, captured metric: 31.79 %, N_cells = 1173
        Starting iteration no. 36, captured metric: 32.22 %, N_cells = 1200
        Starting iteration no. 37, captured metric: 32.51 %, N_cells = 1227
        Starting iteration no. 38, captured metric: 32.75 %, N_cells = 1254
        Starting iteration no. 39, captured metric: 32.87 %, N_cells = 1281
        Starting iteration no. 40, captured metric: 32.89 %, N_cells = 1302
        Starting iteration no. 41, captured metric: 33.26 %, N_cells = 1329
        Starting iteration no. 42, captured metric: 33.48 %, N_cells = 1356
        Starting iteration no. 43, captured metric: 33.77 %, N_cells = 1378
        Starting iteration no. 44, captured metric: 33.93 %, N_cells = 1405
        Starting iteration no. 45, captured metric: 34.01 %, N_cells = 1428
        Starting iteration no. 46, captured metric: 34.23 %, N_cells = 1453
        Starting iteration no. 47, captured metric: 34.37 %, N_cells = 1480
        Starting iteration no. 48, captured metric: 34.58 %, N_cells = 1506
        Starting iteration no. 49, captured metric: 34.72 %, N_cells = 1533
        Starting iteration no. 50, captured metric: 34.89 %, N_cells = 1560
        Starting iteration no. 51, captured metric: 35.13 %, N_cells = 1587
        Starting iteration no. 52, captured metric: 35.25 %, N_cells = 1614
        Starting iteration no. 53, captured metric: 35.44 %, N_cells = 1641
        Starting iteration no. 54, captured metric: 35.68 %, N_cells = 1666
        Starting iteration no. 55, captured metric: 35.83 %, N_cells = 1693
        Starting iteration no. 56, captured metric: 36.07 %, N_cells = 1717
        Starting iteration no. 57, captured metric: 36.36 %, N_cells = 1744
        Starting iteration no. 58, captured metric: 36.75 %, N_cells = 1771
        Starting iteration no. 59, captured metric: 37.07 %, N_cells = 1798
        Starting iteration no. 60, captured metric: 37.28 %, N_cells = 1821
        Starting iteration no. 61, captured metric: 37.85 %, N_cells = 1848
        Starting iteration no. 62, captured metric: 38.09 %, N_cells = 1875
        Starting iteration no. 63, captured metric: 38.43 %, N_cells = 1902
        Starting iteration no. 64, captured metric: 38.6 %, N_cells = 1925
        Starting iteration no. 65, captured metric: 38.94 %, N_cells = 1951
        Starting iteration no. 66, captured metric: 39.19 %, N_cells = 1978
        Starting iteration no. 67, captured metric: 39.55 %, N_cells = 2005
        Starting iteration no. 68, captured metric: 39.79 %, N_cells = 2032
        Starting iteration no. 69, captured metric: 40.01 %, N_cells = 2059
        Starting iteration no. 70, captured metric: 40.23 %, N_cells = 2084
        Starting iteration no. 71, captured metric: 40.42 %, N_cells = 2109
        Starting iteration no. 72, captured metric: 40.81 %, N_cells = 2134
        Starting iteration no. 73, captured metric: 41.0 %, N_cells = 2159
        Starting iteration no. 74, captured metric: 41.31 %, N_cells = 2184
        Starting iteration no. 75, captured metric: 41.46 %, N_cells = 2203
        Starting iteration no. 76, captured metric: 41.77 %, N_cells = 2226
        Starting iteration no. 77, captured metric: 41.97 %, N_cells = 2245
        Starting iteration no. 78, captured metric: 42.31 %, N_cells = 2272
        Starting iteration no. 79, captured metric: 42.63 %, N_cells = 2297
        Starting iteration no. 80, captured metric: 42.92 %, N_cells = 2322
        Starting iteration no. 81, captured metric: 43.26 %, N_cells = 2349
        Starting iteration no. 82, captured metric: 43.58 %, N_cells = 2376
        Starting iteration no. 83, captured metric: 43.94 %, N_cells = 2403
        Starting iteration no. 84, captured metric: 44.31 %, N_cells = 2430
        Starting iteration no. 85, captured metric: 44.63 %, N_cells = 2453
        Starting iteration no. 86, captured metric: 44.98 %, N_cells = 2480
        Starting iteration no. 87, captured metric: 45.27 %, N_cells = 2503
        Starting iteration no. 88, captured metric: 45.66 %, N_cells = 2530
        Starting iteration no. 89, captured metric: 46.01 %, N_cells = 2555
        Starting iteration no. 90, captured metric: 46.33 %, N_cells = 2580
        Starting iteration no. 91, captured metric: 46.53 %, N_cells = 2601
        Starting iteration no. 92, captured metric: 46.85 %, N_cells = 2626
        Starting iteration no. 93, captured metric: 47.12 %, N_cells = 2649
        Starting iteration no. 94, captured metric: 47.38 %, N_cells = 2672
        Starting iteration no. 95, captured metric: 47.71 %, N_cells = 2699
        Starting iteration no. 96, captured metric: 48.08 %, N_cells = 2726
        Starting iteration no. 97, captured metric: 48.35 %, N_cells = 2749
        Starting iteration no. 98, captured metric: 48.72 %, N_cells = 2774
        Starting iteration no. 99, captured metric: 48.99 %, N_cells = 2797
        Starting iteration no. 100, captured metric: 49.22 %, N_cells = 2818
        Starting iteration no. 101, captured metric: 49.46 %, N_cells = 2842
        Starting iteration no. 102, captured metric: 49.68 %, N_cells = 2869
        Starting iteration no. 103, captured metric: 49.91 %, N_cells = 2892
        Starting iteration no. 104, captured metric: 50.12 %, N_cells = 2916
        Starting iteration no. 105, captured metric: 50.18 %, N_cells = 2939
        Starting iteration no. 106, captured metric: 50.43 %, N_cells = 2964
        Starting iteration no. 107, captured metric: 50.6 %, N_cells = 2987
        Starting iteration no. 108, captured metric: 50.81 %, N_cells = 3013
        Starting iteration no. 109, captured metric: 50.98 %, N_cells = 3034
        Starting iteration no. 110, captured metric: 51.12 %, N_cells = 3059
        Starting iteration no. 111, captured metric: 51.37 %, N_cells = 3084
        Starting iteration no. 112, captured metric: 51.48 %, N_cells = 3105
        Starting iteration no. 113, captured metric: 51.65 %, N_cells = 3125
        Starting iteration no. 114, captured metric: 51.89 %, N_cells = 3150
        Starting iteration no. 115, captured metric: 52.03 %, N_cells = 3171
        Starting iteration no. 116, captured metric: 52.24 %, N_cells = 3192
        Starting iteration no. 117, captured metric: 52.37 %, N_cells = 3213
        Starting iteration no. 118, captured metric: 52.61 %, N_cells = 3237
        Starting iteration no. 119, captured metric: 52.74 %, N_cells = 3264
        Starting iteration no. 120, captured metric: 52.82 %, N_cells = 3285
        Starting iteration no. 121, captured metric: 52.95 %, N_cells = 3312
        Starting iteration no. 122, captured metric: 53.18 %, N_cells = 3337
        Starting iteration no. 123, captured metric: 53.26 %, N_cells = 3358
        Starting iteration no. 124, captured metric: 53.47 %, N_cells = 3381
        Starting iteration no. 125, captured metric: 53.74 %, N_cells = 3405
        Starting iteration no. 126, captured metric: 54.01 %, N_cells = 3432
        Starting iteration no. 127, captured metric: 54.22 %, N_cells = 3457
        Starting iteration no. 128, captured metric: 54.49 %, N_cells = 3480
        Starting iteration no. 129, captured metric: 54.64 %, N_cells = 3503
        Starting iteration no. 130, captured metric: 54.82 %, N_cells = 3526
        Starting iteration no. 131, captured metric: 55.11 %, N_cells = 3548
        Starting iteration no. 132, captured metric: 55.32 %, N_cells = 3573
        Starting iteration no. 133, captured metric: 55.59 %, N_cells = 3598
        Starting iteration no. 134, captured metric: 55.82 %, N_cells = 3623
        Starting iteration no. 135, captured metric: 56.03 %, N_cells = 3648
        Starting iteration no. 136, captured metric: 56.27 %, N_cells = 3675
        Starting iteration no. 137, captured metric: 56.4 %, N_cells = 3700
        Starting iteration no. 138, captured metric: 56.54 %, N_cells = 3722
        Starting iteration no. 139, captured metric: 56.82 %, N_cells = 3749
        Starting iteration no. 140, captured metric: 57.11 %, N_cells = 3776
        Starting iteration no. 141, captured metric: 57.39 %, N_cells = 3803
        Starting iteration no. 142, captured metric: 57.54 %, N_cells = 3827
        Starting iteration no. 143, captured metric: 57.75 %, N_cells = 3849
        Starting iteration no. 144, captured metric: 58.0 %, N_cells = 3872
        Starting iteration no. 145, captured metric: 58.11 %, N_cells = 3897
        Starting iteration no. 146, captured metric: 58.35 %, N_cells = 3922
        Starting iteration no. 147, captured metric: 58.58 %, N_cells = 3947
        Starting iteration no. 148, captured metric: 58.74 %, N_cells = 3972
        Starting iteration no. 149, captured metric: 58.88 %, N_cells = 3995
        Starting iteration no. 150, captured metric: 59.03 %, N_cells = 4020
[2025-08-21 09:39:51] INFO     Finished adaptive refinement.
[2025-08-21 09:39:51] INFO     Starting geometry refinement.
[2025-08-21 09:39:51] INFO     Starting refining geometry front triangle.
[2025-08-21 09:39:52] INFO     Starting refining geometry S.
[2025-08-21 09:39:53] INFO     Starting refining geometry 3.
[2025-08-21 09:39:54] INFO     Starting refining geometry rear triangle.
[2025-08-21 09:39:55] INFO     Finished geometry refinement.
[2025-08-21 09:39:55] INFO     Starting renumbering final mesh.
[2025-08-21 09:39:55] INFO     Finished refinement in 19.8754 s
                                                                (151 iterations).
                                                                Time for uniform refinement: 5.6852 s
                                                                Time for adaptive refinement: 10.9176 s
                                                                Time for geometry refinement: 3.1814 s
                                                                Time for renumbering the final mesh: 0.0752 s

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

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.
[14]:
# export the data
export = ExportData(s_cube)
export_openfoam_fields(export, load_path, bounds)
[2025-08-21 09:39:55] INFO     Exporting batch 1 / 1
[2025-08-21 09:39:55] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2025-08-21 09:39:55] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2025-08-21 09:39:57] INFO     Starting interpolation and export of field U.
[2025-08-21 09:39:58] INFO     Writing HDF5 file for field U.
[2025-08-21 09:39:59] INFO     Writing XDMF file for file cylinder2D_withMore_geometries.h5
[2025-08-21 09:39:59] INFO     Finished export of field U in 4.471s.
[2025-08-21 09:39:59] INFO     Exporting batch 1 / 1
[2025-08-21 09:39:59] INFO     Loading precomputed cell centers and volumes from processor0/constant
[2025-08-21 09:39:59] INFO     Loading precomputed cell centers and volumes from processor1/constant
[2025-08-21 09:40:00] INFO     Writing XDMF file for file cylinder2D_withMore_geometries.h5
[2025-08-21 09:40:01] INFO     Finished export of field p in 1.476s.

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.

[ ]: