Baseflow management¶
The base flow is the steady compressible solution you linearize about. This package loads it from npy or npz files, cleans it up (sponge layers, shock treatment, viscosity from Sutherland), and hands mesh aligned (ny, nx) arrays to assembly. Fields are nondimensional primitives with freestream density, velocity, and temperature equal to 1. If spectra look wrong, start here: a misaligned or uncleaned base flow propagates directly into every eigenvalue and gain.
Solver objects (KSP, PC, EPS, ST, MatShell) are defined in the solver glossary.
Overview¶
Baseflow management turns on-disk CFD mean-flow dumps into a mesh-aligned BaseFlow object that LNSE coefficient generation and operator assembly can consume.
The domain owns:
- Primitive field I/O (
npydirectories or a singlenpzarchive) - Optional preprocess (shock cleanup, sponge scaling/boundary ramps, binomial filtering)
- Structured sponge generation (presets and mesh-aware smooth profiles)
- Sutherland air viscosity and transport coefficient fill
It does not generate nonlinear CFD baseflows. Scope starts at loaded mean-flow files and ends at a completed BaseFlow plus sponge field ready for mesh/SBP attachment and LNSE linearization.
Package path: src/linstabpy/baseflow/
Public package exports (linstabpy.baseflow):
| Symbol | Role |
|---|---|
BaseFlow |
Mean-flow state container |
read_grid_fields |
Low-level field dictionary load |
load_case_fields |
Full load + preprocess + BaseFlow construction |
load_grid_fields |
Same as load_case_fields without preprocess report |
apply_preprocess |
Optional filter / sponge / shock pipeline |
generate_sponge |
Structured sponge field generator |
sponge_from_config |
Config/preset-driven sponge construction |
load_sponge_aux |
Optional MATLAB HDF5 sponge_aux loader |
sutherland_air |
In-place viscosity and derived coefficients |
BaseFlow is also exposed on the top-level lazy public API (import linstabpy → BaseFlow) without initializing PETSc/MPI.
Role in the System Pipeline¶
Baseflow Management is stage 3 of the staged scientific pipeline:
All major analysis products (temporal/adjoint eigen, spatial eigen, resolvent gains, β–ω maps) share this preparation path. Downstream LNSE assembly reads primitives (RHO,U,V,W,T), parameters (Re,Ma,Pr,kappa,T_0), and viscosity derivatives (MU,dmudT,d2mudT2,cv,c1,c2). Sponge damping is applied later during operator assembly (after analytical inv(B/1j) scaling, before artificial dissipation and BC recipes).
flowchart TD
A[CFD mean-flow npy/npz] --> B[read_grid_fields / load_case_fields]
B --> C[Field validation FIELD_NAMES ndim==2]
C --> D[Optional transpose and strides]
D --> E[apply_preprocess]
E --> F{Shock cleanup enabled?}
F -->|yes| G[shock_cleanup smooth/repair]
F -->|no| H[Sponge scale / boundary ramps]
G --> H
H --> I[Optional binomial filter]
I --> J[BaseFlow construction]
J --> K[sutherland_air viscosity fill]
K --> L[BaseFlow ready for LNSE]
Package Layout¶
src/linstabpy/baseflow/
├── __init__.py # Public re-exports
├── baseflow.py # BaseFlow dataclass
├── grid_fields.py # npy/npz I/O and case loaders
├── preprocess.py # Filter, boundary sponge, shock integration
├── shock_cleanup.py # Opt-in shock sensor / masked filter / shock sponge
├── sponge.py # Structured sponge generators and presets
└── viscosity.py # Sutherland air model (LinStab2D port)
| Sub-module | Responsibility |
|---|---|
| BaseFlow Model | (ny,nx) field contracts and nondimensional parameters |
| Grid Field I/O | Rectilinear primitive load, stride/transpose, case assembly |
| Preprocess and Transport | Shock cleanup, sponge helpers, viscosity fill |
BaseFlow Data Model¶
Contract¶
BaseFlow (baseflow.py) is a plain @dataclass over NumPy arrays. All flow fields use shape (ny, nx), matching the computational mesh.
| Group | Attributes | Notes |
|---|---|---|
| Parameters | Re, Ma, Pr, kappa, T_0 |
Required at construction |
| Primitives | RHO, U, V, W, T |
Required; 2-D float arrays |
| Viscosity | MU, dmudT, d2mudT2 |
Optional; filled by sutherland_air |
| Derived coeffs | cv, c1, c2 |
Optional scalars; filled by sutherland_air |
@dataclass
class BaseFlow:
Re: float
Ma: float
Pr: float
kappa: float
T_0: float
RHO: np.ndarray
U: np.ndarray
V: np.ndarray
W: np.ndarray
T: np.ndarray
MU: np.ndarray | None = None
dmudT: np.ndarray | None = None
d2mudT2: np.ndarray | None = None
cv: float | None = None
c1: float | None = None
c2: float | None = None
Design notes¶
- Fields are mutated in place by loaders and
sutherland_air; the object is a runtime carrier, not an immutable value object. - Viscosity is intentionally deferred so cases can load geometry/primitives first, then fill transport once temperature and
T_0are final. - State vector convention for LNSE is
q = [ρ, u, v, w, T], aligned with these primitive names.
Grid Field I/O¶
Required field set¶
grid_fields.FIELD_NAMES:
Every field must be present and exactly 2-D. Values are coerced to float via _as_field.
read_grid_fields(source)¶
Loads a dictionary of the eight fields from either:
- Single
.npzfile, keys must matchFIELD_NAMESexactly; missing keys raiseFileNotFoundError. - Directory of
.npyfiles, expects{name}.npyfor each name inFIELD_NAMES.
Non-.npz files and missing directories raise clear path errors.
load_case_fields(...)¶
Primary case entry. Builds coordinates, a BaseFlow, the sponge array, and a preprocess report.
Signature (keyword parameters after source):
| Parameter | Default | Meaning |
|---|---|---|
Re, Ma, Pr, kappa, T_0 |
required | Nondimensional / reference parameters |
stride_ksi |
1 |
Subsample along second index (nx) |
stride_eta |
1 |
Subsample along first index (ny) |
transpose_input |
False |
Transpose all fields before strides |
zero_spanwise |
False |
Force W = 0 after preprocess |
refuse_asymmetric_stride |
False |
Reject stride_ksi != stride_eta |
preprocess |
None |
Optional preprocess config mapping |
Processing order:
read_grid_fields- Optional transpose (
arr.Tfor every field) - Stride validation (
>= 1; optional asymmetric refusal) - Subsample
arr[::sy, ::sx]when strides > 1 - Shape consistency check against
x apply_preprocess(X, Y, rho, u, v, w, T, sponge, preprocess)- Optional zero spanwise velocity
- Construct
BaseFlowfrom cleaned primitives
Returns:
where info is the preprocess diagnostic dictionary.
load_grid_fields(...)¶
Thin wrapper around load_case_fields that drops info and does not pass a preprocess config (preprocess remains disabled). Returns (X, Y, baseflow, sponge).
Indexing convention¶
After optional transpose, arrays are (ny, nx):
stride_etawalks the first indexstride_ksiwalks the second index
This matches structured-mesh conventions used by mesh/SBP construction.
Preprocess Pipeline¶
Entry point¶
apply_preprocess(X, Y, rho, u, v, w, T, sponge, preprocess_cfg) applies optional operations and returns the same arrays plus an info report.
If preprocess_cfg is None or enabled is false, inputs are returned unchanged with summary: "disabled".
When enabled, stages run in this order:
- Shock cleanup (
preprocess.shock), if nestedenabled - Sponge scale (
preprocess.sponge.scale) - Boundary sponge ramps (
preprocess.sponge.boundary) - Explicit binomial filter (
preprocess.filter)
Coordinates X and Y are currently passed through unchanged (reserved for future geometry-aware steps).
Boundary sponge (preprocess-local)¶
_generate_boundary_sponge builds quadratic ramps on edges ordered [top, right, bottom, left]:
- Top/bottom act on the first array axis (
ny) - Right/left act on the second array axis (
nx)
Modes relative to the existing sponge field:
| Mode | Behavior |
|---|---|
add (default) |
sponge += generated |
max |
sponge = maximum(sponge, generated) |
replace |
sponge = generated |
Config keys: points (ints), amplitudes (floats), both length-4 lists.
Explicit binomial filter¶
- Kernel: 5-point binomial
[1,4,6,4,1]/16with edge padding - Directions:
"x"→ axis 1,"y"→ axis 0 - Default fields:
rho, u, v, T(optionalw) - Optional wall-normal window via
eta_min_index/eta_max_index(slice on first index) - Multiple passes; relative L2 change recorded per field in
info
Preprocess info contract¶
Typical keys:
enabled,applied(list of stage tags),summaryfilter: enabled, passes, fields, directions, eta window, optionalrelative_l2_changesponge: scale, boundary details, finalmaxshock: nested diagnostics when shock cleanup ran
Shock Cleanup¶
Module: shock_cleanup.py. Fully opt-in so default Case 1 / Case 2 workflows stay untouched.
Configuration merge¶
_normalize_cfg deep-merges user config onto defaults:
_DEFAULT_CFG = {
"enabled": False,
"sensor": {
"enabled": False,
"kind": "compression_ducros",
"compression_threshold": -1.0,
"ducros_threshold": 0.95,
"eps": 1.0e-14,
"buffer_cells": 3,
"blend_sigma": 0.0,
},
"filter": {
"enabled": False,
"passes": 1,
"directions": ["x", "y"],
"fields": ["rho", "u", "v", "T"],
},
"sponge": {
"enabled": False,
"amplitude": 0.0,
"mode": "add",
"smooth_sigma": 0.0,
},
}
Shock sensors¶
Physical derivatives use computational gradients of X,Y and the field, with a safeguarded Jacobian.
| Kind | Logic |
|---|---|
compression_ducros (default) |
Mask where div ≤ compression_threshold and Ducros ratio div²/(div²+vort²+ε) ≥ ducros_threshold |
compression_only |
Mask on divergence threshold only |
modified_ducros |
Reserved; raises NotImplementedError |
Post-processing:
buffer_cells: binary dilation of the maskblend_sigma: Gaussian blend →preserve_weightin[0,1](buffered cells forced to 1)
Masked filtering¶
_apply_masked_filter blends original and binomial-smoothed values:
Shock-adjacent cells are preserved; smooth regions are cleaned. Valid fields: rho, u, v, w, T.
Shock-local sponge¶
Optional amplitude field supported on the buffered mask (optional Gaussian smooth), combined with the existing sponge via add or max.
Guardrails¶
If filter or shock-sponge is enabled while sensor.enabled is false, cleanup raises:
apply_optional_shock_cleanup always returns copies when enabled paths run, plus diagnostics (mask_fraction, L2 changes, sponge max, etc.).
Structured Sponge Generation¶
Module: sponge.py. Complements preprocess boundary ramps with full mesh-aware generators used when building or overriding sponge layers from YAML.
Edge ordering¶
Direction vectors (pts_dir, amp_dir, width_dir, width_frac_dir) always use this order.
Implementation note: Comments in
sponge.pydocument that for index-based generators (constant/parabolic/localized), top/bottom act on the second array axis and right/left on the first. Smooth generators use physical arc-length distance fields derived fromXandYinstead of pure index strips.
Presets (SPONGE_PRESETS)¶
| Preset | Type | Typical use |
|---|---|---|
localized_stability |
localized |
Stability absorbing layers |
localized_resolvent |
localized |
Resolvent absorbing layers |
smooth_soft / smooth_medium / smooth_quarter / smooth_half / smooth_unit / smooth_double |
smooth_localized |
Fraction-width smooth ramps with increasing amplitude |
generate_sponge¶
Supported sponge_type values:
| Type | Description |
|---|---|
constant |
Constant amplitude strips by point counts |
parabolic |
Quadratic ramp strips by point counts |
localized |
Product of edge masks × amplitude |
smooth / smooth_boundary |
Per-edge smooth ramps blended by max / additive / RSS |
smooth_localized |
Product of per-edge smooth ramps × max active amplitude |
Smooth types require X and Y. Widths may combine absolute width_dir with fractional width_frac_dir scaled by median boundary reference lengths.
Smooth profiles: smoothstep, smootherstep (default), cosine, tanh (uses steepness). Optional power exponent on the ramp.
sponge_from_config¶
if not cfg or not enabled → zeros(shape)
else merge preset (if any) with explicit keys → generate_sponge
Unknown presets raise ValueError listing allowed keys.
load_sponge_aux¶
Optional MATLAB compatibility path: reads sponge_aux from an HDF5 .mat via h5py (optional dependency). Must be 2-D.
Viscosity and Transport Coefficients¶
sutherland_air(bf)¶
Port of LinStab2D sutherland_air.m. Mutates bf in place using NumPy only (no PETSc).
Derived scalars:
Sutherland constants (air):
| Symbol | Value |
|---|---|
| \(S_1\) | 110.4 K |
| \(T_{\mathrm{ref}}\) | 280.0 K |
| \(\mu_{\mathrm{ref}}\) | \(1.735\times 10^{-5}\) Pa·s |
Dimensional temperature: \(T^\* = T \cdot T_0\).
Viscosity and first two temperature derivatives are computed dimensionally, then nondimensionalized:
where \(\mu_0\) is Sutherland viscosity evaluated at reference \(T_0\).
Downstream use¶
LNSE coefficient generation needs MU, dmudT, and often d2mudT2 for viscous linearization of compressible Navier–Stokes. Call sutherland_air after preprocess and before coefficient assembly.
Interaction Sequence¶
sequenceDiagram
participant Consumer
participant Init as baseflow.__init__
participant Grid as grid_fields
participant BF as BaseFlow
participant Pre as preprocess
participant Shock as shock_cleanup
participant Sponge as sponge
participant Visc as viscosity
Consumer->>Init: load_case_fields / apply_preprocess / sutherland_air
Init->>Grid: read_grid_fields
Grid-->>Init: dict of 2D float fields
Init->>Pre: apply_preprocess(...)
Pre->>Shock: apply_optional_shock_cleanup (optional)
Shock-->>Pre: cleaned fields + diagnostics
Pre-->>Init: fields + sponge + info
Init->>BF: construct BaseFlow(RHO,U,V,W,T,...)
Consumer->>Sponge: generate_sponge / sponge_from_config (optional override)
Sponge-->>Consumer: sponge layer field
Consumer->>Visc: sutherland_air(bf)
Visc-->>BF: MU dmudT d2mudT2 cv c1 c2
BF-->>Consumer: mesh-aligned baseflow for LNSE
Upstream consumers¶
- Case Orchestration (
driver): loads case fields after config validation; records preprocess info in metrics/provenance. - Library API users: construct
BaseFlowmanually or via loaders without PETSc init.
Downstream consumers¶
- Mesh Discretization: uses returned
X,Y(and shape) for geometry/SBP. - LNSE Operator Assembly: reads primitives, viscosity, and sponge; assembly order applies sponge after
inv(B)scaling and before dissipation/BCs. - Stability / Resolvent / Router: inherit prepared mean flow through assembled operators.
Domain relations (strength)¶
| Relation | Type | Strength |
|---|---|---|
| Case Orchestration → Baseflow | Data dependency | High |
| LNSE Assembly → Baseflow | Data dependency | High (~9.5) |
| Baseflow → Mesh | Data dependency (shape/geometry alignment) | Medium–High |
Configuration Patterns¶
Illustrative YAML fragments (keys as consumed by the baseflow modules):
# Physical parameters supplied to load_case_fields
Re: 1000.0
Ma: 0.8
Pr: 0.72
kappa: 1.4
T_0: 288.15
# Optional preprocess block passed as preprocess=
preprocess:
enabled: true
shock:
enabled: false
sensor:
enabled: true
kind: compression_ducros
compression_threshold: -1.0
ducros_threshold: 0.95
buffer_cells: 3
filter:
enabled: true
passes: 1
fields: [rho, u, v, T]
directions: [x, y]
sponge:
enabled: false
amplitude: 0.0
mode: add
sponge:
scale: 1.0
boundary:
enabled: false
mode: add
points: [0, 0, 0, 0]
amplitudes: [0.0, 0.0, 0.0, 0.0]
filter:
enabled: false
passes: 0
fields: [rho, u, v, T]
directions: [x, y]
# Optional standalone sponge generator config (sponge_from_config)
sponge:
enabled: true
preset: localized_stability
# or explicit:
# type: smooth_localized
# width_frac_dir: [0.0, 0.28, 0.28, 0.0]
# amp_dir: [0.0, 1.0, 1.0, 0.0]
# profile: smootherstep
Loader options such as stride_ksi, stride_eta, transpose_input, and zero_spanwise are typically wired from mesh/case sections by the driver.
Practical Usage¶
Standard case load¶
from linstabpy.baseflow import load_case_fields, sutherland_air
X, Y, bf, sponge, info = load_case_fields(
"path/to/meanflow_dir_or.npz",
Re=1000.0,
Ma=0.8,
Pr=0.72,
kappa=1.4,
T_0=288.15,
stride_ksi=1,
stride_eta=1,
preprocess={"enabled": False},
)
sutherland_air(bf)
# bf.MU, bf.dmudT, bf.d2mudT2, bf.cv, bf.c1, bf.c2 now populated
Manual BaseFlow + sponge preset¶
import numpy as np
from linstabpy.baseflow import BaseFlow, sponge_from_config, sutherland_air
ny, nx = Y.shape
bf = BaseFlow(Re=..., Ma=..., Pr=..., kappa=..., T_0=...,
RHO=rho, U=u, V=v, W=w, T=T)
sponge = sponge_from_config(
(ny, nx),
{"enabled": True, "preset": "smooth_unit"},
X=X,
Y=Y,
)
sutherland_air(bf)
Library embedding without PETSc¶
from linstabpy.baseflow import BaseFlow, load_grid_fields, sutherland_air does not initialize PETSc/MPI. This supports operator-prep debugging and embedding in larger pipelines before any eigen/resolvent solve.
Validation, Errors, and Diagnostics¶
| Check | Failure mode |
|---|---|
| Missing npy/npz keys | FileNotFoundError |
| Non-2-D field | ValueError with shape |
| Mismatched field shapes | ValueError vs x shape |
| Invalid strides | ValueError (< 1 or asymmetric when refused) |
| Bad preprocess types | TypeError for non-mappings |
| Bad edge vectors | ValueError (must be four entries) |
| Shock filter without sensor | ValueError |
| Unknown sponge preset/type/profile | ValueError |
| MATLAB sponge without h5py | ModuleNotFoundError |
Diagnostics are first-class: preprocess and shock cleanup return structured info dictionaries suitable for metrics/provenance logging (mask fractions, relative L2 changes, sponge maxima, applied stage tags).
Implementation Characteristics¶
| Aspect | Detail |
|---|---|
| Dependencies | NumPy core; SciPy ndimage for shock dilation/Gaussian; optional h5py for MATLAB sponge |
| Parallelism | Local array work; no MPI inside baseflow (distribution begins at PETSc conversion later) |
| Mutation model | In-place viscosity fill; preprocess returns new/updated arrays; shock path copies when active |
| MATLAB heritage | Viscosity law and field packing align with LinStab2D workflows |
| Safety default | Shock cleanup and most preprocess stages default off |
Extension Points¶
- Additional viscosity laws, parallel to
sutherland_air, mutating the same optional fields. - New sponge profiles/presets, extend
_smooth_profileorSPONGE_PRESETS. - Shock sensor kinds,
modified_ducrosis reserved for a group-supplied formula. - Geometry-aware preprocess,
X,Yalready thread throughapply_preprocessfor future deformation-aware cleanup. - Richer I/O,
read_grid_fieldsis the single choke point for new archive layouts.
Summary¶
Baseflow Management is the mean-flow readiness layer of LinStabPy. It enforces a strict (ny,nx) primitive contract, loads CFD dumps from npy/npz, optionally cleans shocks and builds absorbing layers, and fills Sutherland transport coefficients required by compressible LNSE linearization. The module is deliberately free of PETSc/SLEPc concerns so it can run as a lightweight library stage or as the shared foundation of every stability and resolvent workflow.
Key files for implementers:
baseflow.py, state contractgrid_fields.py, I/O and case assemblypreprocess.py, orchestrated cleanupshock_cleanup.py, opt-in shock treatmentsponge.py, absorbing-layer generationviscosity.py, air transport fill