Skip to content

A two-shaft gas turbine with useful shaft power: a more complex acausal system of systems

This worked example takes the same compositional idea as the two-spring example and applies it to a more recognisable engineering system: a small two-shaft gas turbine.

You do not need to know gas-turbine design to follow the example.

At a high level, the engine contains two kinds of component:

  • a compressor squeezes the incoming air and therefore requires mechanical work;
  • a turbine extracts energy from the hot gas and turns it into mechanical work.

The engine uses two shafts to transfer that work.

flowchart TD
    inlet["Inlet"] --> lpc["LP compressor"]
    lpc --> hpc["HP compressor"]
    hpc --> heat["Heat addition"]
    heat --> hpt["HP turbine"]
    hpt --> lpt["LP turbine"]
    lpt --> exhaust["Exhaust"]

    hpt -. "drives" .-> hpc
    lpt -. "drives" .-> lpc
    lpt -. "useful output" .-> output["Output shaft"]

Click a diagram to expand, zoom and pan.

The HP turbine drives the HP compressor.

The LP turbine must do two jobs:

LP turbine work
      =
LP compressor work
      +
useful output work

That useful output is the quantity we ultimately care about.

If the engine drives a generator, propeller, pump, or another mechanical load, this is the work available to that load.

Why there are two kinds of work quantity

The component equations use specific work, measured here in kJ/kg.

That means:

how much mechanical work is transferred for each kilogram of gas flowing through the engine?

To turn that into ordinary shaft power we also need the mass flow rate:

shaft power
    =
specific output work × mass flow

Because:

kJ/kg × kg/s = kJ/s = kW

So a specific output work of 100 kJ/kg at a mass flow of 20 kg/s corresponds to:

2000 kW = 2 MW

A simplifying exhaust assumption

This model only describes component temperatures and work; it does not yet include a nozzle or turbine pressure model.

To determine how much work the LP turbine extracts, we therefore specify the core exit temperature as an operating boundary condition.

For the baseline example we use:

core exit temperature = Quantity(1000, absolute_kelvin) K

A real engine model would usually determine the turbine expansion from pressures and component efficiencies. Here the fixed exit temperature keeps the physics intentionally low-order while giving the shaft-power calculation a well-defined endpoint.

How this example is evaluated

The requests below can be decomposed into ordinary calculations. The parent values make the component connections and shaft balances easy to inspect.

The default causalization analyser also supports linear coupled blocks across subsystem boundaries. This example does not require that capability; a nonlinear coupled block would still need a different formulation.

The gas path still runs physically from compressor to turbine, but those physical arrows do not prescribe the direction in which Bilt must use the equations.

1. Define reusable component systems

We start with a compressor. The system and its equations use scalar quantity declarations, so their physical dimensions are explicit without prescribing array shapes. Compatible unit-carrying tensors can also be supplied.

Temperatures are absolute readings; subtracting two readings gives a relative temperature change. Specific heat multiplies that change to produce specific work. Pressure ratios, efficiencies, and the heat-capacity ratio gamma are dimensionless quantities, supplied using UNITLESS.

Port names describe physical quantities. Units are carried by values rather than encoded in names such as _k or _kj_per_kg.

from typing import Annotated

from bilt import Implementation, Interface, Quantity, equation, field, system
from bilt.units import UNITLESS, absolute_kelvin, kelvin, kilogram, kilojoule, kilowatt, megawatt, second
from bilt.units.dimensions import ABSOLUTE, DIMENSIONLESS, ENERGY, MASS, TEMPERATURE, TIME


AbsoluteTemperature = Annotated[Quantity, TEMPERATURE, ABSOLUTE]
SpecificHeat = Annotated[Quantity, ENERGY / (MASS * TEMPERATURE)]
SpecificWork = Annotated[Quantity, ENERGY / MASS]
MassFlow = Annotated[Quantity, MASS / TIME]
Power = Annotated[Quantity, ENERGY / TIME]
Ratio = Annotated[Quantity, DIMENSIONLESS]


@system
class Compressor(metaclass=Interface):
    inlet_temperature: AbsoluteTemperature = field()
    outlet_temperature: AbsoluteTemperature = field()
    pressure_ratio: Ratio = field()
    isentropic_efficiency: Ratio = field()
    gamma: Ratio = field()
    specific_heat: SpecificHeat = field()
    specific_work: SpecificWork = field()


@equation
def compressor_temperature_relation(
    inlet_temperature: AbsoluteTemperature,
    outlet_temperature: AbsoluteTemperature,
    pressure_ratio: Ratio,
    isentropic_efficiency: Ratio,
    gamma: Ratio,
) -> bool:
    return (outlet_temperature - inlet_temperature) * isentropic_efficiency == (
        inlet_temperature - Quantity(0.0, absolute_kelvin)
    ) * (pressure_ratio ** ((gamma - 1.0) / gamma) - 1.0)


@equation
def compressor_work_relation(
    specific_work: SpecificWork,
    specific_heat: SpecificHeat,
    inlet_temperature: AbsoluteTemperature,
    outlet_temperature: AbsoluteTemperature,
) -> bool:
    return specific_work == specific_heat * (outlet_temperature - inlet_temperature)


compressor_model = Implementation(
    Compressor,
    [
        compressor_temperature_relation,
        compressor_work_relation,
    ],
)

The compressor temperature relation uses inlet_temperature - Quantity(0.0, absolute_kelvin) to express the inlet temperature relative to absolute zero before multiplying it by a dimensionless factor. This preserves the original kelvin-based formula while distinguishing absolute readings from relative changes.

These are equations, not transforms.

For example:

specific work = cp × (outlet temperature - inlet temperature)

does not permanently designate specific work as the output. With a different set of known values, Bilt can use the same relationship in another direction.

The turbine is similarly small:

@system
class Turbine(metaclass=Interface):
    inlet_temperature: AbsoluteTemperature = field()
    outlet_temperature: AbsoluteTemperature = field()
    specific_heat: SpecificHeat = field()
    specific_work: SpecificWork = field()


@equation
def turbine_work_relation(
    specific_work: SpecificWork,
    specific_heat: SpecificHeat,
    inlet_temperature: AbsoluteTemperature,
    outlet_temperature: AbsoluteTemperature,
) -> bool:
    return specific_work == specific_heat * (inlet_temperature - outlet_temperature)


turbine_model = Implementation(
    Turbine,
    [turbine_work_relation],
)

2. Define the two-shaft core

The parent system contains the values needed to connect the four child systems.

@system
class TwoShaftCore(metaclass=Interface):
    # Gas-path temperatures.
    inlet_temperature: AbsoluteTemperature = field()
    low_pressure_compressor_exit_temperature: AbsoluteTemperature = field()
    high_pressure_compressor_exit_temperature: AbsoluteTemperature = field()
    turbine_inlet_temperature: AbsoluteTemperature = field()
    high_pressure_turbine_exit_temperature: AbsoluteTemperature = field()
    exit_temperature: AbsoluteTemperature = field()

    # Compressor design assumptions.
    low_pressure_compressor_pressure_ratio: Ratio = field()
    high_pressure_compressor_pressure_ratio: Ratio = field()
    low_pressure_compressor_efficiency: Ratio = field()
    high_pressure_compressor_efficiency: Ratio = field()

    # Shared fluid properties.
    gamma: Ratio = field()
    specific_heat: SpecificHeat = field()

    # Shaft efficiencies.
    low_pressure_shaft_efficiency: Ratio = field()
    high_pressure_shaft_efficiency: Ratio = field()

    # Coupling values used by the two shaft balances.
    low_pressure_compressor_work: SpecificWork = field()
    high_pressure_compressor_work: SpecificWork = field()
    high_pressure_turbine_work: SpecificWork = field()
    low_pressure_turbine_work: SpecificWork = field()

    # Useful LP-shaft output.
    shaft_output_work: SpecificWork = field()
    mass_flow: MassFlow = field()
    shaft_power: Power = field()

Some of these values exist mainly to provide an explicit hand-off between child systems.

For example:

LP compressor.outlet
        ↕
low_pressure_compressor_exit_temperature
        ↕
HP compressor.inlet

The two equations on either side each touch only one child.

3. Connect the LP compressor

The parent inlet is connected to the LP compressor inlet:

@equation
def low_pressure_compressor_inlet_connection(
    inlet_temperature: AbsoluteTemperature,
    low_pressure_compressor: Compressor,
) -> bool:
    return inlet_temperature == low_pressure_compressor.inlet_temperature

The compressor exit is exposed on the parent:

@equation
def low_pressure_compressor_exit_connection(
    low_pressure_compressor_exit_temperature: AbsoluteTemperature,
    low_pressure_compressor: Compressor,
) -> bool:
    return low_pressure_compressor_exit_temperature == low_pressure_compressor.outlet_temperature

and the parent supplies its pressure ratio, efficiency, and fluid properties:

@equation
def low_pressure_compressor_pressure_ratio_connection(
    low_pressure_compressor_pressure_ratio: Ratio,
    low_pressure_compressor: Compressor,
) -> bool:
    return low_pressure_compressor_pressure_ratio == low_pressure_compressor.pressure_ratio


@equation
def low_pressure_compressor_efficiency_connection(
    low_pressure_compressor_efficiency: Ratio,
    low_pressure_compressor: Compressor,
) -> bool:
    return low_pressure_compressor_efficiency == low_pressure_compressor.isentropic_efficiency


@equation
def low_pressure_compressor_gamma_connection(
    gamma: Ratio,
    low_pressure_compressor: Compressor,
) -> bool:
    return gamma == low_pressure_compressor.gamma


@equation
def low_pressure_compressor_specific_heat_connection(
    specific_heat: SpecificHeat,
    low_pressure_compressor: Compressor,
) -> bool:
    return specific_heat == low_pressure_compressor.specific_heat

Finally, its specific work is copied onto the parent:

@equation
def low_pressure_compressor_work_connection(
    low_pressure_compressor_work: SpecificWork,
    low_pressure_compressor: Compressor,
) -> bool:
    return low_pressure_compressor_work == low_pressure_compressor.specific_work

4. Connect the HP compressor

The LP compressor exit temperature now acts as the HP compressor inlet temperature:

@equation
def high_pressure_compressor_inlet_connection(
    low_pressure_compressor_exit_temperature: AbsoluteTemperature,
    high_pressure_compressor: Compressor,
) -> bool:
    return low_pressure_compressor_exit_temperature == high_pressure_compressor.inlet_temperature

The HP compressor is otherwise connected in the same way:

@equation
def high_pressure_compressor_exit_connection(
    high_pressure_compressor_exit_temperature: AbsoluteTemperature,
    high_pressure_compressor: Compressor,
) -> bool:
    return high_pressure_compressor_exit_temperature == high_pressure_compressor.outlet_temperature


@equation
def high_pressure_compressor_pressure_ratio_connection(
    high_pressure_compressor_pressure_ratio: Ratio,
    high_pressure_compressor: Compressor,
) -> bool:
    return high_pressure_compressor_pressure_ratio == high_pressure_compressor.pressure_ratio


@equation
def high_pressure_compressor_efficiency_connection(
    high_pressure_compressor_efficiency: Ratio,
    high_pressure_compressor: Compressor,
) -> bool:
    return high_pressure_compressor_efficiency == high_pressure_compressor.isentropic_efficiency


@equation
def high_pressure_compressor_gamma_connection(
    gamma: Ratio,
    high_pressure_compressor: Compressor,
) -> bool:
    return gamma == high_pressure_compressor.gamma


@equation
def high_pressure_compressor_specific_heat_connection(
    specific_heat: SpecificHeat,
    high_pressure_compressor: Compressor,
) -> bool:
    return specific_heat == high_pressure_compressor.specific_heat


@equation
def high_pressure_compressor_work_connection(
    high_pressure_compressor_work: SpecificWork,
    high_pressure_compressor: Compressor,
) -> bool:
    return high_pressure_compressor_work == high_pressure_compressor.specific_work

Again, no equation directly mentions both compressors.

The shared parent temperature is the hand-off point.

5. Connect the turbines

The turbine inlet temperature is connected to the HP turbine:

@equation
def high_pressure_turbine_inlet_connection(
    turbine_inlet_temperature: AbsoluteTemperature,
    high_pressure_turbine: Turbine,
) -> bool:
    return turbine_inlet_temperature == high_pressure_turbine.inlet_temperature

The HP turbine exit is exposed on the parent:

@equation
def high_pressure_turbine_exit_connection(
    high_pressure_turbine_exit_temperature: AbsoluteTemperature,
    high_pressure_turbine: Turbine,
) -> bool:
    return high_pressure_turbine_exit_temperature == high_pressure_turbine.outlet_temperature

and the same parent value independently feeds the LP turbine:

@equation
def low_pressure_turbine_inlet_connection(
    high_pressure_turbine_exit_temperature: AbsoluteTemperature,
    low_pressure_turbine: Turbine,
) -> bool:
    return high_pressure_turbine_exit_temperature == low_pressure_turbine.inlet_temperature

The remaining turbine connections are:

@equation
def core_exit_connection(
    exit_temperature: AbsoluteTemperature,
    low_pressure_turbine: Turbine,
) -> bool:
    return exit_temperature == low_pressure_turbine.outlet_temperature


@equation
def high_pressure_turbine_specific_heat_connection(
    specific_heat: SpecificHeat,
    high_pressure_turbine: Turbine,
) -> bool:
    return specific_heat == high_pressure_turbine.specific_heat


@equation
def low_pressure_turbine_specific_heat_connection(
    specific_heat: SpecificHeat,
    low_pressure_turbine: Turbine,
) -> bool:
    return specific_heat == low_pressure_turbine.specific_heat


@equation
def high_pressure_turbine_work_connection(
    high_pressure_turbine_work: SpecificWork,
    high_pressure_turbine: Turbine,
) -> bool:
    return high_pressure_turbine_work == high_pressure_turbine.specific_work


@equation
def low_pressure_turbine_work_connection(
    low_pressure_turbine_work: SpecificWork,
    low_pressure_turbine: Turbine,
) -> bool:
    return low_pressure_turbine_work == low_pressure_turbine.specific_work

6. Balance the shafts and expose useful output work

Once component work has been exposed as ordinary parent values, the shaft balances themselves contain no child-system parameters.

The HP shaft

The HP turbine exists to drive the HP compressor.

After allowing for shaft efficiency:

HP compressor work
    =
HP shaft efficiency × HP turbine work

In Bilt:

@equation
def high_pressure_shaft_balance(
    high_pressure_compressor_work: SpecificWork,
    high_pressure_turbine_work: SpecificWork,
    high_pressure_shaft_efficiency: Ratio,
) -> bool:
    return high_pressure_compressor_work == high_pressure_shaft_efficiency * high_pressure_turbine_work

The LP shaft

The LP turbine has an additional job: after driving the LP compressor, any remaining work can leave the engine through the output shaft.

flowchart LR
    turbine["LP turbine work"] --> efficiency["LP shaft efficiency"]
    efficiency --> available["Work available on LP shaft"]
    available --> compressor["LP compressor requirement"]
    available --> output["Useful output work"]

The balance is:

usable LP turbine work
    =
LP compressor work
    +
useful output work

or:

@equation
def low_pressure_shaft_balance(
    low_pressure_compressor_work: SpecificWork,
    low_pressure_turbine_work: SpecificWork,
    low_pressure_shaft_efficiency: Ratio,
    shaft_output_work: SpecificWork,
) -> bool:
    return low_pressure_shaft_efficiency * low_pressure_turbine_work == low_pressure_compressor_work + shaft_output_work

Finally, mass flow converts specific output work into shaft power:

@equation
def shaft_power_relation(
    shaft_output_work: SpecificWork,
    mass_flow: MassFlow,
    shaft_power: Power,
) -> bool:
    return shaft_power == shaft_output_work * mass_flow

These are all acausal equations.

For example, the last equation can be used either as:

specific work + mass flow → shaft power

or:

required shaft power + mass flow → required specific work

That second direction will be useful in the inverse-design example.

7. Put the implementation together

The four component occurrences are internal implementations:

core_model = Implementation(
    TwoShaftCore,
    [
        low_pressure_compressor_inlet_connection,
        low_pressure_compressor_exit_connection,
        low_pressure_compressor_pressure_ratio_connection,
        low_pressure_compressor_efficiency_connection,
        low_pressure_compressor_gamma_connection,
        low_pressure_compressor_specific_heat_connection,
        low_pressure_compressor_work_connection,
        high_pressure_compressor_inlet_connection,
        high_pressure_compressor_exit_connection,
        high_pressure_compressor_pressure_ratio_connection,
        high_pressure_compressor_efficiency_connection,
        high_pressure_compressor_gamma_connection,
        high_pressure_compressor_specific_heat_connection,
        high_pressure_compressor_work_connection,
        high_pressure_turbine_inlet_connection,
        high_pressure_turbine_exit_connection,
        high_pressure_turbine_specific_heat_connection,
        high_pressure_turbine_work_connection,
        low_pressure_turbine_inlet_connection,
        core_exit_connection,
        low_pressure_turbine_specific_heat_connection,
        low_pressure_turbine_work_connection,
        high_pressure_shaft_balance,
        low_pressure_shaft_balance,
        shaft_power_relation,
    ],
    internal_implementations={
        "low_pressure_compressor": compressor_model,
        "high_pressure_compressor": compressor_model,
        "high_pressure_turbine": turbine_model,
        "low_pressure_turbine": turbine_model,
    },
)

The hierarchy organises the model. It does not prescribe an execution order.

A child system can also have more than one valid internal route to the same port. Which route Bilt uses depends on the values available in the surrounding model, so nesting a subsystem does not turn it into a fixed one-way calculation.

For these requests, the required boundary values can be established before evaluating each child. This is a property of the example, rather than a general restriction on subsystem equations.

For the executable documentation below, the same model is loaded from a temporary Python module. This gives Bilt a real source file for inspect.getsource() during the markdown-exec build.

8. Calculate useful shaft power

For the baseline operating point we specify:

inlet temperature             288 K
turbine inlet temperature    1450 K
core exit temperature        1000 K
mass flow                      20 kg/s

The compressor pressure ratios and efficiencies are also supplied as design assumptions.

Inputs use convenient engineering units. Bilt computes in coherent SI units, so calculated specific work is returned in J/kg and shaft power in W. We explicitly convert to kJ/kg and MW for the printed results and plots.

baseline = TwoShaftCore(
    inlet_temperature=Quantity(288.0, absolute_kelvin),
    low_pressure_compressor_pressure_ratio=Quantity(2.0, UNITLESS),
    high_pressure_compressor_pressure_ratio=Quantity(6.0, UNITLESS),
    low_pressure_compressor_efficiency=Quantity(0.88, UNITLESS),
    high_pressure_compressor_efficiency=Quantity(0.86, UNITLESS),
    turbine_inlet_temperature=Quantity(1450.0, absolute_kelvin),
    exit_temperature=Quantity(1000.0, absolute_kelvin),
    low_pressure_shaft_efficiency=Quantity(0.99, UNITLESS),
    high_pressure_shaft_efficiency=Quantity(0.99, UNITLESS),
    gamma=Quantity(1.4, UNITLESS),
    specific_heat=1.005 * kilojoule / (kilogram * kelvin),
    mass_flow=20.0 * kilogram / second,
)

with core_model:
    low_pressure_compressor_exit = baseline.low_pressure_compressor_exit_temperature
    high_pressure_compressor_exit = baseline.high_pressure_compressor_exit_temperature
    high_pressure_turbine_exit = baseline.high_pressure_turbine_exit_temperature

    low_pressure_compressor_work = baseline.low_pressure_compressor_work
    low_pressure_turbine_work = baseline.low_pressure_turbine_work
    shaft_output_work = baseline.shaft_output_work
    shaft_power = baseline.shaft_power

print(f"LP compressor exit: {low_pressure_compressor_exit.convert_to(absolute_kelvin).value:.1f} K")
print(f"HP compressor exit: {high_pressure_compressor_exit.convert_to(absolute_kelvin).value:.1f} K")
print(f"HP turbine exit:    {high_pressure_turbine_exit.convert_to(absolute_kelvin).value:.1f} K")
print(f"Core exit:          {baseline.exit_temperature.convert_to(absolute_kelvin).value:.1f} K")
print()
print(f"Useful shaft work:  {shaft_output_work.convert_to(kilojoule / kilogram).value:.1f} kJ/kg")
print(f"Useful shaft power: {shaft_power.convert_to(megawatt).value:.2f} MW")

LP compressor exit: 359.7 K HP compressor exit: 639.3 K HP turbine exit: 1167.6 K Core exit: 1000.0 K

Useful shaft work: 94.7 kJ/kg Useful shaft power: 1.89 MW

For this deliberately simple model, the useful output is approximately:

94.7 kJ/kg

1.89 MW at 20 kg/s

Follow the energy through the engine

The station temperatures show where the gas gains and loses energy:

station_names = [
    "Inlet",
    "LP compressor",
    "HP compressor",
    "Turbine inlet",
    "HP turbine",
    "Core exit",
]

station_temperature_readings = [
    baseline.inlet_temperature,
    low_pressure_compressor_exit,
    high_pressure_compressor_exit,
    baseline.turbine_inlet_temperature,
    high_pressure_turbine_exit,
    baseline.exit_temperature,
]

station_temperatures = [reading.convert_to(absolute_kelvin).value for reading in station_temperature_readings]

plt.plot(
    np.arange(len(station_names)),
    station_temperatures,
    marker="o",
)
plt.xticks(
    np.arange(len(station_names)),
    station_names,
    rotation=25,
    ha="right",
)
plt.ylabel("Temperature (K)")
plt.title("Temperature through the two-shaft core")
render_plot()
2026-11-03T14:37:49.031479 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

The compressors raise the air temperature because mechanical work is being put into the gas.

The turbines lower the temperature because energy is being taken out of the gas and converted into shaft work.

Where does the LP turbine work go?

The LP turbine's usable shaft work is divided between the LP compressor and the external load:

usable_low_pressure_turbine_work = baseline.low_pressure_shaft_efficiency * low_pressure_turbine_work

plt.bar(
    ["LP shaft"],
    [low_pressure_compressor_work.convert_to(kilojoule / kilogram).value],
    label="LP compressor",
)
plt.bar(
    ["LP shaft"],
    [shaft_output_work.convert_to(kilojoule / kilogram).value],
    bottom=[low_pressure_compressor_work.convert_to(kilojoule / kilogram).value],
    label="Useful output",
)
plt.axhline(
    usable_low_pressure_turbine_work.convert_to(kilojoule / kilogram).value,
    linestyle="--",
    label="Usable turbine work",
)
plt.ylabel("Specific work (kJ/kg)")
plt.title("How LP turbine work is used")
plt.legend()
render_plot()
2026-11-03T14:37:49.162964 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

The stacked bar represents:

LP compressor requirement
        +
useful shaft output
        =
usable LP turbine work

This is more informative than simply reporting the final exhaust temperature: it gives the model a useful engineering output.

9. Ask a more interesting inverse-design question

The forward calculation answered:

given component design + operating point
                ↓
        useful shaft power

We can now ask a less obvious question in the other direction.

Suppose:

required shaft power       2.0 MW
turbine inlet temperature 1450 K
core exit temperature      1000 K
HP compressor pressure ratio   6
mass flow                    20 kg/s

and ask:

What HP compressor efficiency is required to make that operating point possible?

This is a useful preliminary-design question.

The HP compressor pressure ratio tells us how much the compressor must raise the air pressure. Its efficiency determines how much temperature rise — and therefore shaft work — is required to achieve that pressure ratio.

A more efficient compressor needs less shaft work for the same pressure ratio.

Why this is a better acausal example

The compressor equation is:

(T_out - T_in) × efficiency
    =
T_in × (pressure_ratio^((gamma - 1) / gamma) - 1)

In the forward calculation, Bilt knows:

pressure ratio
efficiency
inlet temperature

and uses the equation to find:

outlet temperature

In the inverse calculation, the surrounding engine model determines how much HP compressor work is available. That determines the required compressor outlet temperature, and Bilt then uses the same compressor equation to solve for:

efficiency

We are not writing a second, manually rearranged compressor model.

Importantly, the pressure-ratio exponent remains evaluated in its normal direction. The current Bilt isolator does not need to invert the power operation for this query.

10. Solve for required HP compressor efficiency

The inverse operating point is:

inverse_case = TwoShaftCore(
    inlet_temperature=Quantity(288.0, absolute_kelvin),
    low_pressure_compressor_pressure_ratio=Quantity(2.0, UNITLESS),
    high_pressure_compressor_pressure_ratio=Quantity(6.0, UNITLESS),
    low_pressure_compressor_efficiency=Quantity(0.88, UNITLESS),
    # high_pressure_compressor_efficiency is deliberately omitted
    turbine_inlet_temperature=Quantity(1450.0, absolute_kelvin),
    exit_temperature=Quantity(1000.0, absolute_kelvin),
    low_pressure_shaft_efficiency=Quantity(0.99, UNITLESS),
    high_pressure_shaft_efficiency=Quantity(0.99, UNITLESS),
    gamma=Quantity(1.4, UNITLESS),
    specific_heat=1.005 * kilojoule / (kilogram * kelvin),
    mass_flow=20.0 * kilogram / second,
    shaft_power=2000.0 * kilowatt,
)

with core_model:
    required_high_pressure_compressor_efficiency = inverse_case.high_pressure_compressor_efficiency

print(
    f"Required HP compressor efficiency: {required_high_pressure_compressor_efficiency.convert_to(UNITLESS).value:.3f}"
)

Required HP compressor efficiency: 0.877

The result is approximately:

0.877

or about:

87.7%

The baseline case used an HP compressor efficiency of 86% and produced about 1.89 MW.

To reach 2 MW without changing turbine inlet temperature, mass flow, pressure ratio, or exhaust condition, the HP compressor must therefore be slightly more efficient.

Follow the reverse computation

The requested shaft power first determines how much specific output work is needed:

shaft power
    ↓
specific output work

The LP shaft balance then determines how much work the LP turbine must provide:

specific output work
        +
LP compressor work
        ↓
required LP turbine work

Because the core exit temperature is known, the LP turbine equation determines the temperature entering the LP turbine — which is also the HP turbine exit temperature.

The HP turbine equation then determines how much work the HP turbine provides.

That fixes how much work is available to the HP compressor.

Finally, the HP compressor equations determine the efficiency required to achieve the fixed pressure ratio with that amount of work.

flowchart TB
    power["Required shaft power (2 MW)"] --> output["Required output work"]
    flow["Mass flow (20 kg/s)"] --> output

    output --> lpbal["LP shaft balance"]
    lpc["LP compressor work"] --> lpbal

    lpbal --> lptwork["Required LP turbine work"]
    lptwork --> lpt["LP turbine"]
    exhaust["Core exit (1000 K)"] --> lpt

    lpt --> hptexit["HP turbine exit temperature"]
    hptexit --> hpt["HP turbine"]
    tit["Turbine inlet (1450 K)"] --> hpt

    hpt --> hpwork["HP turbine work"]
    hpwork --> hpbal["HP shaft balance"]
    hpbal --> hpcwork["Available HP compressor work"]

    hpcwork --> hpcout["Required HP compressor outlet temperature"]
    hpcout --> compressor["HP compressor equation"]
    pr["Pressure ratio (6)"] --> compressor
    compressor --> efficiency["Required HP compressor efficiency"]

The route crosses most of the engine model, but every step is still decomposable: no equation directly couples two sibling subsystems.

11. Explore the nonlinear design trade-off

Now repeat the inverse query for several shaft-power requirements while keeping the same pressure ratio and turbine inlet temperature.

required_powers_mw = np.linspace(0.5, 2.8, 24)
required_high_pressure_compressor_efficiencies = []

for required_power_mw in required_powers_mw:
    core = TwoShaftCore(
        inlet_temperature=Quantity(288.0, absolute_kelvin),
        low_pressure_compressor_pressure_ratio=Quantity(2.0, UNITLESS),
        high_pressure_compressor_pressure_ratio=Quantity(6.0, UNITLESS),
        low_pressure_compressor_efficiency=Quantity(0.88, UNITLESS),
        turbine_inlet_temperature=Quantity(1450.0, absolute_kelvin),
        exit_temperature=Quantity(1000.0, absolute_kelvin),
        low_pressure_shaft_efficiency=Quantity(0.99, UNITLESS),
        high_pressure_shaft_efficiency=Quantity(0.99, UNITLESS),
        gamma=Quantity(1.4, UNITLESS),
        specific_heat=1.005 * kilojoule / (kilogram * kelvin),
        mass_flow=20.0 * kilogram / second,
        shaft_power=Quantity(float(required_power_mw), megawatt),
    )

    with core_model:
        required_high_pressure_compressor_efficiencies.append(
            core.high_pressure_compressor_efficiency.convert_to(UNITLESS).value
        )
plt.plot(
    required_powers_mw,
    required_high_pressure_compressor_efficiencies,
    marker="o",
)
plt.axhline(
    1.0,
    linestyle="--",
    label="100% efficiency",
)
plt.xlabel("Required shaft power (MW)")
plt.ylabel("Required HP compressor efficiency")
plt.title("Inverse design: compressor efficiency required for shaft power")
plt.legend()
render_plot()
2026-11-03T14:37:49.330243 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

Unlike the previous turbine-temperature example, this relationship is not a straight line.

As shaft-power demand increases, less turbine work is available to drive the HP compressor. The compressor must therefore achieve the same pressure ratio using progressively less work, which requires progressively higher efficiency.

The curve also exposes a useful physical limit.

Once the required efficiency exceeds:

1.0

the requested operating point is impossible in this model: a compressor cannot be more than 100% efficient.

For these assumptions, that limit occurs at roughly:

2.68 MW

So the plot is doing more than showing that Bilt can run equations backwards. It turns the same model into a simple feasibility study.

12. Explore the model yourself

The easiest experiments are changes to the operating assumptions.

For example, change mass flow:

mass_flow = 25.0 * kilogram / second

or change the maximum turbine inlet temperature:

turbine_inlet_temperature = Quantity(1500.0, absolute_kelvin)

You can also change the compressor pressure-ratio target:

high_pressure_compressor_pressure_ratio = Quantity(7.0, UNITLESS)

and ask again what compressor efficiency is required for a specified shaft power.

Another useful experiment is to change the core exit boundary:

exit_temperature = Quantity(950.0, absolute_kelvin)

To explore locally, edit these values while zensical serve is running and save the Markdown file. Zensical will rebuild the page and rerun the executable blocks.

The two main forms of the example are now:

known HP compressor efficiency
        ↓
ask for useful shaft power

and:

known required shaft power
        ↓
ask for required HP compressor efficiency

The component equations remain unchanged.

13. What this example demonstrates

Reusable component physics

The compressor and turbine models describe local physical relationships:

Compressor
    temperature rise
    pressure ratio
    efficiency
    specific work

Turbine
    temperature drop
    specific work

The same compressor implementation is reused for the LP and HP compressors, and the same turbine implementation is reused for both turbines.

A physical system made from smaller systems

The enclosing implementation describes the engine architecture:

gas path:
inlet → LPC → HPC → HPT → LPT → exhaust

HP shaft:
HPT → HPC

LP shaft:
LPT → LPC + useful output

The child systems do not need to know what they are connected to.

A useful engineering output

The forward model ends in:

shaft power

rather than only an intermediate station temperature.

That makes the systems-of-systems calculation easier to interpret:

the component relationships collectively determine how much useful mechanical power the engine can deliver.

A more interesting inverse-design problem

The reverse query is now:

What HP compressor efficiency is required to deliver this shaft power while holding pressure ratio and turbine inlet temperature fixed?

This is more informative than simply asking for turbine inlet temperature because the resulting design relationship is nonlinear and exposes a physical feasibility boundary.

Acausal evaluation

The forward and inverse calculations use the same compressor equation.

In one direction:

pressure ratio + efficiency
        ↓
compressor temperature rise

In the other:

available compressor work + pressure ratio
        ↓
required compressor efficiency

No manually inverted model is required.

Acausal modelling means describing the relationships once, then asking for the quantity you need.

The forward and inverse requests here are decomposable. Supported linear feedback between subsystems can also be handled by the default causalization analyser.