qlroms.intrusive_qlroms — qlGalerkin and the data pipelines
The intrusive family: qlGalerkin is the compilation of Galerkin-projected members, and
the three cases define the members it compiles. Every case package has the same shape —
config.py (TEST_CASES + the torch-side grids/operators), fom.py (the full-order
model, a dynamodels Model), rom.py (the single-cluster ROM class and
build_local_model, which caches itself). The offline data pipelines sit next to them:
build_ks (KS FOM + cached trajectory) and build_pinball (snapshot loading).
The cases have their own pages: KS 1-D · KS 2-D · fluidic pinball.
At a glance
| Name | One-liner |
|---|---|
galerkin.qlGalerkin |
Compilation of intrusively-projected members, one GalerkinROM per chart (isinstance tells the family apart); from_operators reloads stacked (b, A, B). |
build_ks |
KS offline data pipeline: build_fom (a case's FOM + merged run config), get_full_trajectory (the physical trajectory, generated in chunks and cached), build_sensor_indices (sensor placement on the KS grid). |
build_pinball |
load_snapshots: stack velocity_*.npy / pressure_*.npy into an (Ndof, Nt) matrix plus the snapshot spacing. |
qlroms.intrusive_qlroms.galerkin
qlGalerkin: the intrusive model family.
A qlGalerkin is simply the compilation (qlroms.base.qlROM) of single-cluster
ROMs whose operators come from an intrusive Galerkin projection of a known
governing-equation operator -- the case ROM classes (ks1d.rom.ROM,
ks2d.rom.ROM, pinball.rom.ROM) own those operators and step_reduced;
compiling K of them yields the quantized-local model. The name records the family,
so isinstance(model, qlGalerkin) identifies intrusive compilations in
family-agnostic code.
GalerkinROM
Bases: QuadROM
One cluster's intrusive quadratic ROM: the family-neutral QuadROM member (qlroms.charts.QuadROM -- geometry + (b, A, B) + ETDRK4 step), labeled as INTRUSIVE: its operators come from a Galerkin projection of a known governing-equation operator, never from a regression.
Source code in qlroms/intrusive_qlroms/galerkin.py
18 19 20 21 22 | |
qlGalerkin
Bases: qlROM
Compilation of intrusively-projected (Galerkin) single-cluster ROMs.
Source code in qlroms/intrusive_qlroms/galerkin.py
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 | |
from_operators(centroids, Phi_all, b_all, A_all, B_all, dt=1.0, Mw=None, Mcontour=32, Rcontour=15.0)
classmethod
Build a qlGalerkin from already-projected reduced operators, skipping any fit -- e.g. to reload stacked (b, A, B) from disk (tests/test_qlgalerkin_bridge.py); the case builders compile their own ROM classes directly.
The reduced continuous quadratic ODE da/dt = b + A a + B(a, a) is the family-neutral qlroms.charts.QuadROM compute; GalerkinROM records the intrusive provenance. centroids (K, N), Phi_all (N, r, K), b/A/B stacked (K, ...); Mw is the chart inner-product weight (e.g. the FEM mass matrix; None for M = I).
Source code in qlroms/intrusive_qlroms/galerkin.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
qlroms.intrusive_qlroms.build_ks
Offline KS build pipeline (ks1d / ks2d): the case FOM (build_fom) and its cached full physical trajectory (get_full_trajectory), which feed the case rom.build_local_model -- see qlroms.utils.builder.build_case for the config-driven orchestration. Also the sensor-placement helper build_sensor_indices.
The full-order models are the cases' own FOM classes (module.FOM: a dynamodels.physical.KS / KS2D subclass); the ROM machinery stays torch. build_fom constructs module.FOM for a case, get_full_trajectory drives model.time_integrate and caches the physical snapshot matrix in the exact same file format/layout as the old torch FOMs, so existing full_trajectory.pth caches keep hitting.
state_dim(model)
Flattened PHYSICAL state length of a KS FOM or config (1D: Nx, 2D: Nx*Ny).
Do not use model.N for dynamodels models: dynamodels.Model.N is the analysis-augmented size (Nphi + Na + Nq), not the physical grid size.
Source code in qlroms/intrusive_qlroms/build_ks.py
25 26 27 28 29 30 31 32 33 | |
build_fom(module, case=None, overrides=None)
Build the case's full-order model and return (fom, cfg).
The FOM is module.FOM(case=...) -- a dynamodels Model subclass
(ks1d.FOM(physical.KS) / ks2d.FOM(physical.KS2D)) that reads the case's
PHYSICAL parameters straight out of its own torch-side config, which it
keeps as fom.cfg. There is no rescaling and nothing is attached after
construction: fom.dt IS the case's physical time step, and the case-side
quantities the ROM stack reads (visc/Lx/lamb1/device/rdtype/cdtype, 2-D
wt) are properties on the class delegating to fom.cfg.
Per-case model params (Nx/Ny/dt/visc/nu/lamb1) live in module.TEST_CASES[case].
Dataset params (Ntrain/Ntest/i0) come from TEST_CASES[case] (1D) or
module.TIME_DEFAULTS (2D). overrides (CLI flags; None values ignored)
win over both and, because the torch ROM configs read their params from
TEST_CASES, are written there before construction.
cfg is the merged dict the scripts run on: TIME_DEFAULTS < TEST_CASES[case] < overrides, plus case/module.
Source code in qlroms/intrusive_qlroms/build_ks.py
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 | |
get_full_trajectory(Ntot, model, i0=0)
Compute or load the full physical trajectory of a KS FOM.
Works for both 1D and 2D KS: model is the CONSTRUCTED dynamodels model
returned by build_fom (physical.KS or physical.KS2D). Generation runs
model.time_integrate in chunks; ks1d spectral history is converted to
physical snapshots via the model's own transforms. The cache file format
and layout are identical to the old torch-FOM pipeline -- a torch-saved
float64 tensor of POST-burn-in physical snapshots, (state_dim, Ncols), at
get_simulation_path(model)/full_trajectory.pth -- so existing caches hit.
Cache-hit rule unchanged: cached_cols >= Ntot -> last Ntot columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Ntot
|
int
|
Number of kept snapshots to return after discarding i0 transient steps. |
required |
model
|
dynamodels FOM (from build_fom). |
required | |
i0
|
int
|
Number of initial transient steps to skip (default 0). Only used when generating from scratch; the cache stores post-burn-in states. |
0
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Trajectory tensor with shape (state_dim, Ntot), float64, CPU. |
Source code in qlroms/intrusive_qlroms/build_ks.py
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 | |
load_case(module, case, Ntrain, Ntest)
Cached trajectory of one test case, split into a training and a test window.
module is ks1d or ks2d. The trajectory comes from get_full_trajectory (generated
and cached on first use), and traj keeps all of it for callers that need more than
the two windows.
Returns:
| Type | Description |
|---|---|
dict
|
dict with |
dict
|
2-D snapshot reshapes to, None in 1-D), |
dict
|
(N, Ntest) and |
Source code in qlroms/intrusive_qlroms/build_ks.py
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | |
qlroms.intrusive_qlroms.build_pinball
Pinball snapshot loading -- the build_ks analog.
Snapshots are written by PinballFOM.run() (scripts/pinball/run_pinball_fom.py) as
load_mass_matrix(kind='velocity', mesh_dir=None, n_dof=None)
The FEM mass matrix of the pinball's Taylor-Hood velocity ("velocity") or pressure
("pressure") space as a torch sparse tensor -- the inner product of the charts, the
clustering, the chart assignment and every error of the models built on the snapshots.
Read from the sidecar file written next to the mesh (aux_fenics.export_mass_matrices, run
by build_local_model, or run_pinball_fom.py --export-mass), so no dolfinx is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
"velocity" or "pressure". |
'velocity'
|
mesh_dir
|
directory of the mesh (default qlroms.utils.paths.PINBALL_MESHES). |
None
|
|
n_dof
|
int | None
|
expected dof count (e.g. the snapshot matrix's rows); a mesh that does not match the snapshots fails here instead of producing a wrong metric. |
None
|
Source code in qlroms/intrusive_qlroms/build_pinball.py
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 | |
load_snapshots(snapshot_dir=None, quantity='velocity', device=None, rdtype=torch.float64, start=0, stop=None)
Stack <quantity>_*.npy snapshots into an (Ndof, Nt) matrix, chronological -- only
the saved snapshots start .. stop-1 (default: all), and only those files are read, so a
window of a long campaign costs its own size, not the campaign's.
Returns:
| Type | Description |
|---|---|
(X, dt)
|
snapshot matrix and the saved-snapshot time spacing from |
float
|
snapshot_metadata.npz (dt * snapshot_interval). |
Source code in qlroms/intrusive_qlroms/build_pinball.py
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 | |
snapshot_time_index(snapshot_dir=None, quantity='velocity')
Monotone integer time index for a regression fit's time_index=..., one
entry per sorted <quantity>_*.npy file: +1 where consecutive files are exactly
snapshot_interval solver steps apart, +2 (a gap -> non-adjacent) anywhere the
step sequence is irregular. The fom_re130 warm-restart seam is the motivating
case: ..., 3996, 3998, 3999, 4001, ... -- the 3998 -> 3999 pair is half a
snapshot interval apart, so it must not become a regression pair.
Source code in qlroms/intrusive_qlroms/build_pinball.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
merge_restart_snapshot_metadata(snapshot_dir, previous_metadata)
Fold an exact restart leg's own snapshot_metadata.npz back into the campaign it continued, into one file describing the whole thing.
PinballFOM.run() clears its bookkeeping and overwrites snapshot_metadata.npz on every call (qlroms.intrusive_qlroms.pinball.solver.py's run()), so after init_velocity_prev-based exact restart (see PinballFOMParameters), the file on disk describes only the just-completed leg's own steps -- not the campaign as a whole. This merges it back with a saved copy of the metadata from before that leg ran, producing a single file indistinguishable from what one uninterrupted run of the combined step count would have written: saved_steps/saved_times concatenated and checked for a uniform, gap- and duplicate-free spacing (an exact restart's saved_times are already campaign-relative, not leg-local -- solver.py's run() seeds self.time from the restart's own step offset), everything else taken from the first leg except num_steps (summed).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snapshot_dir
|
the campaign directory (its CURRENT snapshot_metadata.npz is read as the just-completed leg and then overwritten with the merged result). |
required | |
previous_metadata
|
path to a copy of snapshot_metadata.npz taken before that leg's
run() call (its own overwrite would otherwise destroy the earlier legs' record);
run() leaves it as snapshot_metadata_before_ |
required |
Returns: the (now merged) snapshot_metadata.npz path.
Source code in qlroms/intrusive_qlroms/build_pinball.py
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 | |
build_sensor_indices(fom, n_sensors, sampling='random', seed=0)
Sensor dof indices into the stacked velocity state -- the build_ks analog.
The pinball state is an unstructured Taylor-Hood dof vector, so there is no
grid to lay sensors on: "random" (the qlROM-DA convention) draws sorted
velocity-dof indices, "uniform" strides the dof vector (index-uniform, not
space-uniform). fom is any case handle whose .N is the state dimension.
Source code in qlroms/intrusive_qlroms/build_pinball.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | |
campaign_name(reynolds, tag=None)
Snapshot-campaign name: fom_re140 untagged (the historical layout), fom_re140__
Source code in qlroms/intrusive_qlroms/build_pinball.py
176 177 178 179 180 181 182 183 | |
config_campaign(config)
Campaign name of a FOM config (configs/pinball_.yml): its fom.reynolds and top-level
tag (absent or null -> the untagged fom_re
Source code in qlroms/intrusive_qlroms/build_pinball.py
186 187 188 189 190 191 | |
parse_campaign(dataset)
(reynolds, tag) of a campaign name, the inverse of campaign_name: fom_re140 -> (140.0, None), fom_re140__long -> (140.0, "long").
Source code in qlroms/intrusive_qlroms/build_pinball.py
194 195 196 197 198 | |
load_intrusive_model(dataset, Xtrain, *, K, r_velocity, r_pressure, dt, random_state=1, build=False, cluster_space='pod_lossless', cluster_metric='mass', pressure_centroid='poisson_from_velocity_centroid', i0=0)
Intrusive qlGalerkin for a snapshot campaign, from the build_local_model cache.
The Reynolds number and the tag come from the campaign name (fom_re120 -> 120,
fom_re140__long -> 140 with its own cache files, see campaign_name). On a cache
miss the model is built only if build=True (dolfinx, about 1 h at K=8 on 6000
snapshots); otherwise this prints the build call and returns None, so notebooks
stay cheap. The pressure snapshots are read only when a build is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset
|
str
|
campaign name, e.g. "fom_re120" or "fom_re140__long". |
required |
Xtrain
|
(N, T) velocity training snapshots -- the window [i0, i0+T) of the
campaign's own snapshot sequence. Pass |
required | |
i0
|
int
|
index of Xtrain's first column within the campaign's own snapshot sequence (default 0, i.e. Xtrain starts at the beginning). |
0
|
Returns: the qlGalerkin, or None. Each chart has r_velocity + r_pressure + 1 velocity coordinates (POD modes plus one supremizer per pressure mode and pressure mean).
Source code in qlroms/intrusive_qlroms/build_pinball.py
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 | |