Grid

Collection of connected blocks forming a complete flow domain.

This module defines the Grid, the top-level container for a multi-block structured simulation. A grid is an ordered collection of Block objects together with the topology that connects them. A Grid stores no flow field of its own: every coordinate and conserved quantity lives on the constituent blocks. Therefore the solution is read from one block at a time as for example grid[0].P to access pressure on the first block.

Construction and file formats

Like a plain Python list, blocks can be added to a grid at construction or a later time:

from ember.block import Block
from ember.grid import Grid

grid = Grid([Block(shape=(10, 11, 10))])
rotor = Block(shape=(20, 20, 20))
rotor.set_label("rotor")
grid.append(rotor)

The grid then behaves as a standard Python collection: it supports iteration, len(), membership testing with in, and the usual mutating operations (Grid.append(), Grid.extend(), Grid.insert(), Grid.remove(), Grid.pop(), Grid.clear()). Indexing accepts either an integer position or a label string, and membership testing accepts either a block or a label. Grid.labels lists the labels in order, with None for any unlabelled block:

len(grid)                 # 2
grid[1] is grid["rotor"]  # True -- refers to same block
"rotor" in grid           # True -- membership by label
rotor in grid             # True -- membership by block
grid.labels               # [None, 'rotor'] -- None if unlabelled

Grid.copy() duplicates a grid, and Grid.resample() returns a copy regridded onto a new node count.

Grid.copy([keep_patches])

Create a deep copy of the grid with copied blocks.

Grid.resample(factors)

Resample all blocks, returning a new Grid at the new resolution.

A grid can also be read from and written to two file formats. The reading methods are constructors, returning a new Grid.

  • EMB – Grid.read_emb(), Grid.write_emb(). Our native format: a pickle of the grid with its blocks, patches, and labels, optionally gzip-compressed. Being a pickle of the objects themselves, it is the format that preserves a grid most completely.

  • Plot3D – Grid.read_plot3d(), Grid.write_plot3d(). The standard multi-block structured interchange format, carrying coordinates only. Boundary patches are stored alongside it in a separate FieldView boundary file, which may be read and written with the Plot3D file or on its own via Grid.write_fvbnd().

Grid.read_emb(filename)

Read grid from EMB pickle file.

Grid.read_plot3d(p3d_file[, fvbnd_file, flip_k])

Read grid from Plot3D format file with optional boundary patches.

Grid.write_emb(filename[, compress])

Write grid to EMB binary format file.

Grid.write_fvbnd(filename[, region_id])

Write boundary conditions in FieldView boundary (.fvbnd) format.

Grid.write_plot3d(p3d_filename[, ...])

Write grid to Plot3D format with optional FVBND boundary file.

Connectivity

What distinguishes a Grid from a plain list of blocks is the topology it derives from the blocks’ boundary patches, as found in patches. See also ember.patch for the patch types and their semantics.

Grid.patches presents every patch from every block as one flat, read-only sequence, filterable by patch type (grid.patches.inlet, grid.patches.periodic, and so on). It is a view: patches are still owned by the block they sit on, and are added and removed there.

Grid.connectivity manages communicators that exchange data across the seams between blocks, one per patch type, reached as grid.connectivity.periodic and likewise mixing, nonmatch, cusp. Pairing each patch to its partner on a neighbouring block, and the exchange itself, are described in Communicators.

Changing grid topology – adding or removing a block or a patch – may break the indexing describing pairing, and unfortunately the cache does not detect this. In these situations, the cache must be flushed by hand and the next communicator exchange will then pair the new topology from scratch.:

grid.apply_bconds()  # pairs the periodic patches
# ...
grid.append(another_block)
grid.connectivity.clear()  # drop stale pairs
grid.apply_bconds()  # will pair the new topology

Blocks joined to one another by periodic patches make up a single blade row. Rows are separated from one another by mixing patches, and Grid.rows groups the blocks accordingly, ordering the rows from inlet to outlet; Grid.n_row is their count. The first row’s upstream face is the domain inlet and the last row’s downstream face is the domain outlet. Both properties will pair the periodic patches for themselves.

Grid.connectivity

Get the cached connectivity manager for this grid.

Grid.n_row

Get the number of blade rows in the grid.

Grid.patches

Get read-only view of all patches across all blocks.

Grid.row_station_bid_pid

Per-row upstream/downstream measurement-station patch identifiers.

Grid.rows

Get blade rows by grouping blocks connected by periodic patches.

Global flow field setting

Rather than setting up each block’s flow field individually, a handful of Grid methods populate every block at once from a source outside the grid: an initial guess constructed from meridional or quasi-3D data, the solution on another grid, or an unstructured cloud of points, mapped onto the grid’s structured topology via Grid.align_cart_unstr().

Grid.align_cart_unstr(xyz)

Map an unstructured Cartesian point cloud onto this grid's nodes.

Grid.apply_guess_meridional(block_guess[, ...])

Apply a circumferentially uniform flow field guess.

Grid.apply_guess_quasi3d(block_guess)

Apply quasi-3D flow field guess by interpolating between two meridional planes.

Grid.interp_from_arrays(arrays)

Interpolate a flow field onto this grid from plain arrays.

Grid.interp_from_grid(src)

Interpolate the solution from another grid onto this one.

Grid.set_conserved_cart_unstr(xyz, ...)

Set conserved variables from Cartesian unstructured data.

Grid.set_primitive_cart_unstr(xyz, ...)

Set primitive variables from Cartesian unstructured data.

Time marching

Many of the grid methods, such as Grid.update_residual() and Grid.apply_bconds(), form the inner loop of a time-marching solver. They are documented in ember.solver, and should be used with care.

During time marching, Grid.get_convergence() returns a ConvergenceStep of residual and station monitors for the current step, which ConvergenceHistory accumulates into a time series. Grid.check_nan() raises DivergenceError if the flow field has blown up.

Grid.accumulate_avg(n_step_avg)

Add the current conserved field into the running time-average.

Grid.apply_bconds()

Apply all boundary conditions across the grid once.

Grid.calculate_wdist([limit_pitch])

This method creates a pitchwise-repeated grid to include neighboring passages, extracts all wall nodes from the repeated blocks, builds a KDTree for efficient nearest neighbor search, and calculates the distance from each node to the nearest wall surface.

Grid.check_nan()

Scan every block's density field for NaN; report the first bad block.

Grid.finalise_average()

Commit the accumulated time-average as the solution.

Grid.get_convergence()

Grid-representative convergence monitors at one step, non-dimensional.

Grid.smooth(sf4, sf2)

Apply constant-coefficient artificial dissipation to every block.

Grid.update_bconds([freeze, cfl])

Refresh boundary-condition targets across the grid once.

Grid.update_cached_conserved()

Refresh conserved-dependent caches on every block.

Grid.update_filter(cfl, delta_filt)

Evolve the SFD low-pass filter one step on every block.

Grid.update_residual([sf])

Rebuild the unintegrated net-flow residual on every block.

Grid.update_sources(inviscid, gain_filt)

Zero and rebuild the body force on every block of this grid level.

Grid.update_timestep(rf[, fac_visc])

Recompute the volumetric time step on every block.

Metadata

Scalar properties of the grid as a whole, rather than per-node data: the working fluid, reference length, mean radius, and total node count.

Grid.get_r_ref()

Calculate reference radial coordinates for each row of blocks.

Grid.set_fluid(fluid_obj)

Set equation of state on all blocks, preserving any existing flow field.

Grid.set_L_ref(L_ref)

Set reference length scale on all blocks, preserving dimensional geometry and flow field.

Grid.size

Total number of grid points across all blocks.

class grid.Grid(blocks=None)[source]

Bases: _LabelledList

Initialize grid with optional list of blocks.

Parameters:

blocks (list, optional) – Initial list of blocks to add to the grid.

classmethod read_emb(filename)[source]

Read grid from EMB pickle file.

Automatically detects and handles both uncompressed and gzip-compressed EMB files.

Parameters:

filename (str) – Input EMB file to read

Returns:

New grid containing all blocks, patches, flow data, and metadata from EMB file

Return type:

Grid

Raises:

FileNotFoundError – If file does not exist

classmethod read_plot3d(p3d_file, fvbnd_file=None, flip_k=True)[source]

Read grid from Plot3D format file with optional boundary patches.

Parameters:
  • p3d_file (str) – Input Plot3D grid file to read

  • fvbnd_file (str) – Input FVBND boundary file to read patches from

  • flip_k (bool) – Whether to flip the k-axis to match write_plot3d behavior (default True)

Returns:

New grid containing blocks with coordinates and optional patches from files

Return type:

Grid

set_conserved_cart_unstr(xyz, conserved_cart)[source]

Set conserved variables from Cartesian unstructured data.

Useful for importing flow solutions from unstructured CFD solvers that store data on arbitrary point clouds in Cartesian coordinates.

Automatically detects the coordinate permutation and sign mapping that aligns the Cartesian data with the structured grid, converts coordinates to polar, and transforms the momentum vector accordingly.

The input conserved state vector is:

\[\mathcal{U}_{\mathrm{cart}} = \begin{pmatrix} \rho,\ \rho V_x,\ \rho V_y,\ \rho V_z,\ \rho e \end{pmatrix}\]

The Cartesian momentum components are first converted to velocities, rotated into the polar frame \((x, r, \theta)\), then reassembled as polar conserved variables:

\[\mathcal{U} = \begin{pmatrix} \rho,\ \rho V_x,\ \rho V_r,\ \rho r V_\theta,\ \rho e \end{pmatrix}\]

where the polar velocity components are:

\[\begin{split}V_r &= V_y \cos\theta - V_z \sin\theta \\ V_\theta &= -V_y \sin\theta - V_z \cos\theta\end{split}\]

with \(\theta = \mathrm{atan2}(-z,\, y)\).

Warning

The total energy \(\rho e\) is assumed to be measured from this grid’s thermodynamic datum. External solvers generally use their own, in which case passing their conserved variables here stores the energy on the wrong datum and silently corrupts temperature. Convert the foreign internal energy to pressure first and use set_primitive_cart_unstr(), whose variables are all datum-free. See Datum state.

Parameters:
  • xyz (array_like, shape (N, 3)) – Cartesian coordinates [x, y, z] with components on last axis

  • conserved_cart (array_like, shape (N, 5)) – Cartesian conserved flow variables [rho, rhoVx, rhoVy, rhoVz, rhoe] with components on last axis

set_fluid(fluid_obj)[source]

Set equation of state on all blocks, preserving any existing flow field.

The fluid object specifies the reference scales used to normalise stored thermodynamic quantities. This method rescales the stored nondimensional flow field so that the underlying dimensional state is unchanged. See ember.block.Block.set_fluid() for full details.

Parameters:

fluid_obj (Fluid) – New fluid / equation of state object.

set_L_ref(L_ref)[source]

Set reference length scale on all blocks, preserving dimensional geometry and flow field.

The reference length scale is used to normalise stored coordinates. This method rescales the stored nondimensional values so that the underlying dimensional geometry and flow field are unchanged. See ember.block.Block.set_L_ref() for full details of the nondimensionalisation.

Parameters:

L_ref (float) – Reference length scale [m].

set_primitive_cart_unstr(xyz, primitive_cart)[source]

Set primitive variables from Cartesian unstructured data.

Useful for importing flow solutions from unstructured CFD solvers that store data on arbitrary point clouds in Cartesian coordinates.

Automatically detects the coordinate permutation and sign mapping that aligns the Cartesian data with the structured grid, converts coordinates to polar, and rotates the velocity vector accordingly.

The input primitive state vector is:

\[\mathcal{P}_{\mathrm{cart}} = \begin{pmatrix} \rho,\ V_x,\ V_y,\ V_z,\ p \end{pmatrix}\]

The Cartesian velocity components are rotated into the polar frame \((x, r, \theta)\) to give the polar primitive state:

\[\mathcal{P} = \begin{pmatrix} \rho,\ V_x,\ V_r,\ V_\theta,\ p \end{pmatrix}\]

where:

\[\begin{split}V_r &= V_y \cos\theta - V_z \sin\theta \\ V_\theta &= -V_y \sin\theta - V_z \cos\theta\end{split}\]

with \(\theta = \mathrm{atan2}(-z,\, y)\).

Parameters:
  • xyz (array_like, shape (N, 3)) – Cartesian coordinates [x, y, z] with components on last axis

  • primitive_cart (array_like, shape (N, 5)) – Cartesian primitive flow variables [rho, Vx, Vy, Vz, P] with components on last axis

get_convergence()[source]

Grid-representative convergence monitors at one step, non-dimensional.

Stations are taken from row_station_bid_pid, derived from the grid’s own topology.

Returns:

Residual and station monitors for this step. See ConvergenceStep for the meaning of each field.

Return type:

ConvergenceStep

get_r_ref()[source]

Calculate reference radial coordinates for each row of blocks.

For each element in Grid.rows, calculates the maximum and minimum r coordinates across all blocks, then stores the mean value in Grid.r_ref as a numpy array with the same length as rows.

accumulate_avg(n_step_avg)[source]

Add the current conserved field into the running time-average.

Accumulates each block’s conserved_nd / n_step_avg into its conserved_avg_nd buffer via the Fortran kernel. Called once per step over the final n_step_avg steps of a march, this builds the mean of the converged limit cycle. The buffer is a read-only cached array, so its flags.writeable is toggled around the in-place kernel write (mirrors update_sources()).

align_cart_unstr(xyz)[source]

Map an unstructured Cartesian point cloud onto this grid’s nodes.

Detects the coordinate permutation and sign flips that bring xyz into the grid’s own Cartesian frame, then locates each grid node within the cloud, returning the index correspondence between the two.

This is the same alignment performed internally by set_conserved_cart_unstr() and set_primitive_cart_unstr(). It is exposed separately so that data can also be sent out to an unstructured solver on the same correspondence – for example scattering wdist into an external solver’s node array.

Parameters:

xyz (array_like, shape (N, 3)) – Cartesian coordinates \((x, y, z)\), where N must equal size.

Returns:

  • perm (tuple of int) – Permutation mapping the input’s axes onto the grid’s.

  • signs (tuple of int) – Sign applied to each permuted axis, each +1 or -1.

  • block_indices (list of ndarray) – One index array per block, of that block’s shape, giving the row of xyz that each node of the block corresponds to.

Raises:

ValueError – If no transformation aligns the cloud with the grid to tolerance.

Examples

Transfer a nodal field out to the unstructured ordering:

>>> perm, signs, block_indices = grid.align_cart_unstr(xyz)
>>> out = np.empty(xyz.shape[0])
>>> for block, ind in zip(grid, block_indices):
...     out[ind.flatten()] = block.wdist.flatten()
apply_bconds()[source]

Apply all boundary conditions across the grid once.

Imposes the physical inlet, outlet, and mixing patch conditions on every block, then averages the flow field across periodic boundaries.

apply_guess_meridional(block_guess, refine_factor=1)[source]

Apply a circumferentially uniform flow field guess.

Uses a 1D meridional block as initial guess, interpolating flow properties to all blocks in the grid using nearest-neighbor search in the (x, r) meridional plane. Optionally refines the guess using curvilinear arc-length coordinates for improved interpolation quality.

Parameters:
  • block_guess (Block) – Block containing the 1D guess flow field with shape (ni,). Use block_guess.squeeze() first if your block has singleton dimensions.

  • refine_factor (int, optional) – Refinement factor for the guess block (default=1, no refinement). If > 1, the guess is interpolated along curvilinear arc length, providing a denser point cloud and smoother results.

Raises:

ValueError – If block_guess does not have shape (ni,)

apply_guess_quasi3d(block_guess)[source]

Apply quasi-3D flow field guess by interpolating between two meridional planes.

Uses two 2D meridional faces as boundary conditions on the low- and high-theta k-faces of each block, then linearly interpolates conserved variables across k as a function of the circumferential coordinate theta.

Parameters:

block_guess (Block, shape (ni, nj, 2)) – A single block whose k=0 face is the low-theta meridional plane and k=1 face is the high-theta meridional plane. i is streamwise, j is radial.

Raises:

ValueError – If block_guess does not have shape (ni, nj, 2).

calculate_wdist(limit_pitch=inf)[source]

This method creates a pitchwise-repeated grid to include neighboring passages, extracts all wall nodes from the repeated blocks, builds a KDTree for efficient nearest neighbor search, and calculates the distance from each node to the nearest wall surface.

Parameters:

limit_pitch (float, optional) – Cap the wall distance at limit_pitch times each row’s reference blade pitch (2*pi*r_ref/Nb). This bakes the mixing-length limit directly into the stored wall distance, so downstream turbulence models need no separate cap. Defaults to np.inf (no limit).

Examples

>>> grid = Grid([block1, block2])
>>> # Wall distances are now available as block.wdist for each block
check_nan()[source]

Scan every block’s density field for NaN; report the first bad block.

Cheap enough to call each solver step: only the density component conserved_nd[..., 0] is inspected, since a NaN in any conserved variable propagates into density within a step through the pressure/flux coupling. On the duct smoke test this costs ~0.5% of a single full-field residual evaluation.

Raises:

DivergenceError – If any block contains a NaN. The message names the first such block (index and label), the (i, j, k) node bounding box of the NaN region, and which of the six boundary faces it touches – enough to tell a boundary-seeded blow-up from an interior one. The grid is left untouched so the invalid field can be inspected.

copy(keep_patches=True)[source]

Create a deep copy of the grid with copied blocks.

Returns:

New Grid instance containing copies of all blocks.

Return type:

Grid

Example

>>> grid_copy = grid.copy()
>>> grid_copy[0].conserved[...] = 0  # Does not affect original grid
finalise_average()[source]

Commit the accumulated time-average as the solution.

Copies each block’s conserved_avg_nd into conserved_nd, refreshes the conserved-dependent caches, then re-zeros the accumulator so any subsequent averaging window starts clean.

interp_from_arrays(arrays)[source]

Interpolate a flow field onto this grid from plain arrays.

For a field that arrives without a mesh around it – read back from a file, say. Nothing is materialised on the source side, and with no patch layout to align to the mapping simply runs end to end in index space.

Parameters:

arrays (sequence) – One entry per block, each a sequence of arrays matching ember.block_util.STATE.

interp_from_grid(src)[source]

Interpolate the solution from another grid onto this one.

Assumes the source grid has the same block topology as this one, but possibly a different resolution. Each block is interpolated in index space, which is exact for linear functions on a uniform grid only, and which holds patch boundaries where they started.

The state is transferred as pressure, temperature and velocity, so the two grids may carry different fluids – different reference scales, and different entropy and energy datums – with no conversion needed.

Parameters:

src (Grid) – Source Grid providing the solution.

resample(factors)[source]

Resample all blocks, returning a new Grid at the new resolution.

smooth(sf4, sf2)[source]

Apply constant-coefficient artificial dissipation to every block.

update_bconds(freeze=False, cfl=1.0)[source]

Refresh boundary-condition targets across the grid once.

Advances the slowly-varying BC state that the per-substep apply_bconds() then imposes: exchanges mixing-plane data, snapshots the inlet pressure datum, and re-derives the outlet throttle/spanwise pressure target. Should be called once per outer timestep, before the Runge-Kutta stages.

When freeze is True the targets are held stationary – the mixing exchange and the outlet target re-derivation are skipped – so an averaging window sees a fixed boundary. The update_soln snapshots still run so backflow density relaxation stays anchored to the current step.

cfl is handed straight to ember.patch.OutletPatch.update_target(), which weights a mass flow throttle’s integral by it so one gain holds across a CFL sweep. It is passed per call rather than held on the patch so that it is always the number the march is running at; the default of 1 integrates per call, for a grid stepped by hand.

update_cached_conserved()[source]

Refresh conserved-dependent caches on every block.

Fans update_cached_conserved() out across the grid, forcing each block’s cached properties keyed on the conserved variables to recompute on next access. Needed after writing conserved_nd directly (bypassing the setters), e.g. the explicit time march.

update_filter(cfl, delta_filt)[source]

Evolve the SFD low-pass filter one step on every block.

First-order exponential moving average of each block’s cell-centred conserved state toward its current cell state, with per-cell timestep dt = cfl * dt_vol * vol. cfl may be a per-cell/per-equation array of shape (ni-1, nj-1, nk-1, 5) or a single scalar; the rank selects the matching kernel. delta_filt is the filter time constant.

Must run after the CFL and dt_vol for the step are current. This is the only writer of the read-only conserved_filt_nd buffer, so it owns the flags.writeable toggle (mirrors the timestep writers).

update_residual(sf=0.0)[source]

Rebuild the unintegrated net-flow residual on every block.

Fans the fused set_residual kernel across the grid, writing each block’s residual_nd from its frozen P/T cache, face areas, and body force. Purely per-block (no inter-block exchange), so it simply loops.

Optional post-processing runs in place on each block’s residual: implicit residual smoothing (sf).

Note

A negative-feedback change limiter (multall’s DAMP) used to run here, ahead of the smoother, soft-clipping cells whose per-step change was a large outlier against the per-variable block mean. It was removed: normalising by the running mean made it a solution-dependent per-cell gain that never switched off, and applying it to the residual broke the telescoping the multigrid box-sum restriction relies on. Measured on the clustered duct at cfl=3.0, fac_mgrid=0.4, n_levels=3: the same case converged 2.59 decades undamped, converged 2.57 decades damped with multigrid OFF, and diverged within 25 convergence records with both on. It also roughly doubled the time to settle where it did work (step 1975 against 975). Reduce cfl or fac_mgrid for robustness instead – both are uniform scalars, so neither creates a gain field nor disturbs the restriction.

Parameters:

sf (float, optional) – Implicit residual-smoothing (IRS) coefficient (epsilon). 0 (the default) disables IRS; > 0 applies the exact factored-tridiagonal smoother to each block via smooth_residual_tri_tiled. IRS damps high-frequency residual content so the explicit march tolerates a higher CFL; because it acts only on the residual (which vanishes at convergence) it does not change the steady-state solution. Per-block only: block/periodic interfaces are treated as zero-gradient. Borrows block.scratch as its work buffer – free at this point, since set_residual stages its face flows further into the arena and the march reuses the head of scratch only afterwards.

update_sources(inviscid, gain_filt)[source]

Zero and rebuild the body force on every block of this grid level.

Assembles, into each block.F_body_nd, the viscous shear stresses (unless inviscid), the polar source, and the optional SFD force (when gain_filt is nonzero) – in that order, so the viscous momentum/energy negation does not flip the polar source added afterwards.

The viscous calculation is phased across the whole grid: every block’s BOUNDARY tau/q is computed first, then a single periodic seam exchange runs over the face buffers, then interior tau/q and the face fluxes are produced together in one walk per block. The nodal transport properties both kernels read (mu, kappa, cp) are borrowed from each block’s scratch arena rather than cached on the block, filled in the first phase and read back in the second. This keeps the seam consistent for block-to-block periodic interfaces, where a per-block exchange would read a stale neighbour halo.

Parameters:
  • inviscid (bool) – Skip the viscous shear-stress/heat-flux terms when True.

  • gain_filt (float) – Selective-frequency-damping gain; the SFD force is added only when nonzero.

update_timestep(rf, fac_visc=1.0)[source]

Recompute the volumetric time step on every block.

Uses a max-of-directional-radii variant of the JST/Blazek definition dt_vol = 1 / max(lam_conv, lam_diff), where lam_conv is the largest of the convective spectral radii Lambda_d = |V_rel . dA_d| + a*||dA_d|| over the three directions and lam_diff = fac_visc * (mu_turb/rho)*max_d||dA_d||^2/vol is the turbulent-diffusion radius over the same faces (set_timestep_spectral()). Taking the max of the directional radii (rather than Blazek’s sum) makes the CFL number the true 1D Courant limit (~``2*sqrt(2)`` for the 4-stage RK march) while staying aspect-ratio-independent for the viscous limit too.

rf is the relaxation factor blending the new dt_vol with the existing buffer as rf*new + (1-rf)*old (pass rf=1.0 for a fresh recompute). This is the lone writer of each block’s read-only dt_vol_nd buffer, so it owns the flags.writeable toggle.

fac_visc (>= 1) multiplies the diffusion radius so the viscous march tolerates the same cfl as the inviscid one; 1.0 leaves the bare directional radius untouched.

write_emb(filename, compress=False)[source]

Write grid to EMB binary format file.

Parameters:
  • filename (str) – Output filename for EMB file

  • compress (bool, optional) – If True, compress the file using gzip (default False)

Raises:

ValueError – If grid is empty (contains no blocks)

write_fvbnd(filename, region_id=0)[source]

Write boundary conditions in FieldView boundary (.fvbnd) format.

The FVBND format is used by FieldView to specify boundary regions for visualization purposes. See FieldView Reference Manual page 520.

Parameters:
  • filename (str) – Output filename for FieldView boundary file

  • region_id (int, optional) – Region identifier for labeling patches (default 0)

Raises:

ValueError – If grid is empty (contains no blocks)

write_plot3d(p3d_filename, fvbnd_filename=None, flip_k=True, iregion=0)[source]

Write grid to Plot3D format with optional FVBND boundary file.

Parameters:
  • p3d_filename (str) – Output filename for Plot3D grid file

  • fvbnd_filename (str) – Output filename for FVBND boundary file. If None, no boundary file is written

  • flip_k (bool) – Whether to flip the k-axis for proper volume orientation in Pointwise (default True)

  • iregion (int) – Region number for labeling in FVBND file (default 0)

Raises:

ValueError – If grid is empty (contains no blocks)

Return type:

None

property connectivity

Get the cached connectivity manager for this grid.

The manager is built once and cached on the grid, so the pairings and communicators it owns are computed lazily and reused. Call grid.connectivity.clear() to invalidate the cache after changing the grid topology (adding/removing blocks or patches).

Returns:

Connectivity manager providing access to patch connections via patch-type-specific properties and methods.

Return type:

GridConnectivityManager

property n_row

Get the number of blade rows in the grid.

Returns:

Number of blade rows (groups of blocks connected by periodic patches).

Return type:

int

Examples

>>> grid = Grid([block1, block2])  # Single row
>>> grid.n_row  # Returns 1
>>>
>>> grid_multi = Grid([stator_block, rotor_block])  # Two rows
>>> grid_multi.n_row  # Returns 2
property patches

Get read-only view of all patches across all blocks.

Returns:

Read-only collection providing access to all patches from all blocks with patch type filtering (periodic, mixing, inlet, outlet, wall).

Return type:

GridPatchCollection

property row_station_bid_pid

Per-row upstream/downstream measurement-station patch identifiers.

Returns:

[(up_idx, dn_idx), ...], one pair per blade row ordered inlet to outlet. Each entry is a list of (bid, pid) patch identifiers: the row’s upstream face (inlet for the first row, otherwise mixing) then its downstream face (outlet for the last row, otherwise mixing). Consumed by get_convergence() and ember.convergence_history.ConvergenceHistory.from_grid().

Return type:

list of (list of (int, int), list of (int, int))

property rows

Get blade rows by grouping blocks connected by periodic patches.

Blocks connected by periodic patches are in the same blade row. Multiple rows are separated by mixing patches. Rows are ordered from inlet to outlet based on mixing patch connectivity.

Returns:

List of blade rows, where each row is a list of Block objects. Blocks within a row are connected by periodic patches. Rows are ordered from inlet (upstream) to outlet (downstream).

Return type:

List[List[Block]]

Examples

>>> # Single-row grid
>>> grid = Grid([block1, block2])
>>> rows = grid.rows  # Returns [[block1, block2]]
>>>
>>> # Multi-row grid with mixing patches
>>> grid = Grid([stator_blocks, rotor_blocks])
>>> rows = grid.rows  # Returns [[stator_blocks...], [rotor_blocks...]]
property size

Total number of grid points across all blocks.

Returns:

Sum of all block sizes in the grid

Return type:

int

append(item)

Add item to collection.

Parameters:

item (Any) – Item to add to the collection. The item manages its own label.

Raises:

ValueError – If item’s label already exists in the collection.

clear()

Remove all items from collection.

extend(items)

Add multiple items to collection.

Parameters:

items (list) – List of items to add. Each item manages its own label.

Raises:

ValueError – If any item’s label already exists in the collection.

index(item, start=0, stop=None)

Return index of first occurrence of item.

Parameters:
  • item (Any) – Item to find.

  • start (int, optional) – Start index for search.

  • stop (int, optional) – Stop index for search.

Returns:

Index of the item.

Return type:

int

insert(index, item)

Insert item at specific index.

Parameters:
  • index (int) – Index at which to insert the item.

  • item (Any) – Item to insert. The item manages its own label.

property labels

List of item labels, in order, including None for unlabelled items.

Returns:

The label of each item in the collection. Entries are None where an item has no label.

Return type:

list

pop(index=-1)

Remove and return item at index (default last).

Parameters:

index (int, optional) – Index of item to remove and return. Default is -1 (last item).

Returns:

The removed item.

Return type:

Any

remove(item)

Remove first occurrence of item.

Parameters:

item (Any) – Item to remove.

class grid.GridConnectivity(grid, patch_class)[source]

Bases: object

Initialize connectivity manager for a specific patch type.

Parameters:
  • grid (Grid) – The grid containing blocks with patches to analyze for connectivity

  • patch_class (type) – Subclass of Patch to filter and operate on

apply()[source]

Apply the communicator’s averaging (periodic and non-matching patches).

clear()[source]

Drop cached pairings and communicator so they rebuild on next use.

exchange()[source]

Exchange in/out flux state across mixing-plane patches.

exchange_faces()[source]

Exchange boundary tau/q across periodic patches, face-buffer form.

pair(rtol=1e-06)[source]

Pair patches of the specified type, caching the result.

Filters patches to only include instances of self.patch_class, then uses spatial proximity (KDTree in x,r coordinates) to find potential matches and delegates to each patch’s check_match method for validation. The result is cached; call clear() to recompute after topology changes.

Parameters:

rtol (float, optional) – Relative tolerance for matching (passed to patch check_match methods)

Returns:

Dictionary where keys are (bid, pid) tuples and values are ((matching_bid, matching_pid), transform) tuples. Both patches in each pair are included as separate keys.

Return type:

dict

Raises:

ValueError – If any patch does not have a matching pair

class grid.GridConnectivityManager(grid)[source]

Bases: object

Initialize connectivity manager for a grid.

Parameters:

grid (Grid) – The grid containing blocks with patches

clear()[source]

Drop all cached pairings and communicators across every patch type.

pair(rtol=1e-06)[source]

Pair all patch types and return combined connectivity dictionary.

Parameters:

rtol (float, optional) – Relative tolerance for matching (passed to patch check_match methods)

Returns:

Combined dictionary where keys are (bid, pid) tuples and values are ((matching_bid, matching_pid), transform) tuples for all patch types.

Return type:

dict

property cusp

Get connectivity manager for cusp patches.

property mixing

Get connectivity manager for mixing patches.

property nonmatch

Get connectivity manager for non-matching patches.

property periodic

Get connectivity manager for periodic patches.

class grid.ConvergenceStep(residual, mdot, ho, s, mdot_target=0.0, mdot_throttle=0.0, dP_throttle=0.0, dP_P=0.0, dP_I=0.0, dP_D=0.0)[source]

Bases: object

Grid-wide convergence monitors at a single time step, non-dimensional.

Produced by Grid.get_convergence() and consumed by ember.convergence_history.ConvergenceHistory.record_convergence(), which unpacks the station vectors into one scalar column per station.

Station vectors are ordered inlet to outlet, each blade row contributing its upstream then downstream face ([row0_up, row0_dn, row1_up, row1_dn, ...]), so they have length 2 * n_row.

residual: ndarray

Block-mean |residual_nd| per conserved variable, shape (5,), ordered (rho, rhoVx, rhoVr, rhorVt, rhoe). The rhorVt entry is divided per block by block.r_mid_nd so its magnitude is comparable to the rhoVx/rhoVr residuals; this rescaling is for monitoring only.

mdot: ndarray

Station mass flow rates, shape (2 * n_row,), non-dimensionalised by the fluid mass-flux scale.

ho: ndarray

Station stagnation enthalpies, shape (2 * n_row,), non-dimensionalised by u_ref.

s: ndarray

Station specific entropies, shape (2 * n_row,), non-dimensionalised by Rgas_ref.

mdot_target: float = 0.0

Outlet throttle mass flow setpoint [kg/s]; zero when no outlet is throttled. Per passage, not per annulus, unlike mdot.

mdot_throttle: float = 0.0

Mass flow measured at the outlet patch on its last target update [kg/s].

dP_throttle: float = 0.0

Total throttle pressure correction applied at the outlet [Pa].

dP_P: float = 0.0

Proportional contribution to dP_throttle [Pa].

dP_I: float = 0.0

Integral contribution to dP_throttle [Pa].

dP_D: float = 0.0

the throttle is a PI controller. The column is retained so the pickled ConvergenceHistory (.cnv) layout reads in both directions.

Type:

Derivative contribution to dP_throttle [Pa]. Always zero

exception grid.DivergenceError[source]

Bases: RuntimeError

Raised when a block’s conserved field contains a NaN.

A dedicated type lets a solver loop catch divergence precisely and exit cleanly (leaving the invalid field in place for debugging) while genuinely unexpected errors still propagate. See Grid.check_nan().

add_note(object, /)

Exception.add_note(note) – add a note to the exception

args
with_traceback(object, /)

Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.