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:
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:
Because:
So a specific output work of 100 kJ/kg at a mass flow of 20 kg/s corresponds to:
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:
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:
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:
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:
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:
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:
or:
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:
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()
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()
The stacked bar represents:
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:
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:
In the forward calculation, Bilt knows:
and uses the equation to find:
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:
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:
or about:
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:
The LP shaft balance then determines how much work the LP turbine must provide:
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()
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:
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:
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:
or change the maximum turbine inlet temperature:
You can also change the compressor pressure-ratio target:
and ask again what compressor efficiency is required for a specified shaft power.
Another useful experiment is to change the core exit boundary:
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:
and:
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:
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:
In the other:
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.