qlroms.intrusive_qlroms.pinball — fluidic pinball (FEniCSx)
Incompressible Navier–Stokes past three cylinders, solved with a dolfinx fractional-step scheme on Taylor–Hood P2/P1. The intrusive member uses mass-orthonormal velocity POD modes plus supremizers, UFL assembly of the projected momentum and pressure-Poisson equations, and pressure elimination through the PPE, so it has the same closed quadratic form as the KS members.
dolfinx is needed only to generate snapshots, run the FOM, or build a model on a cache
miss: pinball, pinball.rom and pinball.config import without it, and cached models
reload without it. Everything that touches dolfinx lives in aux_fenics and solver.

\(|\mathbf{u}|\) at the first test snapshot of the \(Re = 90\) campaign (interpolated from the P2 velocity nodes of the Taylor–Hood mesh); white disks are the three cylinders, crosses the near-wake probes FOM observes (obs_idx).
At a glance
| Name | One-liner |
|---|---|
config.PODGalerkinODEOperators |
Dolfinx-free container of one chart's reduced momentum + PPE operators; closed_quadratic_operators() eliminates the pressure. |
fom.FOM |
The solver as a dynamodels discrete map (state = stacked \([\bm{u}; p]\) dofs, obs_idx sensors, serial, \(m = 1\)). |
rom.ROM |
The case's GalerkinROM: mass-orthonormal velocity chart (incl. supremizers) + pressure-eliminated \((\bm{b},\mathbf{A},\mathsf{B})\). |
rom.build_local_model(Xv, Xp, *, K, r_velocity, r_pressure, reynolds, dt, save_dir, mesh_dir, ...) |
Cluster → weighted POD + supremizers → UFL Galerkin + PPE elimination → qlGalerkin; cached, dolfinx only on a miss. |
rom.assemble_galerkin_operators(...) |
The UFL assembly of one chart's operators. |
solver.PinballFOM, solver.PinballFOMParameters |
The fractional-step solver: run() for snapshot campaigns (MPI-capable), step() / get_state() / set_state() for the FOM adapter. |
aux_fenics |
Mesh labels, loader and gmsh generator, taylor_hood_spaces, assemble_mass_matrix, dirichlet_bcs, supremizer and pressure-centroid solves. |
build_pinball.load_snapshots |
Snapshot loading — see qlroms.intrusive_qlroms. |
qlroms.intrusive_qlroms.pinball.config
pinball/config.py -- PODGalerkinODEOperators, the dolfinx-free container of one chart's reduced momentum + pressure-Poisson operators assembled by pinball.rom (UFL); cached models carry a list of them in model.pressure_ops, so this class must stay importable without dolfinx and keep its module path.
PODGalerkinODEOperators
dataclass
Reduced continuous-time operators for one affine POD chart (pinball/rom.py):
a_dot = f + A a + B(a, a) + P b A_pr b = p0 - lap(p_c) + P1 a + P2(a, a)
Source code in qlroms/intrusive_qlroms/pinball/config.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
closed_quadratic_operators()
Pressure-eliminated (f_eff, A_eff, B_eff): substitute the PPE solution b(a) = A_pr^{-1}(p0 - lap(p_c) + P1 a + P2(a,a)) into the momentum equation.
Source code in qlroms/intrusive_qlroms/pinball/config.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
qlroms.intrusive_qlroms.pinball.fom
Pinball full-order model as a dynamodels.Model (discrete map).
FOM wraps the dolfinx fractional-step solver (pinball.solver.PinballFOM) behind the same Model protocol the fitted qlROMs get through qlroms.model.QLModel, so romda twin experiments can pair them: truth = FOM, forecast = QLModel, observing the SAME velocity-dof indices (FOM reads psi[obs_idx], QLModel reads H z + c).
Serial and single-member by design: the state is the stacked [velocity; pressure] dof vector and m = 1 (FOM ensembles are compute campaigns, not a Model feature). dolfinx is imported only inside _build_solver, so the module stays importable without a FEniCSx install (tests stub _build_solver).
FOM
Bases: Model
Fractional-step pinball Navier-Stokes solver as a dynamodels discrete map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
psi0
|
ndarray
|
Initial stacked [velocity; pressure] state, |
None
|
dt
|
float
|
Output step; defaults to |
None
|
reynolds
|
float
|
Reynolds number -- a declared dynamodels parameter (enters alpha0 and the
dataset filename). SET it, don't assimilate it; synced to the solver's
viscosity Constant at every |
60.0
|
mesh_dir
|
path - like
|
Pregenerated mesh directory (see pinball.aux_fenics); None uses the loader default. |
None
|
obs_idx
|
array_like of int
|
Sensor indices into the stacked state (velocity dofs, matching QLModel's H = Phi[obs_idx] rows). |
(0,)
|
dt_solver
|
float
|
Time step of the fractional-step scheme itself. |
0.01
|
Source code in qlroms/intrusive_qlroms/pinball/fom.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
dt_step
property
float: solver step rounded as Model rounds dt, so dt == dt_solver
compares equal inside DiscreteIntegrator (coarser dt -> interpolation).
obs_labels
property
list of str: LaTeX labels for the sensor observables, \(u(x_i)\).
time_step(Nt=1, **kwargs)
Advance the solver Nt fractional steps from current_state.
Returns (psi, t): trajectory (Nt + 1, Nphi, 1) with psi[0] the
current state, and the (Nt + 1,) time grid.
Source code in qlroms/intrusive_qlroms/pinball/fom.py
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | |
get_observables(Nt=1, **kwargs)
Sensor values: the obs_idx rows of the trailing state(s).
Source code in qlroms/intrusive_qlroms/pinball/fom.py
145 146 147 148 149 | |
__deepcopy__(memo)
Deep copy everything EXCEPT the solver (PETSc objects do not deepcopy): the copy rebuilds it and re-seeds from current_state on the first time_step.
Source code in qlroms/intrusive_qlroms/pinball/fom.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
qlroms.intrusive_qlroms.pinball.rom
pinball/rom.py -- the intrusive Galerkin equations for the pinball (Navier-Stokes) case, mirroring ks1d/rom.py: ROM (the case's single-cluster member), qlROM, the governing-equation projection (assemble_galerkin_operators) and build_local_model, which caches itself and needs dolfinx only on a cache miss (all dolfinx/ufl imports are local to the two build functions, so cached models reload and this module imports without FEniCSx). Everything generic (charts, clustering, POD, compilation, DA) comes from qlroms.
The reduced model, per affine chart (velocity modes Phi mass-orthonormal, incl. supremizers; velocity centered on the cluster mean; pressure expanded around the selected pressure centroid):
a_dot = f + A a + B(a, a) + P b (projected momentum equation)
A_pr b = p0 - lap(p_c) + P1 a + P2(a, a) (pressure-Poisson equation, PPE)
with the UFL assembly below providing f = -(u_c.grad u_c, phi_i) + (p_c, div phi_i) - nu (grad u_c, grad phi_i) A = -(u_c.grad phi_j + phi_j.grad u_c, phi_i) - nu (grad phi_j, grad phi_i) B = -(phi_j.grad phi_k, phi_i) P = D_r^T, D_r[l, i] = (div phi_i, psi_l) A_pr = (grad psi_k, grad psi_j), PPE right side from -(conv, grad psi_l).
Eliminating pressure through the PPE closes the velocity dynamics into the affine- quadratic form a_dot = b + A a + B(a, a) every qlroms family shares (PODGalerkinODEOperators.closed_quadratic_operators) -- a compilation of those members IS a qlGalerkin (build_local_model below; K=1 is the global case).
ROM
Bases: GalerkinROM
Pinball single-cluster member: a GalerkinROM whose chart is the cluster's mass-orthonormal velocity POD basis plus supremizers (Mw = the assembled FEM mass matrix) and whose (b, A, B) are the pressure-eliminated projected Navier-Stokes operators (PODGalerkinODEOperators.closed_quadratic_operators).
Source code in qlroms/intrusive_qlroms/pinball/rom.py
55 56 57 58 59 | |
assemble_galerkin_operators(Umodes, Pmodes, u_mean, pressure_center, nu, pressure_centroid_mode='empirical')
UFL-assemble one chart's reduced momentum + PPE operators (module docstring).
Umodes must already be mass-orthonormal (incl. supremizers); u_mean is the chart
centroid; the pressure expansion is around pressure_center (all dolfinx Functions).
Source code in qlroms/intrusive_qlroms/pinball/rom.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
build_local_model(Xv, Xp, *, K, r_velocity, r_pressure, reynolds, dt, save_dir=None, mesh_dir=None, pressure_centroid='empirical', random_state=1, cluster_space='pod_lossless')
Offline intrusive build (cached): cluster velocity snapshots (qlroms.fit_clusters), per cluster do mass-weighted POD (velocity + pressure), supremizer enrichment, mass-re-orthonormalization, and UFL Galerkin operator assembly; compile the pressure-eliminated members into a qlGalerkin (K=1 is the global case).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Xv, Xp
|
aligned (Nu, Nt) velocity and (Np, Nt) pressure snapshot matrices -- chronological, the caller's training window. |
required | |
K, r_velocity, r_pressure
|
clusters and POD ranks. Each chart's final velocity dimension is r_velocity + (r_pressure + 1) supremizers. |
required | |
reynolds, dt
|
physics + snapshot spacing (dt sizes the ETDRK4 stepping). |
required | |
save_dir
|
cache directory (default qlroms.utils.paths.PINBALL_MODELS); the cache file intrusive_model_K{K}_rv{r_velocity}_rp{r_pressure}_Re{reynolds:g}.pth is checked FIRST, so a cached model reloads without dolfinx. |
None
|
|
mesh_dir
|
pinball mesh directory (default PINBALL_MESHES), loaded only on a cache miss. |
None
|
|
pressure_centroid
|
PressureCentroidMode
|
"empirical" (cluster pressure mean) or "poisson_from_velocity_centroid". |
'empirical'
|
random_state, cluster_space
|
forwarded to the clustering. |
required |
Returns:
| Type | Description |
|---|---|
qlGalerkin
|
qlGalerkin compilation of K ROMs (Mw = velocity mass matrix); |
qlGalerkin
|
|
qlGalerkin
|
diagnostics), |
Source code in qlroms/intrusive_qlroms/pinball/rom.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | |
qlroms.intrusive_qlroms.pinball.solver
Full-order incompressible Navier-Stokes solver (dolfinx fractional step).
Two entry points share the same stepping kernel:
- run() -- the offline snapshot-generation loop (MPI-capable): warm restart,
snapshot/force saving, diagnostics. Needs snapshot_dir.
- step() -- one fractional step, the kernel pinball.fom.FOM drives through
the dynamodels Model protocol (serial), together with the state accessors
get_state / set_state / set_reynolds / num_dofs.
PinballFOMParameters
dataclass
Runtime parameters controlling the FOM time integration and snapshot export.
Source code in qlroms/intrusive_qlroms/pinball/solver.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
PinballFOM
Fractional step Navier-Stokes solver used as FOM for the bluff-body benchmark cases.
Source code in qlroms/intrusive_qlroms/pinball/solver.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | |
num_dofs
property
(Nu, Np): local velocity and pressure dof-array sizes.
get_state()
Stacked [velocity dofs; pressure dofs] state vector (a copy).
Source code in qlroms/intrusive_qlroms/pinball/solver.py
209 210 211 | |
set_state(u_values, p_values)
Overwrite the solver state; the AB2 convection history collapses onto the given field (u_n = u_n1 = u), the same approximation as a warm restart.
Source code in qlroms/intrusive_qlroms/pinball/solver.py
213 214 215 216 217 218 219 220 | |
set_reynolds(reynolds)
Update the viscosity Constant in the assembled forms (mu = 1/Re).
Source code in qlroms/intrusive_qlroms/pinball/solver.py
222 223 224 225 | |
step()
Advance one fractional step (tentative velocity, pressure correction, divergence-free update) and shift the AB2 history.
Source code in qlroms/intrusive_qlroms/pinball/solver.py
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | |
run()
Offline snapshot-generation loop (needs snapshot_dir).
Source code in qlroms/intrusive_qlroms/pinball/solver.py
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 | |
qlroms.intrusive_qlroms.pinball.aux_fenics
Pinball dolfinx layer: mesh (boundary labels, loader, gmsh generator), Taylor-Hood spaces and the FEM helpers shared by the intrusive build (pinball.rom, which imports this lazily) and the solver (pinball.solver). Mesh files (pinball_mesh.xdmf / pinball_facet_tags.xdmf / pinball_boundaries.json + .h5) are DATA, default location qlroms.utils.paths.PINBALL_MESHES; generate once with python scripts/pinball/run_pinball_fom.py configs/pinball_fom.yml --mesh-only (needs gmsh) or copy an existing set.
taylor_hood_spaces(mesh)
(V, Q): the P2 velocity / P1 pressure pair every pinball consumer builds on.
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
38 39 40 41 42 43 44 | |
load_pinball_mesh(mesh_dir=None)
Load the mesh and facet tags generated for the pinball benchmark.
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |
generate_pinball_mesh(output_dir=None, *, polynomial_order=2)
Generate the gmsh mesh with facet tags and metadata for the pinball setup.
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | |
function_from_array(space, values)
Function on space whose dof array is the 1-D values (scattered forward).
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
240 241 242 243 244 245 | |
assemble_mass_matrix(V)
Assembled FEM mass matrix (u, v) of the space V.
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
248 249 250 251 252 253 | |
dirichlet_bcs(space, facet_tags, markers, value=None)
Dirichlet BC with value (a Function; default zero) on the facets tagged with
ANY of the integer markers; [] when no dof is tagged (absent tags are fine).
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
256 257 258 259 260 261 262 263 264 265 266 | |
solve_linear_problems(a_form, rhs_forms, bcs)
CG+AMG solve of the UFL bilinear form a_form against each UFL linear form in rhs_forms, BCs applied; one Function per RHS.
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
272 273 274 275 276 | |
compute_supremizers(V, pressure_modes, bcs)
Supremizer enrichment: for each pressure mode psi solve (grad w, grad v) = -(psi, div v) with homogeneous velocity BCs -- stabilises the reduced velocity-pressure coupling (inf-sup).
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
279 280 281 282 283 284 | |
solve_pressure_centroid_from_velocity(Q, velocity_center, bcs)
Pressure centroid from the velocity centroid via the Poisson solve (grad p, grad q) = -(u_c.grad u_c, grad q) with outlet BCs -- the "poisson_from_velocity_centroid" alternative to the empirical pressure mean.
Source code in qlroms/intrusive_qlroms/pinball/aux_fenics.py
287 288 289 290 291 292 293 294 | |