Patch

Patch classes for specifying boundary conditions on structured blocks.

Limiting index rules

Patches are defined by specifying which block face or part of a face they are on. Every Patch subclass is constructed as:

PatchType(i=..., j=..., k=..., label=...)

taking one argument for each of the three indexing directions i, j, and k, subject to the following rules:

  • The first point in a direction is indexed 0; negative indices wrap around such that -1 is the last point.

  • Indices are inclusive, so i=(0,-1) spans the entire range of i coordinate.

  • Integer arguments are interpreted as a constant value of that index: i=0 means the patch spans the first i face; j=-1 means the patch spans the last j face. Integer arguments are shorthand for, e.g. i=(0,0).

  • Patches must be 2D subsets of an external face of the block. This implies that at least one constant dimension must be specified with a value 0 or -1.

  • Omitting a direction argument implies the patch should include every point in that direction, and is shorthand for e.g. j=(0, -1).

  • The elements of a direction tuple should be in ascending order after negative indices are wrapped. k=(6,4) is not valid, and neither is k=(-1, -2).

Types of patches

The different types of patches (e.g. periodic, inlet, outlet) are represented by subclasses of the abstract base class Patch, but are all initialised by passing in the limiting indices and an optional label for later debugging. Storing boundary condition information or matching connections between patches is handled by methods on the subclasses.

Storage and attachment to blocks

A patch can be constructed in isolation without a Block. However, any property that depends on block geometry – resolving negative indices to absolute coordinates, computing patch size, or accessing block coordinate views – will raise an error until the patch is attached to a block.

Patches are stored in a BlockPatchCollection accessible at block.patches. Adding a patch to this collection automatically attaches it to the block, validates its limits against the block shape, and checks that it does not spatially overlap with any existing patch of the same type on the same face. The collection supports retrieval by integer index, by string label, and by patch type:

p = block.patches[0]           # by index
p = block.patches["inlet_hub"] # by label
ps = block.patches.inlet       # list of all InletPatch objects

The type-grouped properties (inlet, outlet, periodic, rotating, etc.) each return a plain list.

Mutation is through the standard collection interface: append, extend, insert, and del:

block.patches.append(InletPatch(i=0, label="inlet_hub"))
del block.patches["inlet_hub"]

len() returns the number of patches on the block.

A GridPatchCollection at grid.patches provides a corresponding read-only aggregate view across all blocks in a Grid. It supports integer indexing, slicing, iteration, len(), and the same type-grouped properties (e.g. grid.patches.periodic), but does not support string-key access or any mutation methods.

Example usage

# example: patch_examples
import numpy as np
import ember.patch
from ember.block import Block
from ember.fluid import PerfectFluid

# Build a block with axial (x), radial (r), and circumferential (t) coordinates.
# The inlet and outlet are characteristic conditions, so the block has to be
# a whole blade passage: t spans exactly one pitch of the set blade count.
fluid = PerfectFluid(cp=1005.0, gamma=1.4, mu=1.8e-5, Pr=0.7)
block = Block(shape=(4, 5, 6))
block.set_fluid(fluid)
block.set_Nb(12)
pitch = 2.0 * np.pi / 12
block.set_x(np.linspace(0.0, 1.0, 4).reshape(-1, 1, 1) * np.ones((4, 5, 6)))
block.set_r(np.linspace(0.5, 1.0, 5).reshape(1, -1, 1) * np.ones((4, 5, 6)))
block.set_t(np.linspace(0.0, pitch, 6).reshape(1, 1, -1) * np.ones((4, 5, 6)))

# Add inlet, outlet, and periodic patches; appending attaches each to the block.
inlet  = ember.patch.InletPatch(i=0,  label="inflow")
outlet = ember.patch.OutletPatch(i=-1, label="outflow")
block.patches.extend([inlet, outlet])
block.patches.extend([
    ember.patch.PeriodicPatch(k=0,  label="lower"),
    ember.patch.PeriodicPatch(k=-1, label="upper"),
])

print(len(block.patches))           # 4
print(len(block.patches.periodic))  # 2

# Patch shape and size are resolved against the block once attached.
# shape is (1, 5, 6): one i-plane, full j and k extent.
print(inlet.size)                        # 30
print(inlet.shape)                       # (1, 5, 6)
# const_dim is the index of the constant face (0=i, 1=j, 2=k).
print(inlet.const_dim)                   # 0
print(block.patches["lower"].const_dim)  # 2

# Negative indices are stored as-is and resolved to absolute coordinates on demand.
# i=(1,-2), k=(1,-2) on shape (4,5,6): i=-2 resolves to 2, k=-2 resolves to 4.
partial = ember.patch.InviscidPatch(j=-1, i=(1, -2), k=(1, -2), label="tip_partial")
block.patches.append(partial)
print(partial.ijk_lim_abs[0, 1])  # 2
print(partial.ijk_lim_abs[2, 1])  # 4
print(partial.shape)              # (2, 1, 4)

# Use the patch slice to index block coordinate arrays directly.
print(block[inlet.slice].x.shape)   # (1, 5, 6)
print(block.xrt[inlet.slice].shape) # (1, 5, 6, 3)

# Set inlet stagnation conditions and flow angles. Each is imposed on the
# pitchwise mean at every span station, so values are a scalar or a
# spanwise profile.
inlet.set_Po_To(Po=2e5, To=1200.0)
inlet.set_Alpha(0.0)
inlet.set_Beta(0.0)
print(inlet.ho_nd.shape)  # (1, 5, 1)

nj = inlet.shape[1]
Po_span = np.linspace(1.8e5, 2.2e5, nj).reshape(1, nj, 1)
inlet.set_Po_To(Po=Po_span, To=1200.0)

# Set static pressure on the outlet.
outlet.set_P(1e5)
print(outlet.P_nd.shape)  # (1, 5, 1)

# Set angular velocity on a rotating wall patch.
rot_patch = ember.patch.RotatingPatch(j=0, label="hub")
block.patches.append(rot_patch)
rot_patch.set_Omega(500.0)
print(rot_patch.rpm)  # 4774.648

Base classes

class patch.Patch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: ABC

Initialize with start and end indices for each dimension.

Indices are inclusive and a single integer sets a constant value in that dimension. See ember.patch for the full index rules.

Parameters:
  • i (int or tuple) – Start and end indices along the 1st axis.

  • j (int or tuple) – Start and end indices along the 2nd axis.

  • k (int or tuple) – Start and end indices along the 3rd axis.

  • label (str, optional) – String identifier for the patch.

set_i_lim(i)[source]

Set the start and end indices on the i dimension.

Indices are inclusive and a single integer sets a constant value in that dimension. See ember.patch for the full index rules.

Parameters:

i (int or tuple) – Start and end indices along the 1st axis.

set_j_lim(j)[source]

Set the start and end indices on the j dimension.

Indices are inclusive and a single integer is shorthand for a constant face, e.g. j=0 is equivalent to j=(0, 0). See ember.patch for the full index rules.

Parameters:

j (int or tuple) – Start and end indices along the 2nd axis.

set_k_lim(k)[source]

Set the start and end indices on the k dimension.

Indices are inclusive and a single integer is shorthand for a constant face, e.g. k=0 is equivalent to k=(0, 0). See ember.patch for the full index rules.

Parameters:

k (int or tuple) – Start and end indices along the 3rd axis.

set_label(label)[source]

Set patch label.

get_ijk_face(perm=(0, 1, 2), flip=())[source]

Block indices for faces on the patch.

For example the constant k face bounded by (i -> i+1) and (j -> j+1) has indices (i, j, k).

Parameters:
  • perm (tuple of int, optional) – Permutation of the dimensions for the output. Default is (0, 1, 2) which corresponds to (i, j, k).

  • flip (tuple of int, optional) – Dimensions to flip in the output. Default is () which means no flipping.

get_ijk_node(perm=(0, 1, 2), flip=())[source]

Block indices for nodes on the patch.

Parameters:
  • perm (tuple of int, optional) – Permutation of the dimensions for the output. Default is (0, 1, 2) which corresponds to (i, j, k).

  • flip (tuple of int, optional) – Dimensions to flip in the output. Default is () which means no flipping.

attach_to_block_resampled(block, src)[source]

Attach to a resampled block, carrying src’s span-varying state.

src is this patch’s still-attached original on the grid block was resampled from, and is the only place the source span stations can be read from once the copy has been re-attached. The base implementation is a plain attach_to_block(): patch state that is one number, or none at all, follows a block onto any node count without help. Patch types holding a value per span station override this to interpolate it onto the new stations.

Used by resample(), which is what puts a configured patch on a coarser grid – the multigrid hierarchy of ember.solver.Solver.run_fmg(), among others.

attach_to_block(block)[source]

Attach this patch to a block and validate limits against block shape.

Do not call directly; attachment is handled automatically when a patch is added to patches via BlockPatchCollection.

Parameters:

block (Block) – The block this patch belongs to. A weak reference is stored.

check_match(other, rtol=1e-06)[source]

Check if this patch matches another patch for pairing purposes.

Base implementation always returns None. Subclasses should override this method to implement their specific matching criteria.

Parameters:
  • other (Patch) – The other patch to compare with

  • rtol (float, optional) – Relative tolerance for matching

copy()[source]

Return a new unattached patch of the same type with the same limits, label, and boundary condition state.

The returned patch is fully independent: it shares no mutable state with the original and is not attached to any block. Attach it to a block via block.patches.append(copy) before using geometry-dependent properties.

Boundary condition parameters (e.g. stagnation conditions on an inlet, static pressure on an outlet) are copied; any cached solver state that depends on block geometry is not.

update_ref_scales()[source]

Re-derive anything this patch holds in nondimensional form.

Called by Block on every attached patch whenever the reference scales change – set_fluid() and set_L_ref() – after the block has swapped the scales and rescaled its own stored field, so an override reads the new scales straight off self.block.

A patch that caches a nondimensional value must override this and either re-derive it from the raw dimensional quantity it came from, or discard it so it is rebuilt on next use. Leaving a stale nondimensional cache behind does not raise: it silently imposes the wrong physics, which is why this is the one hook a patch subclass has to know about.

What the base implementation handles is the sliced views of the face this class caches at attach_to_block(). They share the block’s data, so they see the rescaled field, but they carry derived-property caches of their own that the block’s own clear_cache does not reach – and a patch reads the face through them.

property block

Access the parent block this patch is attached to.

property block_view

Sliced view of the parent block at this patch location; Block with shape shape.

Equivalent to block[patch.slice]. Cached at attach_to_block() to avoid repeated sliced Block creation overhead.

property block_view_offset_1

Sliced view one layer interior to the patch face; Block with shape shape.

Used to read the outgoing characteristic state (e.g. entropy at a subsonic outlet) from the first interior layer. Equivalent to block[patch.slice] offset by one along the constant dimension. Cached at attach_to_block() to avoid repeated sliced Block creation overhead.

property const_dim

Axis of the constant dimension; int in {0, 1, 2}.

property ien

End index in the i dimension; int.

property ijk_lim_abs

Limits with negative indices resolved to positive; ndarray of shape (3, 2).

property ist

Start index in the i dimension; int.

property jen

End index in the j dimension; int.

property jst

Start index in the j dimension; int.

property ken

End index in the k dimension; int.

property kst

Start index in the k dimension; int.

property label

String identifier for the patch; str or None.

property shape

Extent of the patch in each dimension as (ni, nj, nk); the constant dimension is always 1.

property size

Number of nodes on the patch; int, equal to the product of shape.

property slice

tuple of slice objects for indexing the parent block array.

property xrt_centre

Centre coordinates of the patch as (x, r, t); ndarray of shape (3,).

class patch.RevolutionPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: Patch

Patch on a surface of revolution.

Intermediate base class for patches that require surface-of-revolution geometry (inlet, outlet, mixing). A surface of revolution is an annular or axisymmetric surface where one patch axis is purely circumferential (the pitch direction, along which only theta varies) and the other runs meridionally from hub to tip (the span direction, along which both x and r vary).

When a patch is added to a block via patches, attach_to_block() automatically identifies the span and pitch axes from the block geometry. It raises ValueError if the geometry is not a surface of revolution (i.e. a pitch axis with constant x and r cannot be found).

Two operations are provided. The first is pitch-averaging: computing a circumferentially averaged flow state that subclasses (inlet, outlet, mixing) use to apply boundary conditions.

The second is the interface frame. A surface of revolution need not be a plane of constant \(x\), so a condition written against the velocity through the face cannot read that velocity off \(V_x\). The face normal is derived from the geometry, one direction per span node, and resolve_to_interface() / resolve_from_interface() turn the meridional momentum into that frame and back, held for the duration of a calculation so a subclass can be written entirely in terms of a face-normal velocity and work at any orientation; see NonReflectingPatch for what that buys and what it costs. On a plane of constant \(x\) the rotation is the identity and is skipped, so a subclass pays nothing for the generality it does not use.

set_block_avg()[source]

Compute pitch-averaged conserved variables and store in block_avg.

Uses node-based pitch weights to compute a weighted sum of block_view.conserved_nd over the pitch dimension, writing the result directly into self.block_avg.conserved_nd.

attach_to_block(block)[source]

Attach to block and detect surface-of-revolution geometry.

Calls the base Patch attach, then determines span/pitch dimensions and computes meridional properties. Raises ValueError if the patch is not a surface of revolution.

resolve_from_interface()[source]

Rotate block_view momentum in-place from (norm, span) to (x, r) coordinates.

Inverse of resolve_to_interface:

rhoV_norm -> rhoVx = cosxi * rhoV_norm - sinxi * rhoV_span
rhoV_span -> rhoVr = sinxi * rhoV_norm + cosxi * rhoV_span

A no-op on a face whose frame axis already is \(x\); see chi_node.

resolve_to_interface()[source]

Rotate block_view momentum in-place from (x, r) to (norm, span) coordinates.

Modifies block_view.conserved so that the axial and radial momentum components become the interface-normal and interface-span components:

rhoVx -> rhoV_norm =  cosxi * rhoVx + sinxi * rhoVr
rhoVr -> rhoV_span = -sinxi * rhoVx + cosxi * rhoVr

Uses the pre-computed to-interface rotation matrix, broadcast along span_dim to match the full block shape. A no-op on a face whose frame axis already is \(x\); see chi_node.

property chi_node

Angle of the frame axis from \(+x\), one value per span node.

The meridional-plane angle \(\chi\) that resolve_to_interface() rotates through, so that \(V_n = \cos\chi\, V_x + \sin\chi\, V_r\) is the velocity along the frame axis and \(V_s = -\sin\chi\, V_x + \cos\chi\, V_r\) the one in the surface. Zero on a face whose frame axis is \(+x\).

Returns:

Angle [rad], shaped to broadcast over the patch along its span dimension.

Return type:

array

smooth_pitch_121(field, alpha)[source]

Apply a periodic 1-2-1 smoothing pass along the pitch axis.

Returns alpha * smoothed + (1 - alpha) * field where smoothed is one pass of the discrete 1-2-1 filter f[i] = (f[i-1] + 2*f[i] + f[i+1]) / 4 with periodic wrap along pitch_dim. The pitch direction is circumferential, so periodic wrap is exact for an annular passage.

The 1-2-1 filter has amplification \(\cos^2(k\Delta/2)\): it preserves the pitch mean and smooth variation, and annihilates the Nyquist (sawtooth) mode. Blending with the unsmoothed field by alpha tunes the strength: alpha=1 is a full 1-2-1 pass, alpha=0 leaves the field unchanged.

Parameters:
  • field (ndarray) – Field to smooth; any shape with axis pitch_dim.

  • alpha (float) – Blend factor in [0, 1]. 0 disables, 1 is a full pass.

Returns:

Smoothed field, same shape and dtype as field.

Return type:

ndarray

update_ref_scales()[source]

Re-sync the pitch-averaged block to the parent’s reference scales.

block_avg is a Block of its own, holding coordinates and a pitch-mean flow field nondimensionalised against the scales in force when the patch attached. Both have to follow the parent block, or the pitch average decodes this block’s state against stale scales – for the length scale that means angular momentum, the one conserved variable carrying it, and every quantity derived from the resulting tangential velocity.

Each half is applied only when its scale actually moved, so the commoner call does not put the averaged field through a needless dimensional round trip and its float32 rounding.

property block_avg

Pitch-averaged flow field; Block of shape (nspan,).

Coordinates are the pitch-mean x, r, t at each span station. The conserved variables are populated by calling set_block_avg(); before that call the flow-field arrays contain uninitialised values.

property pitch_dim

Axis of the pitchwise (circumferential) dimension; int in {0, 1, 2}.

Detected automatically from block geometry on attach_to_block(): the axis along which only theta varies while x and r remain constant.

property span_dim

Axis of the spanwise (meridional) dimension; int in {0, 1, 2}.

Detected automatically from block geometry on attach_to_block(): the axis along which x and r vary (hub to tip).

property spf

Span fraction at each node, normalised to [0, 1] by meridional arc-length; ndarray of shape (nspan,).

spf[0] == 0.0 at the hub/start corner and spf[-1] == 1.0 at the tip/end corner. Spacing reflects the actual meridional distances between nodes, not their indices.

property weight_pitch

Pitch weights per node as a fraction of block.pitch; ndarray broadcastable against block_view.

Weights sum to 1 along pitch_dim, so a pitch-averaged scalar field is (field * patch.weight_pitch).sum(axis=patch.pitch_dim).

class patch.NonReflectingPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: RevolutionPatch

The steady non-reflecting boundary condition.

Subclasses declare a description for error messages, the inward face normal (or leave the geometry to decide it), the target space’s name and Jacobian, a mapping of each required target row to the setter that fills it, and which rows are seeded from the flow when nothing prescribes them. They add the setters themselves and nothing else: the characteristic treatment, both harmonic relations and the reversed-flow handling are all here.

update_soln() and advance() are called once per timestep, the first refreshing the reference state to match Giles’ definition of the characteristic variables as perturbations about the time-level-\(n\) average, the second taking the condition’s one under-relaxed step on it. apply() is called once per Runge-Kutta stage and only imposes what those two settled, so the rate of the condition does not scale with the stage count.

The face may be any surface of revolution; the condition works in the interface frame and the module docstring says how. What it is restricted to is a mean state subsonic both normal to the face and absolutely, which is checked and warned about. That restriction does not concern the direction of the flow through the face: a span station whose mean has reversed simply takes the other characteristic split, and drives the quantities that split prescribes toward rows of the same target.

sigma = 0.05

Under-relaxation of the characteristic correction, Giles Eq. 5.25, needed for wellposedness. He suggests 1/N for N pitchwise nodes, applied once per timestep, and advance() takes it exactly once per timestep, so the two are in the same units: set it to 1/N and it is 1/N. The bound is not about the transform amplifying – it cannot, its norm grows only logarithmically – but about how far the pitchwise-nonlocal harmonic relations may spread information in one application while the explicit interior march moves it one cell. Overridden by rf_inlet/rf_outlet at the start of a run.

ho_nd

Read-only view of one row of a patch’s prescribed target vector.

A descriptor rather than a plain attribute, so that the named rows stay views on NonReflectingPatch._target with nothing to re-link when a patch is copied or unpickled, and so that a name the patch’s target space does not carry raises rather than quietly returning whatever that row holds: an inflow condition working in angles has no Vr_nd, and one working in mix variables has no tanAlpha.

Resolution is by name against NonReflectingPatch._target_names of the instance, not by a fixed index, because the row order is a property of the target space and the classes do not share one.

s_nd

Read-only view of one row of a patch’s prescribed target vector.

A descriptor rather than a plain attribute, so that the named rows stay views on NonReflectingPatch._target with nothing to re-link when a patch is copied or unpickled, and so that a name the patch’s target space does not carry raises rather than quietly returning whatever that row holds: an inflow condition working in angles has no Vr_nd, and one working in mix variables has no tanAlpha.

Resolution is by name against NonReflectingPatch._target_names of the instance, not by a fixed index, because the row order is a property of the target space and the classes do not share one.

Vr_nd

Read-only view of one row of a patch’s prescribed target vector.

A descriptor rather than a plain attribute, so that the named rows stay views on NonReflectingPatch._target with nothing to re-link when a patch is copied or unpickled, and so that a name the patch’s target space does not carry raises rather than quietly returning whatever that row holds: an inflow condition working in angles has no Vr_nd, and one working in mix variables has no tanAlpha.

Resolution is by name against NonReflectingPatch._target_names of the instance, not by a fixed index, because the row order is a property of the target space and the classes do not share one.

Vt_nd

Read-only view of one row of a patch’s prescribed target vector.

A descriptor rather than a plain attribute, so that the named rows stay views on NonReflectingPatch._target with nothing to re-link when a patch is copied or unpickled, and so that a name the patch’s target space does not carry raises rather than quietly returning whatever that row holds: an inflow condition working in angles has no Vr_nd, and one working in mix variables has no tanAlpha.

Resolution is by name against NonReflectingPatch._target_names of the instance, not by a fixed index, because the row order is a property of the target space and the classes do not share one.

tanAlpha

Read-only view of one row of a patch’s prescribed target vector.

A descriptor rather than a plain attribute, so that the named rows stay views on NonReflectingPatch._target with nothing to re-link when a patch is copied or unpickled, and so that a name the patch’s target space does not carry raises rather than quietly returning whatever that row holds: an inflow condition working in angles has no Vr_nd, and one working in mix variables has no tanAlpha.

Resolution is by name against NonReflectingPatch._target_names of the instance, not by a fixed index, because the row order is a property of the target space and the classes do not share one.

sinBeta

Read-only view of one row of a patch’s prescribed target vector.

A descriptor rather than a plain attribute, so that the named rows stay views on NonReflectingPatch._target with nothing to re-link when a patch is copied or unpickled, and so that a name the patch’s target space does not carry raises rather than quietly returning whatever that row holds: an inflow condition working in angles has no Vr_nd, and one working in mix variables has no tanAlpha.

Resolution is by name against NonReflectingPatch._target_names of the instance, not by a fixed index, because the row order is a property of the target space and the classes do not share one.

P_nd

Read-only view of one row of a patch’s prescribed target vector.

A descriptor rather than a plain attribute, so that the named rows stay views on NonReflectingPatch._target with nothing to re-link when a patch is copied or unpickled, and so that a name the patch’s target space does not carry raises rather than quietly returning whatever that row holds: an inflow condition working in angles has no Vr_nd, and one working in mix variables has no tanAlpha.

Resolution is by name against NonReflectingPatch._target_names of the instance, not by a fixed index, because the row order is a property of the target space and the classes do not share one.

advance()[source]

Take the boundary condition’s one step; call once per timestep.

The change in the incoming characteristics is scaled by sigma, which is exactly Giles’ Eq. 5.25 correction. This is the whole of a timestep’s boundary-condition change: apply() only imposes the result, once per stage.

Per timestep and not per stage because the harmonic relations couple every pitchwise node to every other through the Hilbert transform, so one application can spread information across the whole pitch while the explicit interior march moves it one cell. Giles’ \(1/N\) for \(N\) pitchwise nodes is the restriction that keeps the two in step, and it is a bound per timestep; taking the step once per stage multiplied the rate by the stage count and left sigma dependent on the integrator.

A no-op until something has been prescribed, so that a patch missing a setter still reports it from apply() rather than from here.

apply()[source]

Impose the non-reflecting condition on the patch.

Called once per Runge-Kutta stage, and imposes only: the outgoing characteristics are re-read from the marched face every stage so a wave reaching the boundary still passes through within the step, while the incoming ones are the state update_soln() last authored. The sigma-relaxed correction that advances that state is taken there, once per timestep, not here.

A node-level override is then given the chance to change what actually reaches the block, and its result is deliberately not carried back into the state the solve is still working from, so a condition that has to depart from its own linear theory somewhere does not thereby corrupt the characteristic state it is still solving on.

attach_to_block(block)[source]

Attach to a block, validate the boundary plane and build the transform.

Safe to call repeatedly; a target of the right shape survives re-attachment, and one of the wrong shape is rebuilt at the new shape rather than silently misread – every prescribed row by re-running the setter that filled it, the rest by re-seeding. Replay is at the original arguments, so a prescribed spanwise profile reaches the setter at the length it was set on and is refused if the span station count has moved. Going onto a coarser grid, as the multigrid hierarchy and resample() do it, therefore goes through attach_to_block_resampled(), which interpolates those profiles onto the new stations first.

attach_to_block_resampled(block, src)[source]

Attach to a resampled block, interpolating prescribed profiles.

A prescribed row is recorded as the setter call that filled it (see replayable()), arguments as the caller gave them – which for a spanwise profile is one number per span station of the grid the patch was configured on. Replaying that call against a block with a different number of stations, as attach_to_block() does on its own, hands the setter a profile of the wrong length and it refuses it. So the replay is deferred and every profile argument re-expressed on this patch’s own stations first, interpolated against span fraction (spf, meridional arc-length) rather than node index, so a prescription follows the geometry it was written against and not the mesh spacing.

Scalar arguments pass through untouched, which is what makes this agree with plain re-attachment wherever plain re-attachment worked.

update_ref_scales()[source]

Re-derive the prescribed target against the block’s current fluid.

Every replayable() setter that filled a row is re-run with the dimensional arguments it was given, in the order it was given them, so a prescribed condition keeps meaning what it says: set_Po_To(4e5, 300) is four bar and three hundred kelvin whatever reference scales and datum come to be in force. That makes set_fluid() safe to call at any point, rather than only before the patches are configured.

Rows nothing prescribed are cleared instead, to be taken afresh from the rescaled face – they are a frozen picture of the flow, and the only honest way to re-express one is to look again. They are cleared before the replay so that a row which is prescribed, and merely happens to be seedable, is refilled by its own setter.

The characteristic state is nondimensional with no dimensional original to return to, so it is dropped and rebuilt from the face. A fluid changed mid-march therefore restarts the condition from the marched state rather than continuing on the one it was solving: a small perturbation, and the alternative is carrying numbers that mean nothing under the new scales.

update_soln()[source]

Refresh the frozen reference state; call once per timestep.

Re-derives the pitchwise-mean state and every Jacobian evaluated on it, which apply() then holds fixed across the Runge-Kutta stages of the step. Snapshots the density first, so a reversed node’s density is relaxed from the start-of-step value rather than from whatever the last stage happened to leave.

Pairs with advance(), which takes the boundary condition’s own step on the reference this leaves behind.

Patch types

class patch.PeriodicPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: Patch

Connected boundary to another block.

This applies to patches which are exactly coincident, and patches which are periodic in the circumferential direction of an annular domain. The patches should come in pairs, and connectivity between the two patches is detected automatically. The conserved variables on the two patches are averaged at each time step to enforce periodicity.

check_match(other, rtol=1e-06)[source]

Check if this PeriodicPatch matches another for pairing purposes.

PeriodicPatch matching requires all x, r, t coordinates to match within tolerance, accounting for periodicity in theta and allowing for permutations and flips.

Parameters:
  • other (Patch) – The other patch to compare with

  • rtol (float, optional) – Relative tolerance for matching

Returns:

(perm, flip) if patches match, None otherwise

Return type:

Optional[Tuple[tuple, tuple]]

class patch.InletPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: NonReflectingPatch

Subsonic inflow boundary condition.

Prescribes stagnation enthalpy \(h_0\), entropy \(s\), yaw angle \(\alpha\) and pitch angle \(\beta\) as pitchwise-mean quantities, while absorbing outgoing acoustic waves rather than reflecting them. All four must be set before apply() is called, via set_ho_s() or set_Po_To() together with set_Alpha() and set_Beta(). Each setter converts its target and stores it nondimensionally in the corresponding row of the prescribed target, published as ho_nd, s_nd, tanAlpha and sinBeta, so the patch must already be attached to a block whose fluid is set.

The angles are what makes this condition different from every other in the family. A physical inlet knows its flow angles and not its velocity magnitude, so \((\tan\alpha, \sin\beta)\) are the right variables here, and the target space’s Jacobian is chic_to_bcond() rather than the base class’s chic_to_mix(). Only rows 2 and 3 of the two Jacobians differ; rows 0, 1 and 4 are identical.

A span station whose mean flow has reversed becomes an outflow, and the base class drives it to the static pressure of row 4 instead. Nothing need be configured for that: set_backflow_P() prescribes the pressure and, left alone, it is seeded from the inflow plane at the first timestep. The angle rows are not solved there – row 4 is static pressure in both target spaces – so the factor of \(V_x\) those rows carry never takes the solve singular.

set_Alpha(Alpha)[source]

Prescribe the inflow yaw angle.

Parameters:

Alpha (float or array) – Prescribed inflow yaw angle \(\alpha\) [deg], measured from the meridional plane; must satisfy \(|\alpha| < 90\). A scalar or an array that broadcasts to shape, of which only the pitchwise mean at each span station is imposed.

set_backflow_P(P)[source]

Prescribe the static pressure imposed where the inflow reverses.

A span station whose pitchwise-mean flow has turned round is an outflow: four of its five characteristics leave the domain and only one enters, so one quantity is prescribed and it is static pressure, not the inflow state. This is that pressure. The other four rows are not imposed at such a station – they are what the outgoing waves carry there.

Calling this is optional. Left alone, the row is seeded once from the pitchwise mean of the inflow plane at the first timestep and frozen there. If a large part of the span ends up reversed the inflow is no longer under control and the boundary wants moving upstream, rather than this value tuning.

Parameters:

P (float or array) – Static pressure \(p\) [Pa]; must be positive and finite. A scalar or an array that broadcasts to shape, of which only the pitchwise mean at each span station is imposed.

See also

ember.patch.OutletPatch.set_backflow_ho_s

The mirror of this, prescribing the inflow state an outflow face falls back on

set_Beta(Beta)[source]

Prescribe the inflow pitch angle.

The angle is measured from the machine axis, \(\tan\beta = V_r / V_x\), whatever the orientation of the face it is prescribed on. The condition itself works in the interface frame, where the same flow makes the angle \(\beta - \chi\) with the frame axis, so what is stored is \(\sin(\beta - \chi)\) for the face angle chi_node. On a face of constant \(x\) the frame axis is \(x\) and the two coincide.

What it must satisfy is that flow actually comes in through the face: \(|\beta - \chi| \leq 90\). On a face of constant \(x\) that is the familiar \(|\beta| \leq 90\), and on a radial or reversed face it is the same condition said properly – a duct running along \(-x\) takes \(\beta = 180\), one flowing inward radially \(\beta = -90\).

Parameters:

Beta (float or array) – Prescribed inflow pitch angle \(\beta\) [deg], measured from the machine axis as \(\arctan(V_r/V_x)\) over the full turn. A scalar or an array that broadcasts to shape, of which only the pitchwise mean at each span station is imposed.

set_ho_s(ho, s)[source]

Prescribe the inflow stagnation enthalpy and entropy.

Both are measured from the fluid datum state where \(u = s = 0\) at \((p_\mathrm{dtm}, T_\mathrm{dtm})\), the same convention as ho and s; only differences are physically meaningful, so these are not \(c_p T_0\) and \(c_p \log(\ldots)\). Use set_Po_To() to prescribe a stagnation state instead.

Parameters:
  • ho (float or array) – Prescribed stagnation enthalpy \(h_0\) [J/kg]. A scalar or an array that broadcasts to shape, of which only the pitchwise mean at each span station is imposed.

  • s (float or array) – Prescribed entropy \(s\) [J/kg/K].

set_Po_To(Po, To)[source]

Prescribe the inflow stagnation pressure and temperature.

Converted to the stagnation enthalpy and entropy of set_ho_s() using the fluid of the block this patch is attached to. The prescription is what survives, not the conversion: the pressure and temperature given here are kept, and a later change of fluid re-converts them against the new one, so this stays the stagnation state asked for rather than whatever number the old reference scales and datum made of it.

Parameters:
  • Po (float or array) – Prescribed stagnation pressure \(p_0\) [Pa]; must be positive. A scalar or an array that broadcasts to shape, of which only the pitchwise mean at each span station is imposed.

  • To (float or array) – Prescribed stagnation temperature \(T_0\) [K]; must be positive.

property Alpha

Prescribed inflow yaw angle \(\alpha\) [deg]. Inverse of set_Alpha().

property Beta

Prescribed inflow pitch angle \(\beta\) [deg], measured from the machine axis.

Inverse of set_Beta(): the stored sine is that of the face-frame angle \(\beta - \chi\), which set_Beta() keeps within \(\pm 90\) degrees, exactly numpy.arcsin’s range, so the recovery is exact up to that wrap.

property Po

Prescribed inflow stagnation pressure [Pa].

Recovered from the currently stored ho_nd and s_nd through the block’s fluid, so it reads back the prescribed state however it was set – by set_Po_To() or by set_ho_s() – rather than undoing one setter in particular.

See also

To

The stagnation temperature this state also implies

property To

Prescribed inflow stagnation temperature [K]. See Po.

class patch.OutletPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: NonReflectingPatch

Subsonic outflow boundary condition.

Prescribes the static pressure \(p\) as a pitchwise-mean quantity at each span station, while absorbing outgoing waves rather than reflecting them. It must be set before apply() is called, via set_P(), which stores its target nondimensionally in P_nd, so the patch must already be attached to a block whose fluid is set.

Giles takes the mean-mode residual against the flux-averaged pressure; the mean here is the weighted pitch mean of weight_pitch, the same average every other residual in the family is taken against.

set_adjustment() adds a spanwise radial-equilibrium profile to the prescribed pressure, re-derived from the solution once per timestep by update_target(). Without it the prescribed pressure is imposed on every span station alike, which for a swirling exit flow fights the centrifugal pressure gradient the flow is trying to establish.

set_throttle() turns the prescribed pressure into a starting point rather than the condition: a proportional-integral controller moves the level each timestep until the patch passes a target mass flow. The pressure is still what the boundary imposes – the throttle only chooses which pressure – so the characteristic treatment is untouched by it.

set_backflow_ho_s() (or set_backflow_Po_To()) and set_backflow_Vt() prescribe the inflow state a reversed span station is driven to; see those methods and the module docstring. Its meridional direction is not prescribed and cannot be: backflow comes in normal to the exit surface, so the row that would carry it is pinned at zero. Unlike an angle, a meridional velocity cannot be resolved onto a face of arbitrary orientation without knowing the normal component, which is what the reversed-station solve derives from \(h_0\) – so there is nothing consistent for a setter to mean.

set_adjustment(radial_equilibrium=True, rf=0.1)[source]

Configure the spanwise adjustment to the prescribed pressure.

Swirling flow leaving a blade row carries a centrifugal radial pressure gradient. Prescribing one pressure at every span station fights it and induces unnatural streamline curvature, so the adjustment adds the profile satisfying \(dp/dr = \overline{\rho V_\theta}\, \overline{V_\theta}/r\), integrated from the hub, where the prescribed pressure is then the value enforced. It is re-derived from the solution by update_target() once per timestep and relaxed toward the new value:

\[\Delta p^\mathrm{new} = \mathit{rf}\,\Delta p + (1 - \mathit{rf})\,\Delta p^\mathrm{old}\]

Off unless this method is called, and incompatible with a non-scalar set_P(), which would prescribe a spanwise profile of its own and double count.

There is no dynamic-head term. Such an offset has zero pitchwise mean at every span station by construction, and this patch imposes nothing but pitchwise means, so it would be annihilated exactly.

Parameters:
  • radial_equilibrium (bool, optional) – Include the radial equilibrium offset. Default True; False configures an adjustment that adjusts nothing.

  • rf (float, optional) – Relaxation factor applied to the profile each step. Default 0.1.

set_backflow_ho_s(ho, s)[source]

Prescribe the stagnation enthalpy and entropy imposed where the exit flow reverses.

Reversal is carried at two levels, and both draw on the four backflow quantities this and its companion setters prescribe.

A span station whose mean has reversed is genuinely an inflow plane and is treated as one. Four of its five characteristics turn incoming, so four quantities have to be prescribed, and the four backflow rows are exactly they; the one wave still leaving, the downstream-running pressure wave, is carried through from the interior as always. The prescribed static pressure is not imposed at such a station: pressure is what the free wave carries there. If a large part of the span ends up reversed the exit level is no longer under control, and the boundary wants moving downstream rather than the condition made cleverer.

A node whose interior neighbour is pushing flow inward, at a station whose mean is still forward, is overwritten with the same four quantities and a density, the one quantity those four leave free, taken from the interior: relaxed from its start-of-step value toward the current one at a rate that falls away with the local axial Mach number, and capped to keep the axial velocity real. There is no characteristic split to change at that level – the split belongs to the station’s mean, and the Hilbert transform couples every node of a station to every other – so this one is a limiter on the linear theory, applied to what reaches the block and kept out of the state the solve carries forward.

The rows that can be prescribed are set independently, by this method or set_backflow_Po_To() for the thermodynamic pair and by set_backflow_Vt() for the swirl, so a run can prescribe one and leave the rest seeded. The meridional direction is not among them: the backflow comes in normal to the exit surface, so the velocity in the surface is pinned at zero. See the class docstring.

Both quantities here are measured from the fluid datum state where \(u = s = 0\) at \((p_\mathrm{dtm}, T_\mathrm{dtm})\), the same convention as ho and s.

Calling any of the four is optional. Left alone, the rows are seeded once from the pitchwise mean of the exit plane at the first timestep and frozen there.

Parameters:
  • ho (float or array) – Stagnation enthalpy [J/kg]. A scalar or a spanwise profile. Only the pitchwise mean at each span station is imposed.

  • s (float or array) – Specific entropy [J/(kg K)].

See also

ember.patch.InletPatch.set_backflow_P

The mirror of this, prescribing the pressure an inflow face falls back on

set_backflow_Po_To(Po, To)[source]

Prescribe the backflow stagnation state as pressure and temperature.

Converted to the stagnation enthalpy and entropy of set_backflow_ho_s() using the fluid of the block this patch is attached to, and writing the same two target rows. The prescription is what survives, not the conversion: a later change of fluid re-converts the pressure and temperature given here against the new one.

See set_backflow_ho_s() for what the backflow rows do.

Parameters:
  • Po (float or array) – Stagnation pressure \(p_0\) [Pa]; must be positive and finite. A scalar or a spanwise profile, of which only the pitchwise mean at each span station is imposed.

  • To (float or array) – Stagnation temperature \(T_0\) [K]; must be positive and finite.

set_backflow_Vt(Vt)[source]

Prescribe the tangential velocity imposed where the exit flow reverses.

See set_backflow_ho_s() for what the backflow rows do.

Parameters:

Vt (float or array) – Tangential velocity \(V_\theta\) [m/s]. A scalar or a spanwise profile, of which only the pitchwise mean at each span station is imposed.

set_P(P)[source]

Prescribe the outlet static pressure.

Imposed on the pitchwise mean at each span station, not node by node, so a value varying along the pitch is averaged before use. With set_adjustment() configured this is the hub value and the spanwise profile follows from radial equilibrium; without it, a spanwise array prescribes the profile directly.

Parameters:

P (float or array) – Prescribed static pressure \(p_\mathrm{out}\) [Pa]; must be positive and finite. A scalar or any array that broadcasts to shape.

set_throttle(mdot_target, Kp=0.5, Ki=0.002)[source]

Throttle the outlet to a target mass flow.

Turns set_P() from the condition into a starting point. Each timestep update_target() measures the mass flow through the patch and a proportional-integral controller moves the prescribed level until the two agree:

\[\varepsilon = \frac{\dot m - \dot m_\mathrm{target}} {\dot m_\mathrm{target}}, \qquad \frac{\Delta p_\mathrm{throttle}}{p_\mathrm{ref}} = K_p\, \varepsilon + K_i \sum \varepsilon \,\mathrm{cfl}\]

the sum running over timesteps. Raising the back pressure reduces the flow, so the sign is as written: a mass flow above target pushes the pressure up. What the boundary imposes is still a pressure, and the characteristic treatment is untouched – the throttle only chooses which pressure.

The gains are dimensionless and should not need tuning. For a duct or blade row passing \(\dot m \sim A\sqrt{2\rho(p_0 - p)}\), the steady sensitivity of mass flow to exit pressure is

\[\frac{d\dot m}{\dot m} = -\frac{dp}{2q}, \qquad q = \tfrac{1}{2}\rho V_m^2\]

so a correction of \(2q\,\varepsilon\) cancels the error outright: a Newton step whose natural scale is the exit dynamic head. That is exactly the scale the nondimensionalisation already works in, since \(p_\mathrm{ref} = \rho_\mathrm{ref} V_\mathrm{ref}^2\) with \(V_\mathrm{ref}\) a typical convection velocity. Hence the correction above is formed nondimensionally with no scale factor written anywhere, and \(K_p = 1\) is the notional Newton step.

The default is half that, because a pure Newton step overshoots and rings: the mass flow answers a change in exit pressure only after a wave has crossed the domain. Proportional action alone would then settle at a standing droop, since it can hold a correction only in proportion to an error, and the correction wanted at the target is not zero; the integral is what removes it. Because the scale is a fixed reference quantity rather than the dynamic head of the current solution, neither gain depends on the flow field or on how good the initial guess was.

The integral is weighted by the CFL, not by the step. The proportional term is memoryless and safe under any lag: it holds a fixed correction until the flow answers. The integral is not – over the steps the domain takes to respond it keeps piling on correction for an error it has already acted on – so its gain has to be paced against how much ground each step covers. Under local timestepping that is the Courant number, so a march at twice the CFL needs half as many steps and \(K_i \sum \varepsilon\,\mathrm{cfl}\) keeps one gain valid across a CFL sweep. ember.grid.Grid.update_bconds() passes the march’s cfl down; nothing is held on the patch.

The step count also scales with mesh density, and with whatever multigrid and residual smoothing are doing, and none of that is knowable from here: a patch can count the cells along its own normal but not along the flow path, which for a multi-block machine, or a patch that is not on a streamwise face, is not the same number. Refining the mesh may therefore want \(K_i\) revisited. Changing the CFL does not.

The price of the fixed pressure scale is a loop gain of \(p_\mathrm{ref} / 2q = (\rho_\mathrm{ref}/\rho) (V_\mathrm{ref}/V_m)^2\) rather than exactly one, so the gains do assume the reference scales are representative of the flow. A V_ref far from the exit velocity moves the loop gain by its square, and is the one case where these want retuning.

Only one outlet patch in a grid may be throttled; the solver refuses a grid carrying more. Two patches driving independent controllers at the same target would each apply the full correction for an error they share.

Parameters:
  • mdot_target (float or None) –

    Target mass flow \(\dot m_\mathrm{target}\) [kg/s], through one passage rather than the whole annulus, matching what ember.average.flow_mass() returns for this patch. Must be positive and finite. Pass None to clear the throttle, which reverts the boundary to the pressure set_P() prescribed: the controller’s correction is derived from the gains and the error sum, so clearing those puts it back at zero on the next update_target(). Clearing is therefore the inverse of setting, and does not depend on the order it is done in relative to set_P().

    To keep the operating point the controller found rather than revert to the one that was asked for, re-prescribe it first:

    patch.set_P(patch.P_throttle)
    patch.set_throttle(None)
    

    which is what a run does when it throttles to find an operating point and then holds it. See P_throttle.

  • Kp (float, optional) – Proportional gain, dimensionless. Default 0.5, half the Newton step of 1 above.

  • Ki (float, optional) – Integral gain, dimensionless. Default 0.002, from a sweep on a square duct at cfl=5: ten times that rings, with six crossings of the target at a period of 360 steps, and a third of it never arrives. Since the integral only has to remove the droop, erring low costs settling time while erring high costs stability.

See also

ember.patch.OutletPatch.get_throttle_stats

Controller state, as logged to the convergence history

get_throttle_stats()[source]

Return the throttle state, for the convergence history.

All six values are zero when no throttle is set, which is what ember.grid.Grid.get_convergence() records for a grid whose outlet holds a plain pressure.

Only the measured mass flow and the running error sum are stored; the correction terms below are derived here from them and the gains, so there is no second copy of the controller state to keep in step. The values are those of the last update_target(), so under Grid.update_bconds(freeze=True), which skips it, they stay at the last step the controller actually acted on.

Returns:

mdot_target the setpoint [kg/s]; mdot_throttle the mass flow last measured at the patch [kg/s]; dP_throttle the total correction \(\Delta p_\mathrm{throttle}\) [Pa]; dP_P and dP_I its proportional and integral parts [Pa]; dP_D always zero, the controller being PI. The derivative column is retained so the .cnv record layout stays readable in both directions.

Return type:

dict

attach_to_block(block)[source]

Attach to a block and pin the in-surface backflow velocity at zero.

Done here rather than in a setter because it is not a prescription the user makes but a property of the condition; and after the base class, which is what allocates the target this writes into. Re-pinned on every attach, since a target rebuilt at a new shape comes back zeroed and unset.

update_ref_scales()[source]

Re-derive the prescribed pressure and drop the spanwise adjustment.

The base class replays set_P(), which rebuilds both the level and the target row it feeds. What is left is the adjustment relaxation state, an integral over a solution and a geometry expressed in the old scales: it is dropped, and the next update_target() re-derives the profile from the rescaled solution.

The throttle needs nothing done to it. Its error is a ratio of two dimensional mass flows and its correction is nondimensional by construction, so neither has a dimensional original to return to and the wound-out integral carries over intact. The pressure that correction comes to does follow the new \(p_\mathrm{ref}\), which is the same rule the gains obey in the first place; see set_throttle().

update_target(cfl=1.0)[source]

Recompute the pressure target for the current timestep.

Advances the throttle of set_throttle() and applies the spanwise adjustment of set_adjustment(), if either is configured. Should be called once per outer timestep before the Runge-Kutta stages; ember.grid.Grid.update_bconds() does so. The throttle’s integral advances once per call, so calling this per Runge-Kutta stage instead would scale the integral gain with the stage count.

The two adjustments are orthogonal and simply add: the throttle moves the level of the pitchwise-mean pressure, and radial equilibrium shapes its spanwise profile about that level.

Parameters:

cfl (float, optional) – CFL number of the march, weighting the throttle’s integral so that one \(K_i\) holds across a CFL sweep; see set_throttle(). Passed down by ember.grid.Grid.update_bconds() rather than held on the patch, so it is always the number the march is actually running at. Default 1, integrating per call, which is all a patch stepped by hand outside a march can mean. Unused without a throttle.

property Ki

Integral gain of the throttle, dimensionless.

Zero when no throttle is set; see Kp.

property Kp

Proportional gain of the throttle, dimensionless.

Zero when no throttle is set, since set_throttle() clears both gains along with the setpoint; test mdot_target against None to tell an unthrottled patch from one deliberately given a zero gain. Set through set_throttle(), which documents what the value means.

property mdot_target

Throttle setpoint [kg/s], or None when the patch holds a pressure.

Read by ember.grid.Grid.get_convergence(), and by the solver’s throttle validation, to find the throttled outlet; set through set_throttle().

property P

Outlet static pressure field as imposed [Pa], shaped like the patch.

The whole prescription, node by node, read back from P_nd. Not the inverse of set_P() and not a level: a scalar passed to set_P() comes back broadcast over the face, set_throttle() moves what is here away from what was passed, and set_adjustment() shapes it along the span about a level that is then the hub value rather than any average of this.

For the single number the boundary is holding at, which is what a caller recording an operating point wants, see P_throttle.

property P_throttle

Static pressure level the throttle has arrived at [Pa].

The inverse of set_P(), moved by whatever the controller has done to it:

\[p_\mathrm{throttle} = p_\mathrm{out} + \Delta p_\mathrm{throttle}\]

so with no throttle set this is simply the prescribed pressure, and with one it is the operating point the controller has reached. Scalar whenever a throttle is set, set_throttle() having refused a non-scalar prescription; otherwise whatever shape set_P() was given.

This is the number to record when a run throttles to an operating point and something else has to reproduce it later, and the number to re-prescribe to hold that point:

patch.set_P(patch.P_throttle)   # keep what the controller found
patch.set_throttle(None)

Held apart from P because that is the imposed field rather than its level, and the two differ under set_adjustment() — see calc_radial_equilibrium(), whose profile is anchored at the hub, so the level is the hub value and not the mean of P.

See also

ember.patch.OutletPatch.get_throttle_stats

The correction alone, with the rest of the controller state

class patch.MixingPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: NonReflectingPatch

One side of a non-reflecting mixing plane.

Takes its whole prescribed target from the cross-plane exchange, so it needs no setter, and seeds every row from its own pitchwise mean before the first exchange has happened. Which side of the plane it is on, and so which rows it actually imposes, is settled from the flow rather than declared; see the module docstring.

A note on which average is which. The communicator evaluates its Jacobians on the symmetrised cross-plane average, so both sides linearise the interface jump about the same state; each patch’s own frozen reference state stays its local pitchwise mean, because the reference-state calculation calls set_block_avg() itself and so re-derives it after the exchange has overwritten block_avg. The split is deliberate and follows Saxer: the interface jump belongs to the interface, the boundary condition to the boundary.

set_block_avg()[source]

Pitch-average the face, in interface coordinates unless the plane is reflective.

Overridden only to hold the rotation: the communicator calls this from outside any of the boundary condition’s own entry points, and the cross-plane average it builds has to be in the same frame on both sides. See set_flux_avg().

A reflective plane takes the average in (x, r) instead, and wants to. Its conserved variables are the absolute-frame \([\rho, \rho V_x, \rho V_r, \rho r V_\theta, \rho e]\), which the two sides can compare directly: they share a meridional geometry and a radius, so nothing has to be resolved into a common frame first. The rotation would in fact be actively wrong before the frame has settled, since until then the two sides’ provisional axes are antiparallel and their normal components carry opposite signs; and the frame never does settle on a reflective plane, because settling happens inside the very window this skips.

set_flux_avg()[source]

Compute pitch-averaged node fluxes and store in flux_avg_nd.

Called by MixingCommunicator before reading flux_avg_nd to form the cross-plane flux difference of Saxer Eq. 5.65.

Taken in interface coordinates, so what the kernels below compute as the x-direction flux is the flux through the face whatever the face’s orientation. The two sides of a plane share a meridional geometry and settle to opposite signs, so their frame axes coincide and the difference the communicator takes is between fluxes resolved the same way.

set_target(target=None)[source]

Set the exchanged target, from an explicit array or this side’s own mean.

Called by MixingCommunicator after each exchange. Omitting target re-seeds from the pitchwise mean of the current face state instead, which is how a patch that has not yet been exchanged gets a consistent starting point.

Parameters:

target (array of shape (nspan, 5), optional) – Nondimensional [ho, s, Vr, Vt, P] target values.

set_uniform(cons=None)[source]

Set the pitch-uniform conserved state a reflective plane imposes.

Called by MixingCommunicator after each exchange, with the average of the two sides’ circumferential means. Omitting cons seeds from this side’s own circumferential mean instead, which is how a face that has not yet been exchanged has something physical to impose; apply() does that for itself rather than imposing zeros.

Parameters:

cons (array of shape (nspan, 5), optional) – Nondimensional conserved variables, in (x, r) components and in this patch’s own span order.

get_target()[source]

Return the exchanged target, a nondimensional (nspan, 5) array.

Rows are [ho, s, Vr, Vt, P]. Read by MixingCommunicator to form the symmetrised baseline the cross-plane mismatch is relaxed onto, which is why a patch that has never been exchanged is seeded here rather than left at zero.

get_uniform()[source]

Return the reflective plane’s imposed state, nondimensional (nspan, 5).

Seeded from this side’s own circumferential mean if nothing has been exchanged onto it yet, for the same reason get_target() seeds.

advance()[source]

Take the boundary condition’s step; a no-op on a reflective plane.

A reflective plane has no state of its own between exchanges: what it imposes is settled entirely by the communicator, and apply() imposes it outright.

apply()[source]

Impose the condition on the face.

The plain plane runs the non-reflecting condition of apply(). A reflective one overwrites the whole face with the pitch-uniform state the last exchange left, span station by span station and every stage, hub and casing nodes included – there is no characteristic content to preserve and no relaxation to take, so there is nothing here to be gradual about.

attach_to_block(block)[source]

Attach to a block, validate the plane, and allocate the flux average.

Drops any settled frame: the base class has just rebuilt the provisional one from the new block’s geometry, so a stale settled flag would pin the patch to that provisional frame for the rest of the run and never let the flow correct it.

check_match(other, rtol=1e-05)[source]

Check whether this patch pairs with another across a mixing plane.

Pairs only with the opposite side of a mixing plane running the same treatment, which the inward face normal and the reflective flag identify between them: the two sides of one plane face each other, so their interiors lie on opposite sides of it. Matching is then on meridional geometry alone, so the two sides may differ in pitchwise resolution and blade count but not in spanwise resolution.

Parameters:
  • other (Patch) – The other patch to compare with.

  • rtol (float, optional) – Relative tolerance for matching.

Returns:

None if the patches do not match. False if they match with no spanwise flip needed. True if they match but other’s span must be reversed. Always test with is not None; do not use as a bare truthiness check since False is a valid match result.

Return type:

bool or None

update_soln()[source]

Refresh the frozen reference state; a no-op on a reflective plane.

Nothing on a reflective plane is linearised about a mean state, so there is no reference to freeze.

property flux_avg_nd

Pitch-averaged flux array, shape (nspan, 5); populated by set_flux_avg() and read by MixingCommunicator to form the cross-plane flux difference.

class patch.NonMatchPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: Patch

Non-matching interface between block faces with different node counts.

Connects two block faces that occupy the same physical space but have different nodal distributions. Conserved variables are transferred via bilinear parametric interpolation by NonMatchCommunicator, which precomputes parametric coordinates at initialisation and interpolates each step.

Unlike PeriodicPatch, which requires identical node distributions, and MixingPatch, which exchanges only the pitch average, NonMatchPatch transfers the full pointwise solution across the interface.

Corner x-r coordinates must match between paired faces. Pitchwise and spanwise node counts may differ freely.

check_match(other, rtol=1e-06)[source]

Check if this NonMatchPatch matches another for pairing purposes.

NonMatchPatch matching requires only x,r coordinates to match at corners, allowing for different nodal distributions in the varying dimensions. Theta coordinates are ignored to allow for circumferential mismatch.

Parameters:
  • other (Patch) – The other patch to compare with

  • rtol (float, optional) – Relative tolerance for matching

Returns:

(perm, flip) if patches match, None otherwise

Return type:

Optional[Tuple[tuple, tuple]]

class patch.RotatingPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: Patch

Rotating wall boundary with a prescribed angular velocity.

Overrides the wall angular velocity on the patch face used by the Fortran flux routines to compute wall-relative velocities. Faces without a RotatingPatch default to the block angular velocity Omega.

Angular velocity must be set via set_Omega() or set_rpm() before the solver runs.

set_Omega(Omega)[source]

Set the wall angular velocity.

Parameters:

Omega (float) – Angular velocity [rad/s]. Must be a scalar.

set_rpm(rpm)[source]

Set the wall angular velocity from revolutions per minute.

Converts via \(\Omega = \mathrm{rpm} \cdot 2\pi / 60\).

Parameters:

rpm (float) – Rotational speed [rev/min].

property Omega

Angular velocity [rad/s].

property rpm

Angular velocity in revolutions per minute [rpm].

class patch.CoolingPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: Patch

Cooling flow through a boundary.

set_cool(type=None, mass=None, pstag=None, tstag=None, sangle=None, xangle=None, mach=None, angle_def=None)[source]

Set cooling parameters.

class patch.InviscidPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: Patch

Frictionless slip-wall boundary.

Marks a face as impermeable but frictionless. The patch is included in the slip collection, which causes the Fortran flux routines to apply a slip (zero normal-velocity) condition without viscous stress. Use this instead of a no-slip wall when viscous effects on that face should be suppressed, for example on a symmetry plane or an inviscid endwall.

class patch.CuspPatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: Patch

Cusp boundary at a modelled trailing edge.

Marks the faces either side of a zero-thickness trailing edge where two block faces meet at a cusp. Each solver step the conserved variables at the cusp nodes (the axial range covered by the patch) are averaged across the two faces so that the solution remains continuous at the trailing edge.

Must be on a constant-k face and must span the full j extent of the block. Paired with the corresponding face on the other side of the trailing edge via check_match().

attach_to_block(block)[source]

Attach to block and validate cusp patch constraints.

check_match(other, rtol=1e-06)[source]

Check if this CuspPatch matches another for pairing purposes.

CuspPatch matching requires x and r coordinates to match within tolerance, but allows theta to differ.

Parameters:
  • other (Patch) – The other patch to compare with

  • rtol (float, optional) – Relative tolerance for matching

Returns:

(perm, flip) if patches match, None otherwise

Return type:

Optional[Tuple[tuple, tuple]]

class patch.ProbePatch(i=(0, -1), j=(0, -1), k=(0, -1), label=None)[source]

Bases: Patch

Passive flow-sampling probe.

Records flow history at a point, face, or interior plane. Unlike boundary patches it does not affect wall/slip treatment of the sampled face – it is deliberately excluded from PERMEABLE_TYPES/SLIP_TYPES. The two class flags below relax the generic patch rules so a probe can be placed on an interior constant plane and may coincide with another patch.

Collections

class patch.BlockPatchCollection(block)[source]

Bases: _LabelledList

Initialise with a reference to the parent block.

Parameters:

block (Block) – The block whose boundary conditions this collection manages.

append(patch)[source]

Attach patch to the block, validate limits and overlaps, then add.

extend(patches)[source]

Attach each patch to the block, validate limits and overlaps, then add all.

insert(index, patch)[source]

Attach patch to the block, validate limits and overlaps, then insert at index.

property cooling

All CoolingPatch objects.

property cusp

All CuspPatch objects.

property inlet

All InletPatch objects.

The inflow side of a mixing plane is not one of these: it shares the characteristic base class but is driven by a cross-plane exchange rather than a prescribed inflow state, and is a sibling type rather than a subclass; see mixing.

property inviscid

All InviscidPatch objects.

property mixing

All MixingPatch objects.

Both sides of a mixing plane, inflow and outflow.

property outlet

All OutletPatch objects.

The outflow side of a mixing plane is not one of these: it shares the characteristic base class but is driven by a cross-plane exchange rather than a prescribed exit pressure, and is a sibling type rather than a subclass; see mixing.

property periodic

All PeriodicPatch objects.

property permeable

Patches through which flow passes (non-wall faces).

Includes InletPatch, OutletPatch, PeriodicPatch, MixingPatch, NonMatchPatch, and CuspPatch. Used to identify which boundary faces are not solid walls when computing block boundary fluxes.

property probe

All ProbePatch objects.

property rotating

All RotatingPatch objects.

property slip

Patches that impose no friction (permeable faces and inviscid walls).

Union of permeable and InviscidPatch. Used when applying viscous wall functions: faces in this set are treated as frictionless so no friction is applied at those boundaries.

clear()

Remove all items from 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

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 patch.GridPatchCollection(grid)[source]

Bases: object

Initialise with a reference to the parent grid.

Parameters:

grid (Grid) – The grid whose block patch collections this view aggregates.

property cooling

Return all cooling patches from all blocks.

property inlet

Return all inlet patches from all blocks.

property mixing

Return both sides of every mixing plane, from all blocks.

property outlet

Return all outlet patches from all blocks.

property periodic

Return all periodic patches from all blocks.

property probe

Return all probe patches from all blocks.

property rotating

Return all rotating patches from all blocks.