Architecture¶
LinStabPy is a Python CLI and library for linear stability and resolvent analysis of compressible viscous flows. It ports LinStab2D style linearized Navier-Stokes (LNSE) workflows from MATLAB to an MPI enabled HPC stack on PETSc and SLEPc, with sparse direct factorization (MUMPS by default, optional cuDSS).
This page describes the packages, how they connect, the main processes, and how a case runs. It follows the tree and the module notes in docs/architecture.md.
Conventions
Temporal eigenvalues are \(\omega = \mathrm{eig}(i L_0)\) with \(\exp(-i\omega t)\) time dependence. Unstable means \(\mathrm{Im}(\omega) > 0\). The shift-invert target lives in \(\omega\) space. The resolvent is \(H = C F (L_0 - (\gamma + i\omega) I)^{-1} F B\) with \(\sigma = \sqrt{\mathrm{Re}(\lambda)}\) from the weighted normal problem. SBP orders are \(2\), \(4\), \(6\), \(8\) only. The default shifted solve is KSPPREONLY with PCLU through MUMPS, driven by SLEPc Krylov-Schur with ST=SINVERT. Assembly order is fixed: raw blocks, inv(B/1j) scaling, sponge, dissipation, then BC rows last.
Architecture overview¶
Architecture design philosophy¶
LinStabPy is designed as a staged scientific pipeline, not a service mesh or multi-tenant application. The system starts from CFD mean-flow dumps and YAML case policy, and ends at spectra, modes, gains, metrics, and provenance payloads.
Core design tenets:
| Principle | Manifestation in the codebase |
|---|---|
| Staged pipeline | Driver coordinates discrete stages: config → baseflow → mesh → L0 → factorize → solve → payload, with stage timing and heartbeats |
| Factorize-once, solve-many | Shared linalg.factorize core underpins shift-invert eigenproblems and resolvent \(H\) applies |
| Config as single policy source | YAML + environment + defaults → schema validation → typed Config subsections |
| Lazy loading | PEP 562 __getattr__ on package, mesh, BC, linalg, stability, resolvent, and router so help/version paths stay light and do not initialize PETSc/MPI |
| SciPy and PETSc dual paths | Local CSC/CSR assembly is possible; production path converts to distributed PETSc Mat |
| Separation of concerns | Physics (coefficients/assembly) vs. numerics (mesh/SBP) vs. routing (cost-aware sweeps) |
| Reproducibility | Config hash, git SHA, metrics schema, fixed NPZ contract keys |
The architectural style is a modular layered scientific toolkit:
Core architecture patterns¶
- Pipeline and staged orchestration.
driver/run.pysequences the scientific stages in a fixed order. - Shared kernel with multiple products. One BC enforced LNSE operator family feeds temporal eigen, adjoint and sensitivity, spatial eigen, single frequency resolvent, and \(\beta\) and \(\omega\) maps.
- Strategy with backend registry. Factor backends (MUMPS, LU, cuDSS) are selected and probed under policy and RAM constraints.
- Facade with lazy public API.
import linstabpyexposes analysis entry points without initializing MPI or PETSc. - Declarative configuration and BC recipes. Case behavior comes from YAML and BC rules, not hardcoded geometry branches.
- Cost aware routing. Anchor factorization, preconditioner reuse, residual gates, break-even logic, and RAM autoselect turn single point resolvent into scalable campaigns.
Technology stack overview¶
| Layer | Choice |
|---|---|
| Language | Python ≥3.10, <3.14 |
| Local arrays / sparse | NumPy, SciPy (CSC/CSR) |
| Parallel runtime | MPI via mpi4py |
| Distributed linear algebra | PETSc (petsc4py), complex scalars required |
| Eigenproblems | SLEPc (slepc4py), EPS / weighted normal problems |
| Sparse direct solvers | MUMPS (default via PCLU); optional cuDSS (GPU shell PC) |
| Configuration | PyYAML; typed dataclasses + schema validation |
| Packaging | conda-forge environment + editable pip install |
| Outputs | NPZ result payloads; metrics.json + provenance |
Environment note: environment.yml targets conda-forge complex-scalar PETSc/SLEPc. GPU PETSc is not a supported conda path; cuDSS is an optional, non-conda path. Plain import linstabpy does not initialize PETSc or MPI.
High-level deployable mental model¶
flowchart TB
subgraph Entry
A1["CLI: linstabpy.driver.cli"]
A2["Library: import linstabpy"]
end
subgraph Pipeline["Staged HPC Pipeline"]
direction TB
P1["Config validate + typed policy"]
P2["Baseflow load + preprocess"]
P3["Mesh + SBP + filters"]
P4["Coefficients + L0 + BC"]
P5["Factorize shifted systems"]
P6a["Stability EPS"]
P6b["Resolvent H / gains"]
P6c["Router beta-omega map"]
P7["NPZ + metrics + provenance"]
P1 --> P2 --> P3 --> P4 --> P5
P5 --> P6a
P5 --> P6b
P4 --> P6c
P6c --> P6b
P6a --> P7
P6b --> P7
P6c --> P7
end
subgraph Backends
B1["PETSc Mat/Vec/KSP"]
B2["SLEPc EPS"]
B3["MUMPS / LU / cuDSS"]
end
A1 --> P1
A2 --> P4
A2 --> P6a
A2 --> P6b
A2 --> P6c
P5 --> B1
P5 --> B3
P6a --> B2
P6b --> B2
System context¶
Roles, scope, and external pieces are defined once in overview. The architecture-relevant cut is how the two entry points reach the runtime:
flowchart LR
subgraph Users
U1["Researchers / Engineers"]
U2["Library API consumers"]
end
subgraph Inputs
YAML["YAML case config"]
CFD["CFD baseflow npy/npz"]
end
subgraph LinStabPy
CLI["driver.cli / run"]
LIB["linstabpy public API"]
end
subgraph Runtime
MPI["MPI / mpi4py"]
PETSc["PETSc"]
SLEPc["SLEPc"]
MUMPS["MUMPS"]
cuDSS["cuDSS optional"]
NP["NumPy / SciPy"]
end
subgraph Outputs
NPZ["Results NPZ"]
MET["metrics.json + provenance"]
end
U1 --> CLI
U2 --> LIB
YAML --> CLI
CFD --> CLI
CLI --> MPI
CLI --> PETSc
CLI --> SLEPc
PETSc --> MUMPS
PETSc --> cuDSS
CLI --> NP
LIB --> PETSc
CLI --> NPZ
CLI --> MET
The CLI needs the full stack: MPI with PETSc and SLEPc plus MUMPS underneath, SciPy for local assembly, YAML and base-flow files in, NPZ results with metrics and provenance out. The library needs only NumPy and SciPy until a solve starts; PETSc enters lazily with the first distributed matrix or factorize call. Optional cuDSS factors ride as a PETSc preconditioner shell wherever the backend policy selects them. Solver vocabulary on this page is defined in the solver glossary.
Container view¶
Packages here are cohesive Python modules with a runtime and data contract. They run in one MPI process group as a single Python application.
Domain module division¶
| Domain | Type | Code paths | Role |
|---|---|---|---|
| Case Orchestration | Core | driver/, __init__.py |
PETSc/MPI init, staged runs, CLI, payload, public lazy API |
| Configuration Management | Supporting | config/ |
Merge/validate YAML·env·defaults; typed solver policy |
| Baseflow Management | Core | baseflow/ |
Mean-flow ingest, sponge, shock cleanup, viscosity |
| Mesh Discretization | Core | mesh/ |
Geometry, SBP ops, filters, deformation, masking |
| LNSE Operator Assembly | Core | operators/, boundary_conditions/ |
Coefficients, L0/spatial assembly, BC recipes, Chu norm |
| Stability Analysis | Core | stability/, solvers/eigenvalue.py, spatial_eig.py |
Temporal/adjoint/spatial EPS + sensitivity |
| Resolvent Analysis | Core | resolvent/ |
H operator, weighted gains, forcing models |
| Solver Routing | Core | solvers/router/ |
\(\beta\)/\(\omega\) maps, anchors, residual gates, RAM autoselect |
| Linear Algebra | Infrastructure | linalg/, mumps_options.py |
Factor backends, shifted ops, cuDSS |
| Shared Utilities | Infrastructure | utils/, metrics_schema.py |
PETSc bridge, YAML, cost, provenance, Floquet |
Domain module architecture¶
flowchart TB
subgraph Orchestration["Case Orchestration"]
CLI["cli.py"]
RUN["run.py"]
PRE["preflight.py"]
PAY["payload.py / io.py"]
end
subgraph Config["Configuration Management"]
SCH["schema.py"]
TYP["typed.py"]
DEF["defaults.py"]
end
subgraph PhysicsPrep["Physics Preparation"]
BF["baseflow"]
MESH["mesh + SBP"]
end
subgraph Operators["LNSE Operator Assembly"]
COEF["coefficients"]
ASM["assembly"]
SPAT["spatial"]
BC["boundary_conditions"]
EN["energy_norm"]
end
subgraph Analysis["Analysis Domains"]
STAB["stability"]
RES["resolvent"]
RTR["solvers/router"]
end
subgraph Solvers["Solver Backends"]
EIG["solvers/eigenvalue"]
SEIG["solvers/spatial_eig"]
end
subgraph LA["Linear Algebra Infrastructure"]
FAC["factorize"]
BE["backends"]
SH["shifted"]
CU["cudss_factor"]
end
subgraph Utils["Shared Utilities"]
PC["petsc_core / petsc_init"]
YIO["yaml_io"]
PROV["provenance / cost_metrics"]
end
CLI --> RUN
RUN --> PRE
RUN --> Config
RUN --> BF
RUN --> MESH
RUN --> ASM
RUN --> STAB
RUN --> RES
RUN --> RTR
RUN --> PAY
ASM --> COEF
ASM --> BC
ASM --> MESH
ASM --> BF
SPAT --> COEF
STAB --> EIG
STAB --> SEIG
STAB --> FAC
RES --> FAC
RTR --> RES
RTR --> FAC
RTR --> ASM
FAC --> BE
FAC --> PC
EIG --> FAC
Storage design¶
LinStabPy is file-oriented, not database-backed.
| Store | Form | Contract |
|---|---|---|
| Case policy | YAML on disk / env overrides | Merged into typed Config; schema-validated |
| Mean flow | npy directory or npz | Primitives RHO,U,V,W,T on (ny,nx); optional sponge |
| Runtime operators | In-memory SciPy CSC or PETSc Mat |
Sparse \(5 N_{gp} \times 5 N_{gp}\) (or reduced free DOFs) |
| Results | NPZ | Fixed key contract in driver.payload |
| Metrics | JSON | metrics_schema.SCHEMA: provenance, problem, cost, quality |
| Provenance | Embedded in metrics / payload | Config hash/MD5, git SHA, hostname, factor backend/precision |
There is no persistent application database. Reproducibility is achieved through config hashing, schema versioning, and immutable result contracts rather than transactional storage.
Inter-domain module communication¶
Communication is in-process Python calls (service calls, composition, and data hand-off of arrays/matrices), not network RPC.
flowchart LR
CO["Case Orchestration"]
CM["Configuration"]
BM["Baseflow"]
MD["Mesh"]
OA["LNSE Assembly + BC"]
SA["Stability"]
RA["Resolvent"]
RR["Router"]
LI["Linear Algebra"]
UT["Utilities"]
CO -->|"config dependency"| CM
CO -->|"data"| BM
CO -->|"service"| SA
CO -->|"service"| RA
CO -->|"service"| RR
CO --> OA
CO --> LI
OA -->|"composition"| MD
OA -->|"data"| BM
OA --> LI
SA -->|"service"| LI
SA -->|"data"| OA
RA -->|"service"| LI
RA -->|"data"| OA
RR -->|"service"| RA
RR -->|"service"| LI
RR -->|"data"| OA
BM -->|"data"| MD
MD --> UT
SA --> UT
RA --> UT
LI --> UT
Strength highlights
- Orchestration ↔ Stability/Resolvent: primary service calls
- Assembly ↔ Mesh / Baseflow: composition and data (~strongest physics coupling)
- Stability / Resolvent / Router ↔ Factorization: shared factorize-once spine
Boundary-conditions note: The boundary_conditions/ package is physically separate but logically part of LNSE Operator Assembly; BC application is the last non-interchangeable step of operator finalization.
Component view¶
Project structure¶
src/linstabpy/
├── __init__.py # Lazy public API (no PETSc on import)
├── metrics_schema.py # Run metrics JSON contract
├── mumps_options.py # MUMPS option helpers
├── driver/ # CLI + case pipeline
├── config/ # Schema, typed Config, defaults
├── baseflow/ # BaseFlow + I/O + preprocess
├── mesh/ # Mesh, SBP, filters, FD weights
├── operators/ # LNSE assembly, coefficients, spatial, norms
├── boundary_conditions/ # Dirichlet/Neumann recipes
├── stability/ # Eigenvalue services, acceptance, sensitivity
├── resolvent/ # Operator, gain, analysis
├── solvers/ # EPS backends + router/
├── linalg/ # factorize, backends, shifted, cuDSS
└── utils/ # PETSc init/core, YAML, metrics, provenance
Public library surface (lazy exports from linstabpy.__init__):
build_lnse_system,build_lnse_operatorcompute_chu_normtemporal_eigenvalues,adjoint_eigenvalueswavemakerresolvent_analysis,resolvent_mapBaseFlow
CLI entry: linstabpy.driver.cli.main → lazy import of driver.run (so --version / --help avoid PETSc).
Core functional components¶
Case orchestration (driver)¶
| Component | Responsibility |
|---|---|
cli.py |
Argparse; cache check (config hash / git SHA); force-recompute env |
run.py |
Full pipeline: init PETSc/MPI, load config/fields, mesh, assemble L0, stability/resolvent stages, payload |
preflight.py |
Non-blocking semantic advisories (dissipation vs mesh coupling, freestream scale, etc.) |
payload.py / io.py |
NPZ contract keys; save/load results |
Notable runtime behaviors:
- Stage wall-time and RSS delta logging; optional stage heartbeats
- Env knobs:
LINSTABPY_CASE_CONFIG,LINSTABPY_CASE_OUTPUT_NPZ,LINSTABPY_FORCE_RECOMPUTE, … - Alpha-filter treated as an operator-level property (not stage-dependent)
Configuration (config)¶
| Component | Responsibility |
|---|---|
schema.py |
Lenient allowed-key validation + FD-order checks → ConfigValidationError |
typed.py |
Dataclasses: Config, EigenCfg, LinSolveCfg, ResolventCfg, SpatialCfg, MeshCfg, MpiCfg |
defaults.py |
Dissipation and other apply defaults |
Resolution order: YAML case → environment → defaults.
Baseflow (baseflow)¶
| Component | Responsibility |
|---|---|
BaseFlow |
(ny, nx) fields RHO,U,V,W,T + Re,Ma,Pr,kappa,T_0; viscosity via sutherland_air |
grid_fields |
npy directory / npz load |
| preprocess / sponge / shock_cleanup / viscosity | Readiness for linearization |
Mesh (mesh)¶
| Component | Responsibility |
|---|---|
Mesh |
Mutable runtime carrier; views MeshGeometry, MeshOperators, MeshRuntimeState |
SBP (sbp.py, sbp_generator.py, fdweights.py) |
Differentiation, norms, dissipation, Mohseni–Colonius pole treatments |
filters |
Mattsson-style stabilization attached to mesh runtime |
PETSc-dependent mesh objects are lazy-imported.
LNSE operator assembly (operators + boundary_conditions)¶
Canonical PETSc assembly order (documented in assembly.py; non-interchangeable):
- Assemble raw block operator from coefficients + FD stencils
- Apply analytical
inv(B/1j)left scaling → L0 - Add sponge damping
- Add artificial dissipation
- Last: apply BC recipe (Dirichlet/Neumann); free-DOF sets
State vector: q = [ρ, u, v, w, T].
Supports Cartesian and cylindrical coefficient paths; SciPy CSC and PETSc Mat backends.
| Sub-module | Role |
|---|---|
| Temporal Operator Assembly | Large sparse L0 for temporal/resolvent paths |
| Physical Coefficients | LNSE coeffs, metrics, Chu energy-norm weights |
| Spatial Operator Assembly | Spatial block operators reusing temporal helpers |
| Boundary Condition Recipes | Declarative Dirichlet/Neumann rules and free-DOF masks |
Stability (stability + solvers)¶
| Sub-module | Role |
|---|---|
| Stability Eigenvalue Services | temporal_eigenvalues / adjoint_eigenvalues / spatial entry; acceptance |
| Eigen Solver Backends | SLEPc EPS shift-invert; residual reorder; conjugate-pair completeness |
| Structural Sensitivity | wavemaker from direct + adjoint modes |
Resolvent (resolvent)¶
- Factorize shifted operator once; reuse for forward/adjoint applies
- Weighted normal operator shell for SLEPc; gains \(\sigma = \sqrt{\mathrm{Re}(\lambda)}\)
- Optional conservative momentum forcing maps
Solver router (solvers/router)¶
| Piece | Role |
|---|---|
resolvent_map |
Per-\(\beta\) L0 assembly; schedule frequency lines |
band_marking |
Label STABLE / SMOOTH / NEAR_POLE bands |
map_walk / anchors |
Real factor at anchors; later \(\omega\) uses factor as PC; residual gate → refactor |
stable_sweep |
Stable-regime sweep strategies |
recycling / break_even |
Cost-aware reuse decisions |
ram_autoselect |
Backend choice under memory policy |
certification |
Consistency / RSVD certification hooks |
Config nuance: Router dissipation strength is read from top-level dissipation_epsilon, while non-router paths may use nested operator_dissipation.epsilon. Both enable flags and strength must be set or dissipation is a silent no-op.
Technical support components¶
Linear algebra (linalg)¶
| Component | Responsibility |
|---|---|
factorize / Factorization |
solve, transpose solve, block solve, refactorize, destroy |
backends |
Normalize/probe availability, precision, MUMPS BLR/ordering |
shifted_operator |
Build \(L_0 - i\omega I\) and related shifts |
cudss_factor |
Optional GPU factor as PETSc PC shell |
Default path: KSPPREONLY + PCLU + MUMPS.
Shared utilities (utils + metrics_schema)¶
| Component | Responsibility |
|---|---|
petsc_init / petsc_core |
Init ordering (mpi4py before PETSc; thread level MULTIPLE when needed); SciPy↔PETSc conversion |
| YAML I/O, cost metrics, provenance | Cross-cutting reproducibility and ops telemetry |
| Floquet helpers | Supporting periodic-flow utilities |
metrics_schema |
Versioned metrics JSON groups |
Component responsibility division (summary)¶
flowchart TB
subgraph CoreBusiness
ORCH["Orchestration: CLI run payload"]
BF["Baseflow: ingest preprocess"]
MESH["Mesh: SBP filters geometry"]
OP["Operators+BC: L0 spatial norms"]
ST["Stability: temporal adjoint spatial wavemaker"]
RS["Resolvent: H gains modes"]
RT["Router: beta-omega maps anchors"]
end
subgraph Infra
CFG["Config: schema typed defaults"]
LA["Linalg: factorize backends shifted"]
UT["Utils: PETSc bridge metrics provenance"]
end
ORCH --> CFG
ORCH --> BF
ORCH --> MESH
ORCH --> OP
ORCH --> ST
ORCH --> RS
ORCH --> RT
OP --> BF
OP --> MESH
ST --> OP
ST --> LA
RS --> OP
RS --> LA
RT --> OP
RT --> RS
RT --> LA
LA --> UT
MESH --> UT
ST --> UT
RS --> UT
Component interaction relationships¶
- Library consumers may call assembly and analysis APIs directly, bypassing full CLI orchestration, but still depend on the same operator and factorization contracts.
- Router is the only domain that systematically re-assembles per \(\beta\) and walks many \(\omega\) with measured cost feedback.
- Stability and Resolvent never own mesh generation; they consume finalized operators and energy weights.
- Utilities never own case flow; they underpin conversion, init, and telemetry.
Key processes¶
Core functional processes¶
Temporal eigenvalue stability analysis (primary product path)¶
End-to-end hydrodynamic stability for compressible flows: load mean flow, assemble LNSE + BCs, factorize shifted systems, compute temporal (and optionally adjoint) eigenvalues/eigenvectors.
flowchart TD
Start["Case Launch"] --> InitRuntime["Initialize PETSc and MPI"]
InitRuntime --> ResolveConfig["Merge and Validate YAML Config"]
ResolveConfig --> LoadBaseflow["Load CFD Mean-Flow Fields"]
LoadBaseflow --> PreprocessBF["Preprocess Sponge Viscosity Shock Cleanup"]
PreprocessBF --> BuildMesh["Build Structured Mesh and SBP Operators"]
BuildMesh --> GenCoeffs["Compute LNSE Physical Coefficients"]
GenCoeffs --> AssembleL0["Assemble Sparse L0 Operator"]
AssembleL0 --> ApplyBCs["Apply Dirichlet Neumann BC Recipes"]
ApplyBCs --> FactorShift["Factorize Shifted LNSE System"]
FactorShift --> EigenSolve["SLEPc Temporal Adjoint Eigen Solve"]
EigenSolve --> ValidateModes["Validate Modes Residuals Weights"]
ValidateModes --> EmitPayload["Emit Spectra Modes Metrics Provenance"]
EmitPayload --> End["Case Complete"]
sequenceDiagram
participant CLI as driver_cli
participant RUN as driver_run
participant CFG as config
participant BF as baseflow
participant M as mesh
participant OP as operators_BC
participant LA as linalg
participant ST as stability_solvers
participant OUT as payload_io
CLI->>RUN: run case
RUN->>CFG: load validate typed Config
RUN->>BF: load_case_fields preprocess viscosity
RUN->>M: Mesh SBP filters
RUN->>OP: coefficients L0 BC recipe
RUN->>LA: factorize shifted L0
RUN->>ST: SLEPc temporal adjoint EPS
ST->>LA: repeated solves
RUN->>OUT: eigenvalues modes metrics provenance
Key steps
- Runtime initialization, PETSc/MPI, logging, distributed orchestration
- Configuration resolution, YAML/env/defaults; eigen/mesh/MPI policy
- Baseflow ingest, npy/npz →
BaseFlow+ preprocess/transport - Mesh discretization, structured mesh, SBP (2, 4, 6, 8), filters, runtime state
- Coefficient generation, Cartesian or cylindrical compressible LNSE coeffs
- Operator assembly and BCs, sparse L0 for \(q=[\rho,u,v,w,T]\); free DOFs
- Shift-invert factorization, MUMPS/LU/cuDSS factorize-once
- Eigenanalysis, SLEPc EPS; residuals; optional structural sensitivity
- Result emission, NPZ + metrics + provenance
Resolvent gain analysis (single frequency)¶
Shares prep through L0, then specializes in \((L_0 - i\omega I)\) factors and weighted gain extraction.
flowchart TD
Start["Resolvent Case Start"] --> InitRuntime["Initialize Runtime"]
InitRuntime --> ResolveCfg["Resolve Frequency Weights Backend Policy"]
ResolveCfg --> PrepMean["Provide Mesh-Aligned BaseFlow"]
PrepMean --> SBPOps["Supply SBP Diff and Norm Operators"]
SBPOps --> AssembleL0["Assemble L0 with BCs and Energy Maps"]
AssembleL0 --> FactorOmega["Factor L0 minus i omega I"]
FactorOmega --> BuildH["Build Resolvent H and H Hermitian Actions"]
BuildH --> WeightedShell["Form Weighted Normal Operator Shell"]
WeightedShell --> GainEPS["SLEPc Extract Gains and Mode Pairs"]
GainEPS --> ResidualCheck["Validate Residuals"]
ResidualCheck --> PackageOut["Package Gains Modes Metrics"]
PackageOut --> End["Resolvent Complete"]
Beta–frequency resolvent gain map (cost-critical production path)¶
flowchart TD
Start["Map Campaign Launch"] --> ValidateMap["Validate Map Router Resource Policy"]
ValidateMap --> LoopBeta["For Each Spanwise Beta"]
LoopBeta --> AssembleBeta["Assemble LNSE Operator for Beta"]
AssembleBeta --> MarkBands["Mark Frequency Bands"]
MarkBands --> ScheduleWalk["Schedule Frequency Line Walk"]
ScheduleWalk --> AnchorFactor["Factor Measured Anchors"]
AnchorFactor --> WalkFreq["Walk Frequencies with PC Reuse"]
WalkFreq --> ResidualGate{"Residual Gate OK?"}
ResidualGate -->|Yes| ComputeGain["Compute Leading Resolvent Gain"]
ResidualGate -->|No| Refactor["Refactor Anchor and Continue"]
Refactor --> ComputeGain
ComputeGain --> MoreFreq{"More Frequencies?"}
MoreFreq -->|Yes| WalkFreq
MoreFreq -->|No| MoreBeta{"More Betas?"}
MoreBeta -->|Yes| LoopBeta
MoreBeta -->|No| RecordMap["Record Map Cost Samples Certification"]
RecordMap --> End["Map Complete"]
Spatial stability eigenproblem¶
Config solver.spatial (fallback solver.eigen) → spatial block operators → generalized SLEPc EPS.
flowchart TD
Start["Spatial Eigen Start"] --> ResolveSpatial["Resolve solver.spatial Policy"]
ResolveSpatial --> SupplyBF["Supply Mean-Flow Fields"]
SupplyBF --> MeshIndex["Provide Mesh Indexing and SBP"]
MeshIndex --> SpatialOps["Assemble Spatial Block Operators"]
SpatialOps --> FactorBackend["Configure Shift-Invert Factor Backend"]
FactorBackend --> SpatialEPS["Run Generalized SLEPc Spatial EPS"]
SpatialEPS --> ReturnModes["Return Spatial Modes and Metadata"]
ReturnModes --> End["Spatial Eigen Complete"]
Baseflow ingest and operator prep (shared foundation)¶
flowchart TD
Start["Prep Stage Start"] --> SchemaCheck["Validate Case Schema and FD Orders"]
SchemaCheck --> LoadFields["Ingest npy or npz Primitives"]
LoadFields --> Preprocess["Shock Cleanup Sponge Viscosity Fill"]
Preprocess --> MeshCreate["Create or Deform Mesh Attach SBP Filters"]
MeshCreate --> Coeffs["Generate LNSE Coefficients"]
Coeffs --> BCRecipe["Apply BC Recipes Free-DOF Sets"]
BCRecipe --> FinalizeL0["Finalize Sparse L0 for Downstream Solves"]
FinalizeL0 --> End["Operator Ready"]
Adjoint and structural sensitivity (supporting path)¶
flowchart TD
Start["Direct Modes Available"] --> AdjointSolve["Run Adjoint Eigen Solve"]
AdjointSolve --> CombineModes["Combine Direct and Adjoint Modes"]
CombineModes --> Sensitivity["Compute Structural Sensitivity"]
Sensitivity --> EmitSens["Emit Sensitivity Fields and Metadata"]
EmitSens --> End["Sensitivity Complete"]
Technical processing workflows¶
| Concern | Mechanism |
|---|---|
| PETSc init ordering | mpi4py before PETSc; complex scalars mandatory for production |
| Operator finalization | Fixed order: raw blocks → inv(B) scale → sponge → dissipation → BC last |
| Shift construction | linalg.shifted builds \(L_0 - i\omega I\) (and related) once per target |
| Eigen drive | SLEPc EPS shift-invert using factorization as PC/solve backend |
| Resolvent drive | MatShell weighted normal operator; repeated H / Hᴴ applies via factors |
| Map routing | Band mark → anchors → PC reuse → residual gate → optional stable sweep |
| Backend choice | Probe + RAM autoselect among MUMPS/LU/cuDSS |
Data flow paths¶
YAML + npy/npz
→ Config (typed) + BaseFlow (ny,nx fields)
→ Mesh (X,Y, Dx,Dy, W, filters)
→ Coefficients + Energy weights (Chu W_diag / invW_diag)
→ L0 PETSc Mat + free-DOF IS + BC-enforced rows
→ Factorization handle(s)
→ Spectra / gains / modes (distributed Vec → gathered arrays)
→ NPZ payload + metrics.json (provenance, cost, quality)
Key data objects
| Object | Contract |
|---|---|
BaseFlow |
Parameters + (ny,nx) primitives; optional MU / dμ/dT |
Mesh / operators |
X,Y, SBP Dx,Dy,…, weights W, optional dissipation |
LNSESystem / L0 |
Sparse \(5 N_{gp} \times 5 N_{gp}\) (or reduced free DOFs) |
| BC recipe | Rules → enforced rows + free-DOF index sets |
| Energy weights | Chu norm diagonals W_diag / invW_diag |
| Runtime NPZ | Fixed keys in driver.payload |
| Metrics JSON | provenance, problem, cost, quality groups |
Exception handling and quality gates¶
| Gate | Behavior |
|---|---|
| Schema validation | Unknown top-level keys / invalid FD orders → ConfigValidationError (fail fast before HPC cost) |
| Preflight advisories | Non-blocking warnings (dissipation vs mesh coupling, freestream scale) |
| Residual gates (eigen) | Mode residual checks, target distance, conjugate-pair completeness |
| Residual gates (resolvent/map) | Force refactor when PC-reuse accuracy degrades |
| Acceptance services | stability/acceptance.py post-checks before payload commit |
| Certification hooks | Router consistency / RSVD-style certification metadata |
| Cache / recompute | CLI config-hash and git-SHA cache checks; LINSTABPY_FORCE_RECOMPUTE override |
There is no distributed saga or compensating transaction model: failures abort the stage with logged metrics where available; partial map results may still be recorded depending on campaign progress.
Technical implementation¶
Core module implementation¶
Orchestration hub¶
driver/run.py is a large (~1.8k line) façade that owns stage sequencing, timing, and payload emission. This concentration is intentional for scientific reproducibility (one place defines "a case") but is an architectural pressure point for change risk. Library users can bypass it by calling domain APIs directly.
Operator assembly¶
Assembly is the mathematical heart of the system:
- Coefficients encode compressible LNSE physics (Cartesian/cylindrical).
- SBP stencils supply discrete derivatives and norms with summation-by-parts stability properties.
- Left scaling by analytical
inv(B/1j)produces the temporal operator L0 used by eigen and resolvent paths. - Sponge and artificial dissipation are operator-level stabilizations.
- BC recipes mutate rows/columns and publish free-DOF sets last, so solvers never see unconstrained boundary rows.
Factorization layer¶
Factorization is the performance spine:
- Default: KSPPREONLY + PCLU + MUMPS
- API: solve, transpose solve, block solve, refactorize, destroy
- Optional cuDSS as a PETSc PC shell for GPU-capable deployments
- Shifted systems built once per \((\beta,\omega)\) (or eigen shift) and reused across SLEPc iterations and resolvent matvecs
Analysis products¶
- Stability: SLEPc EPS on shifted L0; adjoint path; wavemaker from direct×adjoint
- Resolvent: \(H = C F (L_0 - (\gamma + i\omega) I)^{-1} F B\); weighted normal eigenproblem; \(\sigma = \sqrt{\mathrm{Re}(\lambda)}\)
- Router: per-\(\beta\) assembly + frequency line walks with measured anchors and residual-gated reuse
Key algorithm design¶
| Algorithm area | Design choice | Rationale |
|---|---|---|
| Spatial discretization | SBP FD orders 2, 4, 6, 8 + Mattsson-style filters | Energy-stable structured discretization; MATLAB LinStab2D parity |
| Axisymmetric poles | Mohseni–Colonius treatments | Correct pole behavior on cylindrical grids |
| Temporal eigen | Shift-invert via direct factors | Dense spectral regions near targets; robust on stiff LNSE |
| Resolvent gains | Weighted normal MatShell + SLEPc | Avoid forming dense H; reuse factors for H and Hᴴ |
| Map campaigns | Anchor factors as PC + residual gate | Amortize factorization across nearby \(\omega\); protect accuracy |
| Backend policy | RAM autoselect + MUMPS BLR options | Fit large free-DOF systems into node memory |
| Energy inner product | Chu norm diagonals | Physically meaningful compressible disturbance energy |
Data structure design¶
| Structure | Representation | Notes |
|---|---|---|
| Mean-flow fields | NumPy (ny, nx) arrays on BaseFlow |
Mesh-aligned primitives |
| Mesh operators | Sparse differentiation/norm matrices | Attached to Mesh runtime views |
| L0 / spatial blocks | SciPy CSC or PETSc Mat |
Production path is PETSc distributed |
| Free DOFs | PETSc index sets / masks | After BC enforcement |
| Factors | Opaque Factorization handle |
Owns KSP/PC lifecycle |
| Modes / gains | Distributed Vec → NumPy in payload |
Gathered for NPZ contract |
| Map records | Structured arrays + cost samples | Router recording + certification |
State dimension scales as 5 degrees of freedom per grid point (density, three velocities, temperature), reduced by Dirichlet eliminations.
Performance optimization strategies¶
- Factorize-once, solve-many, Dominant cost is sparse direct factorization; eigen iterations and resolvent applies reuse factors.
- Anchor + PC reuse on maps, Pay full factor cost at anchors; nearby frequencies reuse as right preconditioners until residual gate trips.
- Break-even / recycling logic, Router estimates when refactor is cheaper than struggling iterative reuse.
- RAM autoselect, Choose MUMPS/LU/cuDSS under memory policy; MUMPS BLR compression where available.
- Lazy imports, Keep CLI help and library import off the PETSc critical path.
- Stage metrics, Wall time and RSS deltas identify which stage dominates (assembly vs factor vs EPS).
- Optional parallelism across \(\beta\), Conceptual extension: parallelize independent \(\beta\) lines when memory allows; keep per-line \(\omega\) walks sequential for factor reuse.
- SciPy local path, Useful for small debugging assemblies without full MPI cost.
Primary bottlenecks
- Sparse direct factor memory and time (grows superlinearly with free DOFs)
- Per-\(\beta\) reassembly on large maps
- Monolithic
run.pyand large assembly/coefficient modules (developer throughput, not FLOPs) - Dual dissipation config paths (silent no-op risk → wasted or wrong HPC runs)
Scalability design¶
| Axis | Strategy |
|---|---|
| Grid size | MPI-distributed PETSc matrices; direct sparse solvers |
| Parameter space (\(\beta,\omega\)) | Router map campaigns with cost-aware reuse |
| Analysis modes | Same L0 family → many products without separate physics stacks |
| Hardware | CPU complex PETSc/MUMPS default; optional cuDSS GPU factors |
| Embedding | Library API for pipeline integration; CLI for batch cases |
| Extension points | New BC recipes; new factor backends via registry; new router band strategies; spatial/temporal coefficient models |
Security design¶
LinStabPy is a trusted scientific HPC toolkit, not a multi-tenant service. Security posture is appropriate to that model:
| Concern | Approach |
|---|---|
| Multi-tenant auth / isolation | Out of scope, runs under cluster user accounts and MPI job schedulers |
| Input trust | YAML and npy/npz are trusted researcher inputs; schema validation rejects unknown keys and bad FD orders but is not a sandbox |
| Code execution | No dynamic code execution from case files beyond configured numeric policy |
| Secrets | No cloud credentials subsystem; provenance records host/git/config hash, not secrets |
| Supply chain | conda-forge PETSc/SLEPc pins; editable package install in controlled envs |
| Result integrity | Config hash, git SHA, metrics schema, residual/certification gates support scientific integrity more than adversarial security |
Hardening guidance for operators: run inside standard HPC job isolation; treat case directories as trusted; do not expose the CLI as an unauthenticated network service.
Deployment architecture¶
Runtime environment requirements¶
| Requirement | Detail |
|---|---|
| Python | ≥3.10, <3.14 |
| MPI | Working MPI + mpi4py |
| PETSc/SLEPc | Complex scalar builds via petsc4py/slepc4py |
| MUMPS | Default sparse-direct backend through PETSc |
| NumPy/SciPy/PyYAML | Local numerics and config |
| Optional cuDSS | GPU sparse direct; not the supported conda default path |
| Resources | Memory-bound for large free-DOF factors; multi-rank for distributed Mat |
Init constraints
- Initialize
mpi4pybefore PETSc - Prefer thread level MULTIPLE when backends require it
- Do not expect
import linstabpyalone to set up the parallel runtime
Deployment topology structure¶
flowchart TB
subgraph UserSpace["User / CI workspace"]
YAML["Case YAML"]
DATA["Baseflow npy or npz"]
OUT["NPZ + metrics.json"]
end
subgraph Launch["Launch layer"]
SCHED["HPC scheduler mpirun sbatch"]
CLI["linstabpy.driver.cli"]
end
subgraph Ranks["MPI process group"]
R0["Rank 0 orchestration logging"]
R1["Rank 1 PETSc ownership"]
Rn["Rank N distributed Mat Vec"]
end
subgraph Native["Native solver stack"]
PETSc["PETSc"]
SLEPc["SLEPc"]
MUMPS["MUMPS"]
GPU["Optional cuDSS"]
end
YAML --> CLI
DATA --> CLI
SCHED --> CLI
CLI --> R0
CLI --> R1
CLI --> Rn
R0 --> PETSc
R1 --> PETSc
Rn --> PETSc
PETSc --> MUMPS
PETSc --> GPU
PETSc --> SLEPc
R0 --> OUT
Typical deployment unit: one MPI job = one case (or one map campaign). There is no long-running application server.
Library embedding topology: parent Python process imports linstabpy, explicitly initializes PETSc/MPI when needed, and calls public analysis APIs; I/O may be owned by the parent pipeline.
Scalability in deployment¶
- Strong scaling: PETSc distribution of sparse operators and vectors across ranks
- Parameter scaling: router \(\beta\) loops and frequency walks; future multi-\(\beta\) parallelism where memory permits
- Vertical scaling: larger-memory nodes for MUMPS factors; BLR compression; optional GPU factors
- Throughput scaling: many independent cases via scheduler array jobs (embarrassingly parallel across cases)
Monitoring and operations¶
| Signal | Source | Use |
|---|---|---|
| Stage wall time | Driver stage logging | Find slow prep vs factor vs EPS |
| RSS / peak memory | Cost metrics | Backend and mesh sizing |
| Optional GPU memory | Metrics when enabled | cuDSS capacity planning |
| Residuals / acceptance | Stability & resolvent validators | Scientific quality gate |
| Map cost samples | Router recording | Tune anchors and break-even |
| Provenance | Config hash, git SHA, hostname, backend/precision | Reproduce and audit runs |
| Preflight advisories | preflight.py |
Catch policy/mesh mismatches early |
| Heartbeats | Optional stage heartbeats | Long-running map health on HPC |
Operations guidance
- Validate YAML with schema before large allocations.
- Start with single-frequency or single-shift cases before full \(\beta\)–\(\omega\) maps.
- Watch factor memory first; adjust ranks, BLR, or backend via RAM policy.
- Ensure dissipation enable + strength are set consistently (router top-level vs nested operator paths).
- Archive NPZ +
metrics.jsontogether for provenance. - Use config-hash caching and
LINSTABPY_FORCE_RECOMPUTEdeliberately in CI or parameter sweeps.
Development guidance¶
| Audience | Guidance |
|---|---|
| New contributors | Learn the stage order and assembly order first; treat BC-last and factorize-once as invariants |
| Physics developers | Extend operators/coefficients and BC recipes; keep SciPy and PETSc paths aligned |
| Solver developers | Extend linalg/backends and router walk strategies; never bypass residual gates silently |
| API consumers | Prefer lazy public exports; initialize PETSc only on real solve paths |
| Maintainers | Consider splitting run.py into stage modules behind one façade; unify dissipation config; publish short ADRs for assembly order, factor policy, and router gates |
Architecture risks and recommendations¶
| Item | Severity | Notes / recommendation |
|---|---|---|
| No checked-in ADRs | Medium | Publish ADRs for assembly ordering, factor backend policy, residual gates, dissipation single source of truth |
Monolithic driver/run.py |
Medium | Extract prep / assemble / stability / resolvent / emit stages; keep one façade |
| Large assembly / coefficients / router modules | Medium | Add internal seams without breaking public contracts |
| Dual dissipation config paths | Medium | Unify nested fields; keep aliases with deprecation warnings |
| Typed config still migrating | Info | Consolidate typed.py with utils._solver_cfg dual resolution |
| Spatial path less CLI-central | Low | Document spatial and map entry points beside lazy public API |
| GPU path optional / non-conda | Info | Keep CPU complex PETSc as supported default; document cuDSS clearly |
Summary¶
LinStabPy implements a clean, domain-aligned LNSE analysis architecture:
- YAML-driven Case Orchestration feeds mesh-aligned Baseflow into SBP Mesh discretization.
- LNSE Operator Assembly (with declarative BC recipes) produces a single family of BC-enforced operators.
- A shared Linear Algebra factorization layer underpins Stability, Resolvent, and cost-aware Router campaigns on PETSc/SLEPc.
- Package boundaries match scientific responsibilities; external CFD generation, GUIs, and unstructured 3D frameworks remain firmly out of scope.
The main architectural pressures are orchestration monolith size, dual config paths for dissipation/solver policy, and missing formal ADRs, not a mismatch between intended domains and the codebase layout.
LinStabPy, linear stability and resolvent analysis of compressible flows.