Mean-line designers

Classes linking mean-line flow fields to design variables.

A MeanLineDesign turns aerodynamic variables into a MeanLine flow field and back again. Writing the mean-line design for a new machine is a single subclass:

class MyStage(MeanLineDesign):
    type: ClassVar[str] = "my_stage"
    n_row: ClassVar[int] = 2

    psi: float
    phi: float
    Po1: float = 1e5

    def forward(self, fluid: ember.fluid.Fluid):
        ml = self.allocate(fluid)
        ...
        return ml

    def backward(self, ml): ...

The Tutorial works through such a class in full, from an empty file to a designed fan; this page document the class in more detail. The data structure storing the flow field is documented at Mean-line flow field.

The design contract

A design declares two class variables, writes two methods, and inherits the rest:

type

The name an input file asks for, under mean_line:.

n_row

Number of blade rows the design describes. The mean line it builds has shape (2, n_row).

MeanLineDesign.forward()

Written by the design: build a mean line from the design variables.

MeanLineDesign.backward()

Written by the design: recover the design variables from a mean line.

MeanLineDesign.allocate()

Provided: an empty mean line of the right size and fluid.

MeanLineDesign.design()

Provided: run forward, check the round trip, freeze the result.

MeanLineDesign.solve_for()

Provided: drive unknowns until backward reports the targets asked for.

A built-in design defined in the turbigen package is registered automatically. A new user-created design need not be installed: it is picked up from any turbigen_plugins directory beside the input file, or in any directory above it.

In addition to the class variables, type and n_row, a design declares its design variables as dataclass fields. Values for the design variables are taken from under the mean_line: key in the input file, converted to the annotated type and rejected if that fails. A field with no default is required, and omitting it from the input file is an error. A field with a default is optional, and the defaulted value is recorded in output.yaml for future reproducibility.

Design process

Loading an input file converts its mean_line: mapping into an instance of the class named by type, with defaults filled in. turbigen then calls design() on that instance, passing the working fluid specified by fluid:, and that method runs the design:

  1. forward() builds a MeanLine;

  2. backward() inverts it, and the result is compared against the nominal design variables;

  3. mass is checked to be conserved through the machine;

  4. the MeanLine is frozen, so that every stage which follows — annulus, blades, mesher, post-processing — reads it and cannot write to it.

Because the check runs there, a nominal MeanLine that exists is the requested design, and there is no third state between what was asked for and what was achieved.

forward() should start with allocate(), which returns an empty MeanLine of the right shape and working fluid, fills it in using the setters documented at Mean-line flow field, and returns it. It should make no assumption about the equation of state: a design written in terms of enthalpy and entropy works for a perfect gas and a real one alike. Forward method builds one line by line from the design equations.

The fluid it is passed is the equation of state named by fluid:, whose interface is documented in ember.fluid. Thermodynamic properties come from its two method families: a set_X_Y returns the density and internal energy pair for the two properties named — set_P_T(), set_P_s(), set_P_h(), set_h_s() and the rest — and a get_Z evaluates one property from that pair, so get_h(), get_s(), get_T(), get_a() and so on. The whole interface is documented in ember.fluid.

backward() goes the other way, returning a plain dict keyed by field name. A key that is not a field is reported for information but never checked, so a design is free to return whatever else is worth printing next to a CFD solution; a field mapped to None declares itself deliberately not invertible, and is skipped; a field with no key at all warns once, naming the variable that can no longer be checked or reported. backward() can run on a nominal design, or a mixed-out CFD solution — it is the single definition of what each design variable means. Backward method writes one for the design variables of a fan.

Implicit design problems

Where possible, forward() should build the mean line explicitly from the design variables, but there are often situations where the mean line cannot be built directly from the natural choice of design variables. solve_for() adjusts unknowns until the residual calculated through backward() meets the targets asked for, thus solving implicit design problems.

For example, a turbine stage at given stator exit Mach number cannot be built in one pass: that Mach number depends on the temperature, which depends on the static state and loss. So the design puts the whole construction in a closure over the quantities it does not yet know — here the blade speed and the three swirl velocities — and asks for the values of those which make backward() report the design variables asked for:

def build(U, Vt1, Vt2, Vt3_rel):
    """Fill in `ml` for one trial set of unknowns."""
    ...

self.solve_for(
    ml,
    build,
    unknowns={"U": U0, "Vt1": Vt1_0, "Vt2": Vt2_0, "Vt3_rel": Vt3_rel_0},
    targets={
        "psi": self.psi,
        "Ma2": self.Ma2,
        # Repeating stage: the flow leaves as it entered
        "Alpha1": "Alpha3",
    },
    name="stage",
)

build is called as build(**unknowns) and writes into the same MeanLine every time. The values in unknowns are initial guesses, which may be scalars or arrays, and the guess must itself give a valid mean line (but not necessarily one that meets the targets). During iteration, any calls to backward() that error have a penalty residual applied.

There must be at least as many targets as unknowns, or the solve is refused as underdetermined. On success the mean line is left rebuilt at the solution, so forward can return it directly, and the solved unknowns are returned as a dict for a design that wants to keep them.

A numeric target is a value that key must take; a string target names another key of backward()’s output that it must equal.

Thermodynamic datum

A design that expects high temperatures and pressures should move the fluid dynamic datum before allocating the mean line. For example, if the inlet stagnation conditions are specified as design variable fields:

ml = self.allocate(fluid.change_datum(P_dtm=self.Po1, T_dtm=self.To1))

See Datum state for more detail on this part of the fluid API.

exception turbigen.design.DesignError

Bases: Exception

A mean-line design could not be produced.

class turbigen.design.MeanLineDesign

Bases: Node

Base for mean-line designers.

n_row: ClassVar[int | None] = None

Number of blade rows this design describes.

forward(fluid: Fluid)

Return a mean line built from this design’s variables.

Use allocate() for the empty mean line, fill it in, and return it.

backward(ml)

Return the design variables represented by mean line ml.

allocate(fluid: Fluid) MeanLine

Return an empty mean line of the right size, ready to fill in.

Parameters:

fluid (ember.fluid.Fluid) – The equation of state, already built from the config. A design never sees the config node, only the fluid object it describes.

design(fluid: Fluid) MeanLine

Return a mean line built from this design.

Checks that the result inverts back to the design variables that asked for it, then freezes it at the earliest opportunity, so that every stage which follows – annulus, blades, mesher, post-processing – reads the mean line and cannot write to it.

solve_for(ml, build, unknowns, targets, *, rtol=0.0001, max_iter=100, name='')

Adjust unknowns until backward() reports targets.

Use this for the parts of a design that cannot be constructed explicitly, where some quantity has to be guessed and then corrected. Everything else should be built directly in forward.

Parameters:
  • ml (MeanLine) – The mean line being designed. build writes into it, and backward() is read from it.

  • build (callable) – build(**unknowns), constructing ml for one trial set of values. Its return value is ignored.

  • unknowns (dict) – Quantities to solve for, mapped to their initial guesses. Values may be scalars or arrays.

  • targets (dict) –

    Maps a key of backward()’s output to the value it must take. A number means that key must equal it. A string names another key of backward()’s output that it must equal, which is how conditions like a repeating stage are written:

    targets={"psi": psi, "Alpha1": "Alpha3"}
    

  • rtol (float) – Largest acceptable scaled residual. A mean line stores its state as float32, and targets are derived quantities, so residuals below about 1e-6 are not reachable however many iterations are spent: do not tighten this towards float64 tolerances.

  • max_iter (int) – Iteration limit, in units of Jacobian evaluations.

  • name (str) – Label for this solve, used in error messages.

Returns:

The solved values, in the same shapes as unknowns.

Return type:

dict

Raises:

DesignError – If the system is underdetermined, or the solve does not converge.