Tutorial 1: 2D flow past a cylinder
flowTorch workshop 29.09.2025 - 02.10.2025
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.
[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:
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:
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.
[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:
<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.
[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:
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:
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.
[ ]: