Tutorial 1: 2D flow past a cylinder
Outline
Loading the data
Computing a metric and adding geometries
Creating a new mesh
Exporting the data
Optional: performing an SVD
Using the
DataLoaderMore about geometry objects
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:
a point cloud representing the coordinates of the cell centers
a value (metric) associated with each cell center, indicating the importance of that cell
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:
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:
mesh_info_<save_name>.pts_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:
<save_name>.h5which contains all the exported flow fields along with the metric and cell levels<save_name>.xdmfwhich 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:
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:
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]: