Note
Go to the end to download the full example code.
Time-dependent controls and the first-order delay#
The first-order delay#
Every time-dependent optimization variable (a control) enters the simulation
through a first-order delay, which turns a possibly abrupt decision into a gradual
response and improves the stability and dimensionality of the optimization. The
delayed output o(t) follows the input signal i(t) with a time constant
tau:
The helper below integrates that equation (mirroring
noads.core.scenarios.temporalscenario.delay1_rhs), with the input signal given
as (times, values) knots interpolated linearly, and an initial output of zero.
def first_order_delay(times, values, tau, t_eval):
"""Response of the first-order delay to a piecewise-linear input signal."""
times, values = array(times, dtype=float), array(values, dtype=float)
def rhs(t, o):
return (interp(t, times, values) - o) / tau
solution = solve_ivp(
rhs, (t_eval[0], t_eval[-1]), [0.0], t_eval=t_eval, max_step=0.25
)
return solution.y[0]
Response to a step, a ramped step, and a pulse, for two time constants. For a ramped
step of ramp duration 2*tau the output reaches 95 % of its target after about
4*tau, which the model interprets as one fleet-replacement lifetime.
t = linspace(0, 60, 601)
inputs = {
"step": ([0, 10, 10.001, 60], [0, 0, 1, 1]),
"ramped step (2τ)": ([0, 10, 30, 60], [0, 0, 1, 1]),
"pulse": ([0, 10, 30, 50, 60], [0, 0, 1, 0, 0]),
}
fig1, axes1 = subplots(1, 2, figsize=(9, 4), layout="constrained", sharey=True)
for ax, tau in zip(axes1, (5.0, 10.0)):
for label, (times, values) in inputs.items():
ax.plot(t, interp(t, times, values), ":", linewidth=1)
ax.plot(t, first_order_delay(times, values, tau, t), label=label, linewidth=2)
ax.axhline(0.95, color="gray", linewidth=0.8, linestyle="--")
ax.set_title(f"τ = {tau:.0f} yr")
ax.set_xlabel("Time [yr]")
axes1[0].set_ylabel("Delay output")
axes1[0].legend(loc="lower right", fontsize="small")
fig1.suptitle("First-order delay response (dotted: input, solid: delayed output)")
show()
years = linspace(2025, 2075, 501)
def ramped_pulse(eis, max_share, ramp_up=5.0, lifetime=25.0, ramp_down=5.0):
"""Trapezoidal market-share input, as built in ``AircraftDesign._control_share``."""
times = [eis, eis + ramp_up, eis + lifetime, eis + lifetime + ramp_down]
return times, [0.0, max_share, max_share, 0.0]

Demand avoidance — a valid supply-shift ratio#
In the low-demand formulation each market has a supply-shift ratio control – the
fraction of trend supply that is avoided – with knot values (every 2.5 years, as
optimization variables) bounded to [0, 0.9] and delay tau = 10 yr. The
delayed control is the realised avoidance; 1 - SR is the fraction of trend
traffic still served. A single market is shown: the supply-shift ratio ramps
linearly from 0 to 0.5 between 2030 and 2050, then drops to 0.2 and stays there.
sr_knots = linspace(2025, 2075, 21)
sr_market = array([
0.0,
0.0,
0.0,
0.0625,
0.125,
0.1875,
0.25,
0.3125,
0.375,
0.4375,
0.5,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
0.2,
]) # noqa: E501
assert (sr_market >= 0.0).all(), "supply-shift ratio below 0"
assert (sr_market <= 0.9).all(), "supply-shift ratio above 0.9"
fig4, ax4 = subplots(figsize=(7.5, 4.5), layout="constrained")
ax4.plot(sr_knots, sr_market, "o", color="C4", markersize=3)
ax4.plot(years, interp(years, sr_knots, sr_market), ":", color="C4", linewidth=1)
ax4.plot(
years,
first_order_delay(sr_knots, sr_market, 10.0, years),
color="C4",
linewidth=2,
label="Single market: avoided share (delayed)",
)
ax4.axhline(0.9, color="red", linewidth=0.8, linestyle="--", label="upper bound 0.9")
ax4.set_xlabel("Year")
ax4.set_ylabel("Supply-shift ratio (avoided share)")
ax4.set_ylim(0, 1)
ax4.set_xlim(2025, 2075)
ax4.set_title(
"Demand avoidance, supply-shift ratio (single market)\n"
"(markers + dotted: control-point inputs; solid: delayed output)"
)
ax4.legend(loc="upper left", fontsize="small")
show()

Total running time of the script: (0 minutes 0.805 seconds)

