A decision-support family for the smallest public subsidy a PPP needs — single-objective sizing, multi-objective Pareto fronts, robust optimisation under stress, and continuous risk-allocation search
Author: David Duarte Arancibia, Austral Infrastructure Fiscal Intelligence Date: April 27, 2026 Audience: Ministry of Finance officials, sector regulators, PPP units, MDB advisors, finance practitioners Companion software: Austral platform —
https://platform.austral-intelligence.com
Public–Private Partnerships (PPPs) typically require some form of public support — most commonly a Viability Gap Fund (VGF), an upfront grant sized as a percentage of capital expenditure — to be simultaneously bankable for the private sector, attractive on a Value-for-Money (VfM) basis, and affordable for the government over the contract term. Practitioners size that subsidy by hand, walking sequentially through risk allocation, cost-of-equity calculation, VfM testing, financial-model bankability, and fiscal-affordability testing — and iterating only when one of those gates obviously fails.
This paper formalises the sizing problem as a constrained optimisation over the project's risk allocation and financial structure, with the VGF percentage as the principal sizing knob and bankability + VfM + affordability as the binding constraints. We describe a family of seven complementary search modes — three single-objective variants (min_vgf, min_fiscal_npv, max_irr_within_envelope), a multi-objective Pareto front via NSGA-II (the Non-dominated Sorting Genetic Algorithm II), robust optimisation under a stress catalogue, continuous risk-allocation search in 13 dimensions on the NSGA-II Pareto front (with a 4-dimensional financial-structure search via differential evolution, DE, as a separate mode), and a Bayesian-optimisation alternative — all sharing the same financial-model engine, the same per-jurisdiction FCCL aggregation, and the same tamper-evident audit chain.
The contribution is fourfold: (i) a precise statement of the sizing problem with its design variables and gates; (ii) a reproducible reference implementation that any practitioner can run in ten minutes against a worked Ghanaian PPP example (the Accra–Tema Motorway Extension), with signed PDF reports for review by Supreme Audit Institutions; (iii) a transparent treatment of the iteration logic — including how the optimiser handles infeasible regions, multi-jurisdiction fiscal splits, multi-currency exposure with per-year FX paths, and stress catalogues; and (iv) a decision-support family that lets the practitioner ask the optimiser the question that matches the actual decision: "smallest grant", "smallest lifetime cost", "most attractive bid within an envelope", "smallest grant under stress", or "what does the trade-off curve look like".
Keywords: Public–Private Partnership, Viability Gap Funding, fiscal commitments and contingent liabilities (FCCL), value for money, bankability, optimisation, multi-objective Pareto front, robust optimisation, audit-evidence.
Feedback & discussion. This paper is published openly so that practitioners — Ministry of Finance staff, sector regulators, MDB advisors, lenders, academics — can challenge it, refine it, and put it to work on real projects. We welcome corrections, counter-examples, and proposed extensions on any section.
- Comment in-page at
https://austral-intelligence.com/research(open comments on every section)- Email the author: [email protected]
- Live demo or walkthrough on request
Designing a Public–Private Partnership (PPP) is an iterative problem disguised as a sequential one. Practitioners typically run five analyses — risk allocation, cost of equity, value-for-money, financial model bankability, and fiscal affordability — and treat them as hand-offs. In practice, each downstream stage feeds back into the upstream one: an unattractive Value-for-Money (VfM) result demands a different risk allocation; an unbankable financial model demands more public support; an unaffordable fiscal exposure demands a smaller project.
The single number that connects all of these together is the Viability Gap Fund (VGF) — the upfront grant the government commits, expressed as a percentage of capex. Almost every other lever (risk allocation, concessional terms) ultimately influences how big the VGF must be. The right framing of PPP feasibility is therefore: what is the smallest VGF that simultaneously satisfies bankability, value-for-money, and fiscal-affordability gates, given the project's design and a defensible risk allocation?
For the worked example in this paper — the Accra–Tema Motorway, a $620M user-pays toll concession — the answer the optimiser returns is VGF = 0%: the project is bankable with no subsidy, placing effectively zero direct fiscal commitment on the government (§10).
This paper proposes a structured, iterative framework for that question and shows how the Austral platform implements it end-to-end:
Risk allocation (GIH baseline)
│
▼
Cost of equity (CAPM)
│
▼ ◄─── feedback loop A:
Value for Money adjust the risk allocation
│ if VfM is not likely positive
▼ ◄─── feedback loop B:
Financial Model adjust VGF % until the project
(bankability) is bankable
│
▼
Fiscal effect
(FCCL aggregation)
The framework is implemented in two equivalent ways:
POST /workflow/optimize) runs the entire loop and returns a
convergence verdict, the final risk allocation, the required VGF %,
and the resulting fiscal exposure.A Workflow Optimizer dashboard (Figure 1) lets a practitioner configure the project, the concessional financing, and the gating thresholds, then watch the optimiser converge in seconds.
Figure 1 — The Workflow Optimizer dashboard.
This paper provides:
The platform is at https://platform.austral-intelligence.com. Anonymous "guest mode" access is sufficient to follow the walkthrough end-to-end without creating an account.
A PPP feasibility assessment is a search through a constrained design space. Once the project parameters (capex, opex, contract length, tariffs) are fixed by sector economics, the only remaining levers are the financial structure and the risk allocation. The objective is to find the smallest VGF that lets the project clear three gates simultaneously:
Sizing problem. Given a project specification (capex, opex, revenue profile, contract length) and a baseline risk allocation \(a_0\), find the design vector \((a^{*},\, v^{*},\, c^{*})\) — risk allocation, VGF as a percentage of capex, and concessional-loan structure — that solves
\[\begin{aligned} \min_{a,\,v,\,c}\quad & v \\[2pt] \text{subject to}\quad & \mathrm{ADSCR}_{\min}\!\bigl(f(a, v, c)\bigr) \;\geq\; \tau_{\mathrm{ADSCR}} && \text{(bankability)} \\ & \mathrm{IRR}_{\mathrm{eq}}\!\bigl(f(a, v, c)\bigr) \;\geq\; K_e(a) && \text{(bankability)} \\ & \mathrm{VfM}(a) \;\geq\; \tau_{\mathrm{VfM}},\quad \mathbb{P}\!\bigl(\mathrm{PSC} > \mathrm{PPP}\bigr) \;\geq\; p_{\min} && \text{(value for money)} \\ & E_j\!\bigl(f(a, v, c)\bigr) \;\leq\; C_j \quad \forall\, j \in \mathcal{J} && \text{(per-jurisdiction affordability)} \\ & 0 \;\leq\; v \;\leq\; v_{\max} && \text{(policy cap on the VGF)} \end{aligned}\]
where \(f(\cdot)\) is the financial-model engine, \(K_e(a)\) is the CAPM cost of equity (which depends on the risk premium implied by the allocation), and \(\mathcal{J}\) is the set of fiscal jurisdictions sharing the contingent liability.
Three gate families dominate every real PPP:
The design variables are:
VGF gets the spotlight because, of all the sizing knobs, it is the cleanest one for a public authority to commit to in cabinet: a single number, an upfront cash outlay, easy to compare against the fiscal envelope and against alternative public investments. The optimiser therefore treats VGF as the secondary objective (minimise) once the gates are met.
A fiscal economist's first objection is the right one to answer head-on:
the smallest grant is not the smallest fiscal cost. An upfront VGF can be
cheaper or dearer, in present value, than a stream of availability payments or
a contingent guarantee. Minimising the VGF is a presentational default — the
number cabinet actually votes on — not a claim of fiscal optimality. The
practitioner who wants the smallest lifetime commitment runs the
min_fiscal_npv mode (§8.6.1), which minimises the discounted sum of every
commitment — grant, availability payments, guarantee payouts, terminal value —
at a recorded sovereign discount rate. The framework ships both objectives
precisely because they answer different decisions; it does not assert that one
dominates the other.
The five stages are not independent. Each stage's output silently re-shapes the inputs of every other stage:
| Stage | Output that feeds back | Where it lands |
|---|---|---|
| Risk allocation | implied risk premium | Cost of equity (Ke) |
| Cost of equity | Ke (and WACC) | VfM discount rate, FM equity IRR target |
| VfM | PSC vs PPP net-present-value (NPV) difference | Whether allocation needs more transfer |
| FM bankability | min ADSCR, equity IRR, debt schedule | VGF % must rise or fall |
| Affordability | annual exposure vs ceiling | Project may need to be smaller |
A naive sequential walk through the chain — risk → equity → VfM → FM → affordability — almost never converges on the first pass. The practitioner sees a borderline VfM at the third stage, intuits that "a bit more risk transfer" might fix it, walks back to risk allocation, and re-runs everything downstream. Each manual cycle takes hours; ten cycles take a fortnight; a senior analyst's gut-feel choice of which knob to turn next is rarely auditable.
The iterative framework formalises that "go-back-and-tweak" instinct into two closed feedback loops the optimiser executes automatically:
┌─────────────────────────────────────────────┐
│ │
▼ │
Risk allocation (GIH baseline) │
│ │
▼ │ outer loop A
Cost of equity (CAPM) │ flip the next
│ │ transferable risk
▼ │ toward Private
Value for Money ─── VfM gate fails ────────────────┘
│ no more risks → INFEASIBLE
▼ pass
Financial Model ◄─── inner loop B
(bankability) bisect VGF % on [0, vgf_cap]
│ until min ADSCR ≥ target AND
│ equity IRR ≥ Ke AND LLCR ≥ 1
▼ pass
Fiscal effect ─── ceiling exceeded ⇒ flag breach
(per-jurisdiction) per affected jurisdiction
│
▼
VERDICT (with smallest VGF that satisfies every gate)
The inner loop is what actually sizes the VGF. Bankability is
monotone in VGF: more grant money means lower outstanding debt, so
ADSCR rises, equity IRR rises, LLCR rises. (Concessional loan terms
shift the curves but don't change the monotonicity.) That gives us a
classical monotonic bisection problem on [0%, vgf_cap%]. The
search proceeds as a three-phase decision tree:
┌──────────────────────────────────────┐
│ Step 1 — probe the floor: VGF = 0 % │
└──────────────────┬───────────────────┘
│
┌────────────┴────────────┐
bankable not bankable
│ │
▼ ▼
return VGF = 0 % ┌───────────────────────────┐
(no public support │ Step 2 — probe the ceiling │
is needed) │ VGF = vgf_cap │
└────────────┬───────────────┘
│
┌────────────┴────────────┐
bankable not bankable
│ │
▼ ▼
┌──────────────────────────┐ return INFEASIBLE
│ Step 3 — bisect inside │ (project cannot be
│ [lo = 0, hi = cap] │ made bankable even
│ mid = ½ (lo + hi) │ at the support cap)
│ bankable(mid)? │
│ yes → hi ← mid │
│ no → lo ← mid │
│ repeat until │
│ hi − lo < tol │
└────────────┬─────────────┘
│
▼
return VGF = hi
(the smallest VGF on the
bisection grid that still
passes every bankability
gate, default tol = 0.5 %)
Each step calls the financial model engine which runs the full debt waterfall, computes annual ADSCR, equity IRR, and LLCR, and returns whether all bankability gates are satisfied. With a typical project (5-tranche debt, 25-year contract, monthly waterfall), one FM run takes 0.5 to 1 second. Bisection converges in 7–10 iterations, so the inner loop completes in well under a minute even on cold starts.
A typical bisection trace on a project that needs a non-trivial subsidy (the §10 worked example happens to clear at VGF = 0 % and so does not exercise the full bisection):
INNER #1 VGF= 0.00% ADSCR= 0.83x IRR= 6.2% ✗ adscr 0.83 < 1.05
INNER #2 VGF= 40.00% ADSCR= 1.91x IRR=27.4% ✓ all checks pass
INNER #3 VGF= 20.00% ADSCR= 1.31x IRR=15.9% ✓ pass — shrink hi
INNER #4 VGF= 10.00% ADSCR= 1.06x IRR=10.4% ✓ pass — shrink hi
INNER #5 VGF= 5.00% ADSCR= 0.95x IRR= 8.4% ✗ fail — grow lo
INNER #6 VGF= 7.50% ADSCR= 1.02x IRR= 9.5% ✗ fail — grow lo
INNER #7 VGF= 8.75% ADSCR= 1.05x IRR=10.0% ≈ pass — converged
↳ smallest VGF that meets bankability
If the VfM gate fails, the inner loop is irrelevant: there is no VGF that makes a value-destroying PPP attractive. The outer loop instead reaches up to the risk allocation and transfers one risk at a time from Public/Shared to Private. Each flip lifts the implied risk premium, raises Ke, and changes the PSC-vs-PPP comparison. The priority order is a transferability heuristic — most-transferable risk first — that the optimiser adopts as a practitioner convention, informed by the GIH allocation matrices but not taken from them (the GIH catalogue allocates risks Public/Private/Shared by sector; it does not rank them by transferability)2:
Risks that policy-and-practice never transfer (site acquisition, permitting, legal change, expropriation) are excluded from the flip list. If the optimiser exhausts every transferable risk and VfM is still failing, the verdict is infeasible as a PPP — the project should be reconsidered as a traditional public investment, divested, or redesigned.
The discrete loop has one property that is provable and one that is a well-motivated heuristic — and audit-grade use depends on telling them apart:
For practitioners who want a tighter search across the full
financial-structure design space (concessional rate, concessional
tenor, concessional share, all simultaneously) — for example, when
negotiating a multilateral loan that is itself a moving target — the
platform also offers a continuous-mode optimiser (§8.6) that
searches a 4-dimensional bounded box using SciPy's
differential_evolution1. The continuous mode trades convergence
proof for flexibility: any candidate point in the box can be
evaluated, and the search is fully reproducible via a fixed RNG seed.
Both loops collapse the multi-gate feasibility test into a single scalar loss so any optimisation library can drive it. The formulation is a Big-M penalty:
\[\mathcal{L}(a,\, v,\, c) \;=\; M_{a}\bigl(\tau_{\mathrm{ADSCR}} - \mathrm{ADSCR}_{\min}\bigr)^{+} + M_{i}\bigl(K_e - \mathrm{IRR}_{\mathrm{eq}}\bigr)^{+} + M_{\ell}\bigl(1 - \mathrm{LLCR}_{\min}\bigr)^{+} + v\]
where \((x)^{+} \equiv \max(0,\, x)\) and the penalty weights satisfy \(M_a,\, M_i,\, M_{\ell} \gg v_{\max}\) — in practice \(M_a = M_{\ell} = 10^{6}\) and \(M_i = 10^{5}\). This guarantees that the optimiser will always prefer a feasible (low-VGF, gates satisfied) candidate to any infeasible one, however small that infeasible candidate's VGF.
The same loss formulation drives the discrete bisection (the
bankable() test is just loss == vgf for that candidate) and the
continuous SciPy search (where the loss surface looks like a flat
plateau of M-sized infeasible territory with a feasible "valley"
the size of the bounded box; the optimiser efficiently slides into
the valley and then drives vgf down within it).
The combined loops terminate when one of three things happens:
vgf_cap% the project
fails bankability. Output: the trace showing the bisection
collapsed at the cap, plus the binding gate(s).Every termination — converged or infeasible — writes one row into
the workspace's tamper-evident audit chain (sha256-linked
ActivityLog) and is downloadable as a signed PDF with the
audit row hash embedded as a QR code. A reviewer can therefore
verify, without re-running the optimisation, that the report they
hold is bit-for-bit the one the platform produced at the time of the
decision.
Figure 2 — Iteration loop schematic showing the two automatic feedback paths around the five-stage feasibility chain.
Risk allocation is the foundation of every PPP. It determines:
The international convention is the Global Infrastructure Hub (GIH) risk taxonomy2, which catalogues 12–25 risks across the project lifecycle and provides default allocation guidance (Public, Private, Shared) by sector. Hellowell and Vecchi (2012)3 examine the link from the project's risk allocation to the equity-side risk premium priced into PPP equity returns; the cost-of-equity formula in §4.1 operationalises that link by attaching a basis-point increment to each risk transferred to the SPV, weighted by severity.
/risk)The Risk module loads the GIH catalogue and lets the practitioner override allocations per risk. Each scenario stores:
risk_allocations: {risk_id: "Public" | "Private" | "Shared"}alloc_justifications: {risk_id: string}risk_register: the materialised view with likelihood × impact and
status trackingThe implied risk premium \(\pi\) is computed as
\[\pi \;=\; \pi_{\max}\cdot \frac{\sum_{r}\, s_r \cdot \alpha_r^{\mathrm{SPV}}} {\sum_{r}\, s_r \cdot 100}\]
where \(r\) indexes the risk catalogue (the GIH allocation categories), \(s_r\) is a severity weight assigned by the platform — an Austral configuration layered on the GIH categories, not a GIH-supplied value — \(\alpha_r^{\mathrm{SPV}} \in \{0, 50, 100\}\) is the share of risk \(r\) retained by the SPV (Public, Shared, Private respectively), and \(\pi_{\max} = 4.0\%\) is the platform's default ceiling on the implied risk premium (configurable per workspace, set to align with the order of magnitude of equity-side risk premia discussed in Hellowell and Vecchi (2012)3).
There is no fixed gate at this stage — risk allocation is the input the next stages consume. The optimiser treats it as the design variable it adjusts when downstream VfM fails.
Figure 3 — Risk allocation matrix.
The Capital Asset Pricing Model (CAPM) gives the cost of equity:
\[K_e \;=\; R_f \;+\; \beta\!\cdot\!\mathrm{ERP} \;+\; \mathrm{CRP} \;+\; \pi_{\mathrm{illiq}} \;+\; \pi\]
where:
The WACC follows:
\[\mathrm{WACC} \;=\; (1 - w_d)\,K_e \;+\; w_d\,K_d\,(1 - t)\]
with \(w_d\) the debt share, \(K_d\) the cost of debt, and \(t\) the corporate tax rate.
/equity/calculator)The Equity module is a CAPM calculator backed by Damodaran's beta and country-risk-premium tables. It accepts:
It returns Ke, WACC, and the full decomposition (Rf, asset β, relevered β, Blume-adjusted β, ERP, CRP, default spread).
There is no hard gate — the practitioner sanity-checks Ke is in a plausible band (typically 10–25% in real terms for emerging markets). Out-of-band Ke usually signals an input error.
The Risk → Equity handoff is automated: the "Send to..." popover
on a Risk scenario forwards the implied premium directly into the
Equity calculator's additional_premium_pct field, plus seeds the
country and sector hint.
Figure 4 — Risk → Equity handoff banner.
The Value-for-Money test compares the Net Present Cost (NPC) of two delivery models for the same service:
Both NPCs are discounted to the same base year. The PSC discount rate is the social discount rate (typically 5–8% real); the PPP discount rate is often the WACC of the structuring partner.
VfM = PSC_NPC - PPP_NPC (positive = PPP saves money). Expressed as
% of PSC: VfM% = VfM / PSC_NPC × 100.
A Monte Carlo overlay perturbs the demand and cost assumptions to
produce a distribution; the headline statistic is P(PSC > PPP) — the
probability the PPP beats the comparator across the simulated paths.
/vfm)The VfM module wraps ppp_core.engine.psc.calc_psc_and_ppp for the
deterministic comparison plus ppp_core.engine.vfm_monte_carlo for the
stochastic overlay. It also stores qualitative-VfM questionnaire scores
(screening, incentives, NFB, full questionnaire).
The conventional gate is
\[\mathrm{VfM} \,\geq\, 5\% \quad\wedge\quad \mathbb{P}\bigl(\mathrm{PSC} > \mathrm{PPP}\bigr) \,\geq\, 0.60.\]
Both are configurable in the optimiser's gating thresholds. If either fails, feedback loop A transfers the next "transferable" risk from Public/Shared to Private (in priority order: construction overrun, schedule, demand, O&M, design, technology, archaeological, force majeure) and re-runs Stages 2–3.
The manual Equity → VfM handoff prefills the WACC as the PPP discount rate; the optimiser (§8), however, discounts the PPP-side cash flows at the cost of equity (Ke) — a deliberately conservative choice (it makes the PSC-vs-PPP test harder to pass), and the one the worked example (§10) uses.
Figure 5 — VfM screen with handoff banner.
The Financial Model (FM) projects the SPV's cash flows year-by-year under a specific financing structure (debt, equity, grants, concessional loans). It produces:
A project is bankable when
\[\mathrm{ADSCR}_{\min} \,\geq\, \tau_{\mathrm{ADSCR}} \quad\wedge\quad \mathrm{IRR}_{\mathrm{eq}} \,\geq\, K_e \quad\wedge\quad \text{no covenant breach.}\]
Typical lender targets: \(\tau_{\mathrm{ADSCR}}\) between \(1.20\) and \(1.30\), and equity IRR at or above the cost of equity required by the SPV's investors.
/fm)The FM module wraps ppp_core.engine.model.run_model. The full input
schema (ModelInputs) covers general project parameters, opex
streams, revenue model (user-pays / availability / mixed), funding
mix (5 debt tranches + equity + grants), and government payments.
The FM offers a debt-sizing solver (bisection on debt%) and a built-in health check that aggregates 10 binary tests.
The optimiser's bankability gate is
\[\mathrm{ADSCR}_{\min} \;\geq\; \tau_{\mathrm{ADSCR}} \quad\wedge\quad \mathrm{IRR}_{\mathrm{eq}} \;\geq\; K_e^{\,\text{target}}\]
where \(\tau_{\mathrm{ADSCR}}\) is the lender's covenant floor (typically \(1.20\)–\(1.30\)) and \(K_e^{\,\text{target}}\) is the cost-of-equity hurdle the SPV's investors require — by default the CAPM-derived Ke from Stage 2, but caller-overridable in the optimiser's gating thresholds.
If the gate fails, feedback loop B bisects the VGF % of capex on
[0%, vgf_cap%] until bankability passes. VGF is injected as
funding.government_contribution = vgf_pct × total_investment.
The user supplies the concessional loan (share, rate, tenor, grace) as a fixed input — the optimiser doesn't tune it, since it is typically a policy choice or a multilateral commitment.
Figure 6 — Financial Model bankability output.
A bankable project may still be unaffordable. The fiscal affordability test compares the government's annual exposure across the contract life against the fiscal ceiling:
For each year, the exposure / ceiling ratio gives a usage %; any year above 100 % marks a ceiling breach.
The IMF/World Bank PFRAM 2.05 is the de facto reference tool for this aggregation — an analytical tool aligned with IPSAS 32 / GFSM 2014 rather than an accounting standard itself; Austral's FARO module (its fiscal-affordability and FCCL-aggregation tool) follows its approach.
/faro/affordability)FARO's affordability endpoint takes the workspace's PPP project portfolio, projects GDP forward at the macro growth rate, and computes direct + contingent exposure year-by-year. It returns:
An FM-to-affordability adapter (via the FM-to-affordability adapter) auto-translates FM-shape projects (which use a different schema than PFRAM) into the PFRAM-shape series the affordability calc expects, so a bankable FM project can flow into the FCCL check with no manual re-import.
\[E_j(t) \,\leq\, C_j \quad \forall\, t \in [t_0,\, t_0 + T],\ \forall\, j \in \mathcal{J}\]
i.e. for every year \(t\) over the contract horizon \(T\) and every jurisdiction \(j\), exposure \(E_j(t)\) stays at or below the jurisdiction's ceiling \(C_j\).
If the gate fails, the project is infeasible at the planned scale. Mitigations: defer construction start, reduce capex, shift to a user-pays revenue model, or cap government commitments.
Figure 7 — FARO affordability after the FM-to-affordability adapter runs. The exposure reads ~0.00% of GDP because the seeded demo is a user-pays toll concession at VGF = 0%: it places no direct fiscal commitment on the government, so a near-zero FCCL series is the correct result, not an empty one. The adapter is still exercised — it carries the FM cash flows into the FCCL check, which here returns ~0.
The platform ships two algorithms that solve the same sizing problem from §2.1 with different trade-offs:
Both modes use the same financial-model engine, the same VfM engine and Monte Carlo, the same per-jurisdiction FCCL aggregation, and the same audit chain. A discrete-mode verdict and a continuous-mode verdict on the same project are therefore directly comparable: any difference in the recommended VGF is attributable purely to the difference in search strategy, not to a change in the underlying calculation.
The technical specifications of both modes — request bodies, response shapes, algorithm configuration, default bounds — live in Appendix C so this section can stay conceptual.
When the VfM gate fails, the discrete optimiser walks down a fixed priority list of transferable risks and flips them, one at a time, from Public or Shared toward Private. The list is the international convention for "this risk can move":
Each flip lifts the implied risk premium, raises the cost of equity, and changes the PSC-vs-PPP comparison. Risks that policy and practice never transfer (site acquisition, permitting, legal change, expropriation) are excluded from the list and remain Public regardless of the VfM verdict. If the optimiser exhausts the transferable list without VfM passing, the verdict is infeasible as a PPP — the project should be reconsidered as a traditional public investment, divested, or redesigned.
VGF is searched on [0 %, vgf_cap %] with a default cap of 40 % of
capex. The bisection terminates at a 0.5 % tolerance — i.e. the
recommended VGF is reported within half a percentage point of capex
of the smallest value the bankability gate accepts. The first probe
at 0 % checks whether the project is already bankable without any
public support; the second probe at the cap checks whether even the
maximum support is enough. If both probes fail in the same
direction, the verdict is infeasible rather than a meaningless
midpoint number.
The five-stage chain runs on five thin "bridges" — one per hop — that translate the output shape of one Austral module into the input shape of the next. The bridges are conceptually:
| # | Hop | What the bridge does |
|---|---|---|
| A | Risk → Equity | Derive an implied risk premium from the allocation matrix and feed it as the SPV-specific premium into CAPM. |
| B | Equity → VfM | Discount the PPP-side cash flows at the cost of equity (Ke) for the PSC-vs-PPP comparison (a conservative choice — the manual prefill forwards the WACC, but the optimiser uses Ke). |
| C | Equity → FM | Forward the cost of equity as the equity-IRR target for the bankability gate. |
| D | VfM → FM | Forward the underlying ModelInputs so the bankable model is the same project the VfM Monte Carlo accepted. |
| E | FM → Affordability | Aggregate FM cash flows into the PFRAM shape and run the fiscal-ceiling calc per jurisdiction. |
Each bridge is a thin adapter (~30 lines of code) that the optimiser calls in sequence; the practitioner can also call them one at a time from the UI, via the "Send to…" handoff buttons, when working in manual mode.
Both modes are exposed as a single HTTP POST that returns a verdict payload — a one-line summary, the smallest converged VGF, the per-jurisdiction fiscal-exposure series, and a per-iteration trace that explains how the search got there. The discrete endpoint also has a Server-Sent-Events sibling that streams each iteration step as it lands, so the UI shows live convergence rather than a spinner. A typical synchronous wall time is 1 to 60 seconds for the discrete mode and 30 to 60 seconds for the continuous mode (40 to 60 financial-model evaluations at roughly half a second each).
The full request and response schemas, including all gating thresholds, jurisdiction inputs, and continuous-mode bounds, are documented in Appendix C.
The continuous mode searches a four-dimensional bounded box over VGF percentage, concessional share, concessional rate, and concessional tenor, holding the risk allocation fixed at the user-supplied baseline. Inside the box, the same Big-M loss formulation from §2.6 collapses every gate violation into a single scalar that any black-box optimiser can drive. The platform uses SciPy's differential-evolution algorithm because it is already shipped with the engine, handles bounded continuous problems natively, and — crucially for an audit-grade tool — is fully reproducible from a single integer seed. Re-running the same inputs with the same seed produces a bit-for-bit identical trace, which is what makes the continuous-mode verdict signable in the same way the discrete-mode verdict is.
The trade-off is straightforward: the continuous mode buys flexibility (and richer trade-off information across the financing-structure design space) at the cost of a longer wall time and a search that converges to the best candidate seen rather than to a provably optimal point. For decisions that need to be defended on convergence guarantees alone, the discrete mode is preferred; for decisions where the financing package is itself a moving target, the continuous mode is the better fit.
The continuous mode supports three objectives, all sharing the same gates and the same Big-M penalty for infeasible candidates:
| Objective | Secondary term to minimise | When to use |
|---|---|---|
min_vgf (default) |
\(v\) — the VGF as % of capex | Cabinet decision: "what's the smallest upfront grant we can commit to?" |
min_fiscal_npv |
\(\displaystyle \frac{1}{\mathrm{capex}}\!\left[v\cdot\mathrm{capex} + \sum_{t=1}^{T}\frac{\mathrm{AP}_t + \mathrm{Grants}_t}{(1+r_g)^t} + \frac{\mathrm{Terminal}}{(1+r_g)^T}\right]\) | Treasury decision: "what financing structure minimises the present value of every commitment over the contract life?" The required sovereign_discount_rate_pct is recorded in the audit row so the answer can be replayed with a different rate. |
max_irr_within_envelope |
\(-\mathrm{IRR}_{\mathrm{eq}}\), with an additional Big-M penalty if \(v > V_{\mathrm{cap}}\) | Procurement decision: "we have a hard fiscal envelope of \(V_{\mathrm{cap}}\) — get us the most attractive bid the market will respond to." Requires vgf_envelope_pct. |
The three answers diverge in cases where each one matters most.
min_vgf and min_fiscal_npv agree when the project is mostly
user-pays (no AP / grants stream), but split decisively for
availability-based PPPs at long tenors and high sovereign discount
rates. max_irr_within_envelope typically pushes VGF up to the cap
to maximise leverage and equity returns — which is exactly what a
competitive procurement wants to see.
All three objectives use the same FM engine, the same VfM Monte Carlo, the same per-jurisdiction FCCL aggregation, and the same audit chain. A run from any objective is directly comparable to a run from any other on the same project.
Collapsing every gate violation and every sizing knob into a single scalar is the right thing to do when the policymaker has already made the priority call ("we want the smallest grant"). When the policymaker hasn't — when it's still genuinely a trade-off between upfront grant, equity returns, and lifetime fiscal cost — the right output isn't one answer, it's a trade-off curve.
The platform exposes
POST /workflow/optimize/pareto, which runs NSGA-II6 (a standard
multi-objective evolutionary algorithm, implemented here in pure
numpy with no extra dependency) over the same four design variables
as the continuous mode, with three competing objectives:
\[\min_{a,\,v,\,c}\; \bigl(\, v,\; -\mathrm{IRR}_{\mathrm{eq}},\; \mathrm{NPV}_{\mathrm{fiscal}}(v,c;r_g)\,\bigr) \quad\text{s.t. all gates pass.}\]
The Big-M penalty from §2.6 is added to each objective component so infeasible points dominate themselves out of the front naturally. The endpoint returns the non-dominated set of candidates plus each candidate's full design vector and KPIs. The frontend renders three pairwise 2-D scatters (VGF × IRR, VGF × NPV, IRR × NPV) with the front in purple over the dominated cloud in grey, plus a sortable click-to-select table of front candidates. Picking a point on the curve (cheap-upfront-but-low-IRR vs. competitive-bid-but- expensive) replaces picking the answer.
Reproducible via fixed seed; audit chain compatible.
Bankability at the central case isn't bankability under the
scenarios that actually break PPPs. The platform therefore exposes
POST /workflow/optimize/robust, which wraps the discrete-mode
optimiser in an outer stress loop and reports the smallest VGF
that keeps the project bankable across every shock in a
user-selected catalogue:
\[\min_{a,\,v,\,c}\; v \quad\text{s.t.}\quad \text{gates pass}\bigl(f(a, v, c, s)\bigr)\quad \forall\, s \in \mathcal{S}.\]
The MVP ships with a built-in catalogue of five shocks:
For each candidate VGF, the optimiser re-runs the FM under every shock and only counts it as feasible if every shock clears the gate. The verdict identifies the binding shock (the one that forces the highest required VGF) and per-shock binding status, so the practitioner sees not just "you need 18% VGF to be robust" but "the +200 bps rate shock is what's binding — if you can hedge that out, the required VGF drops to 11%". This is the natural ask from rating agencies, fiscal-risk officers, and lender independent monitors.
The discrete mode's outer loop walks a fixed priority list of
transferable risks one flip at a time — a defensible heuristic but
not an exhaustive search. For projects with strongly correlated
risks (e.g. construction overrun ↔ schedule overrun) the greedy
policy can miss combinations that a continuous search finds. The
Pareto endpoint accepts a flag search_risk_allocation: true that
extends the design vector from 4 to 13 dimensions: each of the
nine transferable GIH risks gets a continuous αr ∈ [0, 1]
representing the SPV's retained share of that risk, smooth-thresholded
to Public/Shared/Private at 0.25/0.75 for the verdict. The implied
risk premium and per-candidate Ke are recomputed from the alpha
vector inside the search loop. The four risks policy never transfers
(site acquisition, permitting, legal change, expropriation) are not
in the search vector — they are held at their baseline (Public)
throughout, so the continuous search never proposes a transfer the
discrete loop's exclusion list also forbids.
VfM Monte Carlo isn't re-run per candidate — at 300 simulations per VfM call, that would multiply search cost by ~300×. The MVP keeps VfM as a one-shot baseline computation and lets the per-candidate Ke do the discriminating work; a more rigorous "VfM-per-candidate" mode is on the roadmap.
The continuous-mode endpoint accepts an algorithm flag with two
options:
differential_evolution (default): SciPy's DE, deterministic
given a seed, ~50 evaluations to converge on a 4-D problem, no
extra dependency.bayesian: scikit-optimize's gp_minimize with EI acquisition
and Sobol initial points. Sample-efficient (~20 evaluations) but
with a per-call GP-fit overhead that only pays off when each FM
evaluation is itself expensive (>5 s — typical for multi-currency
Monte Carlo).If scikit-optimize isn't installed on the host, the optimiser
falls back to differential-evolution with a recorded algorithm_used
field in the audit row. No error, no behaviour change for existing
callers; the choice is a soft preference, not a hard requirement.
Both modes accept a list of jurisdictions — federal, state, municipal — each carrying a configurable share of the project's contingent liability and judged against its own fiscal ceiling expressed as a percentage of its own GDP. The fiscal-effect block reports per-entity peak exposure, headroom, and breach year (if any). The verdict surfaces a one-line warning when any jurisdiction breaches its ceiling, even if the headline (federal- only) ceiling is comfortably within bounds — a state-level contingent liability can sink an otherwise-affordable project at the level of government that actually carries it.
The optimiser also accepts a reporting-currency translation: the financial-model engine continues to run in the project's financing currency, but the headline fiscal-effect series is translated into the reporting currency for direct comparison against the government's own ceilings. The translation supports two modes:
CurrencySettings.fx_path
accepts an explicit per-year array. The aggregator translates
year-by-year flows using the per-year rate; short paths pad with
the last value, long paths truncate to the contract term. This is
the foundation for forward-curve translation (IMF WEO forwards)
and for stochastic FX scenarios — both are now wire-up rather
than design problems.When a path is supplied, the response carries the full fx_path
back so the frontend can chart it alongside the fiscal-effect
series. The headline fx_rate field reports the path average for
at-a-glance display.
Multi-objective Pareto runs and per-shock robust loops routinely take longer than the 60-second comfort window for a synchronous HTTP request. The platform exposes a generic asynchronous-job interface so the frontend doesn't block:
POST /workflow/optimize/jobs accepts a {kind, body} envelope
(kind ∈ {"continuous", "pareto"}) and returns immediately with a
job_id.GET /workflow/optimize/jobs/{id} returns the job's status
(queued | running | completed | failed | cancelled),
progress, result audit row ID, and a small metadata dict.POST /workflow/optimize/jobs/{id}/cancel is best-effort: the
worker checks a cancelled-set between phases.The MVP runs the worker in-process via FastAPI BackgroundTasks
rather than dispatching through a Celery broker. Same UX as a
production Celery setup (immediate job_id response, polling for
status, cancellation, audit-row-on-completion); when the deployment
needs horizontal worker scaling, the dispatcher swaps to Celery in a
single method — the row schema, the polling endpoint, and the
frontend stay identical.
Every optimiser run — converged or infeasible, discrete or continuous — writes one row into the workspace's tamper-evident audit chain. Each row is sha256-linked to the previous one, so inserting, deleting, or rewriting any row in the past breaks every hash downstream and is detectable by inspection. The recorded payload contains the full input body, a canonical hash of the inputs, a canonical hash of the trace, the headline KPIs, and the per-jurisdiction breach status.
Each row can be downloaded as a signed PDF that re-renders the
verdict and embeds the row hash both as printed text and as a QR
code in the page header. A Supreme Audit Institution or lender
independent monitor can scan the QR code, query the platform's
audit endpoint, and confirm that the report they hold is bit-for-bit
the canonical one — without trusting the platform itself. The PDF
is generated by deterministically replaying the optimisation
against the stored inputs, and an X-Replay-Match HTTP header
indicates whether the replay produced the same trace hash as the
chain. Any drift between replay and chain is a tamper signal that a
reviewer can act on.
This section is the practical core of the paper. A reader with no prior Austral exposure can reproduce the entire feasibility loop end- to-end by following these eight steps.
Navigate to https://platform.austral-intelligence.com. The platform automatically creates an anonymous "guest" workspace on first visit; no signup is required.
Figure 8 — Workspace dashboard with the Workflow Optimizer button.
In the left sidebar, open Financial Model. Click Demo Reset → Load demo pack. Five demo projects appear (Accra-Tema Motorway, Atacama Solar PV, Bogota Airport T3, Lagos Water Treatment, Regional Hospital Nairobi).
Repeat for Risk Platform so a baseline GIH allocation matrix is available.
Figure 9 — Seeded FM portfolio.
Click the green Workflow Optimizer button in the workspace
dashboard header (or navigate to
/workspaces/<wid>/workflow/optimize).
Figure 10 — Workflow Optimizer landing page.
| Card | Field | Default | What it means |
|---|---|---|---|
| Project source | dropdown | Accra-Tema Motorway | Pick any FM scenario as the base project |
| Concessional loan | Share % of debt | 25 | What fraction of debt is concessional |
| Concessional loan | Rate % | 3.5 | Concessional annual interest rate |
| Concessional loan | Tenor (yrs) | 25 | Repayment period |
| Concessional loan | Grace (yrs) | 4 | Grace period before repayment starts |
| CAPM | Country | Ghana | Damodaran country lookup for CRP (matches the Accra–Tema demo) |
| CAPM | Sector mix | Engineering/Construction (100 %) | Drives the unlevered asset beta |
| CAPM | Bond yield % | 6.5 | 10-year sovereign yield (Rf) |
| CAPM | D/E % | 250 | Project debt-to-equity ratio |
| CAPM | Tax % | 25 | Corporate tax rate |
| CAPM | Cost of debt % | 8 | Senior commercial debt rate |
| Gating | VfM % min | 5 | Minimum VfM% for "positive" verdict |
| Gating | P(PSC>PPP) min | 0.6 | Minimum confidence from MC |
| Gating | Min ADSCR target | 1.05 | Lender's minimum coverage |
| Gating | Equity IRR min % | 18 | Required equity return |
| Gating | Max outer iter | 8 | Cap on risk-allocation flips |
| Gating | Max inner iter | 12 | Cap on VGF bisection steps |
| Gating | VGF cap % | 40 | Maximum VGF as % of capex |
You can click Load demo defaults to populate all fields with known-converging values.
The button shows a spinner; backend computation typically takes 1–60 seconds.
Figure 11 — Optimizer running.
The result panel renders three areas:
Figure 12 — Converged verdict with KPI tiles.
Each row in the trace is one decision point:
Figure 13 — Iteration trace expanded.
Below the trace, a stacked bar chart shows the annual fiscal commitments year-by-year, with debt outstanding overlaid. The three summary tiles report max contingent (peak), total direct commitments (nominal sum), and terminal handback value.
Figure 14 — Fiscal effect chart + summary tiles.
You now have a complete feasibility report on this project that can be replicated, parameterised, and re-run.
Project: Accra-Tema Motorway Extension (Ghana, 30-year user-pays toll concession, $620M total investment — the platform's seeded demo project)
Inputs: - 25% concessional debt at 3.5% (25-year tenor, 4-year grace) - CAPM: Ghana, 6.5% Rf, Engineering/Construction sector, 250% D/E, 25% tax, 8% cost of commercial debt - Gating: VfM% ≥ 5%, P(PSC>PPP) ≥ 0.6, Min ADSCR ≥ 1.05, Equity IRR ≥ 18%
Demo-data run (1.0s wall time):
status: converged
Iteration trace
LOOP A #1 Public/Private/Shared = 3/4/6
implied_premium = 2.15% Ke = 27.37% WACC = 12.11%
VfM% = 132.86% P(PSC>PPP) = 1.00
gate: VfM is positive (no risk flip needed)
LOOP B #1 VGF = 0.00%
min ADSCR = 1.052x Equity IRR = 22.08%
gate: PASS
Verdict
Bankable with VGF = 0% of capex after 1 outer and 1 inner iterations.
Final fiscal exposure (selected years). This is a user-pays toll concession, so the government makes no availability payments and, at VGF = 0%, no grant — its direct fiscal commitment is effectively zero. The debt outstanding is the SPV's, shown for context, not a government liability:
| Year | Govt availability payments ($M) | Govt grant / VGF ($M) | SPV debt outstanding ($M) |
|---|---|---|---|
| 2026 | 0 | 0 | 465 |
| 2030 | 0 | 0 | 410 |
| 2040 | 0 | 0 | 184 |
| 2050 | 0 | 0 | 0 |
Reading the result: the project is bankable WITHOUT public support (VGF = 0%) at the relaxed 1.05x ADSCR target, on the baseline risk allocation (3 Public / 4 Private / 6 Shared, no flips needed). Cost of equity is 27.4% (Ghana's high sovereign-plus-emerging-market risk premium, relevered to 250% D/E); WACC is 12.1% once the 25% concessional tranche and senior debt are blended in. One honest caveat: the equity-IRR gate in this run is the sponsor's stated 18% hurdle — below the 27.4% CAPM cost of equity — so VGF = 0% means the structure clears that 18% hurdle and the 1.05x ADSCR floor, with equity earning 22.08%; holding equity to its full CAPM cost of equity would require a positive VGF. Surfacing exactly that distinction is the point of the gate. Because this is a user-pays toll concession carrying no subsidy, the government's direct fiscal commitment is effectively zero — the fiscal-effect view (Figure 14) correctly shows ~$0 of direct commitments and only the SPV's declining debt-outstanding profile, which is exactly what a user-pays structure should produce. (The reported VfM of 132.86% is a demo-data artefact: the seeded public-sector-comparator inputs are deliberately unfavourable to public delivery, so read it as "the VfM gate passes comfortably", not as a realistic 132% margin.)
Counterfactual: raising the bankability target to 1.30x makes the project infeasible even at 40% VGF — the demo project's revenue profile simply can't deliver lender-grade coverage. This is exactly the kind of finding the framework surfaces: a project that LOOKS attractive on VfM may not be bankable; a project that's bankable may not be affordable; the iteration loop systematically searches for the structure that satisfies all three.
fx_path array. Auto-fetching IMF WEO forward curves and generating stochastic FX paths from a calibrated GBM are still on the follow-up list.max(terminal value, outstanding debt) at handback. More granular guarantee modelling (MRG payouts, FX guarantees) would add precision.Eleven distinct features power the framework. Each writes into the same tamper-evident audit chain, so any one of them can be reviewed by an SAI or lender independent monitor with the same QR-coded signed PDF.
| # | Feature | Where | Notes |
|---|---|---|---|
| 1 | Discrete optimiser (greedy + bisection) | §8.1–§8.3 | Original audit-grade default. |
| 2 | Continuous optimiser (SciPy DE) | §8.6 | 4-D search over VGF + concessional structure. |
| 3 | Live SSE iteration stepper + step commentary | §8.5 | Real-time progress; one-line commentary per step, generated from the step's own numbers. |
| 4 | Tamper-evident audit chain + signed PDF | §8.9 | sha256 chain, QR-coded row hash, X-Replay-Match header. |
| 5 | Per-jurisdiction + multi-currency awareness | §8.7 | Federal/state split, per-jurisdiction breach status, scalar FX or per-year fx_path. |
| 6 | Objective family | §8.6.1 | min_vgf, min_fiscal_npv, max_irr_within_envelope. |
| 7 | Multi-objective Pareto front (NSGA-II) | §8.6.2 | Non-dominated set over VGF × IRR × fiscal NPV. |
| 8 | Robust optimisation under stress | §8.6.3 | 5-shock catalogue; reports binding shock + worst-case VGF. |
| 9 | Continuous risk-allocation search | §8.6.4 | 13-D NSGA-II with per-candidate Ke from α-derived implied premium. |
| 10 | Bayesian optimisation alternative | §8.6.5 | algorithm: "bayesian" flag with graceful DE fallback. |
| 11 | Async job queue | §8.8 | Generic /jobs dispatcher with status polling. |
The follow-up list is deliberately short. None of these is a blocker for any of the eleven shipped features; each adds depth in a specific direction.
fx_path is user-supplied. Wiring the existing WEO fetcher in austral-platform/backend/app/modules/faro/weo.py to populate the path automatically is a contained change. A stochastic mode (Monte Carlo FX paths from a calibrated GBM) would output a fan chart (P5/P50/P95)./workflow/optimize/pareto/compare endpoint that takes two audit IDs and returns the dominated/dominating regions of each front is a small addition.max(terminal value, outstanding debt) simplification with explicit MRG payouts and FX-guarantee modelling. Touches the FARO module more than the optimiser.https://austral-intelligence.com/research (in-page comments
are open on every section)The appendices below are reference material for practitioners and implementers. They restate the API contracts, the algorithm configuration, the cross-module bridges, and the glossary in the form most useful for adapting the framework to a different platform or a different country setup. None of the appendices is required reading for the main argument of the paper.
The same optimisation can be run via API (no UI required) for embedding in batch pipelines or external tools:
TOKEN=$(curl -sS -X POST https://platform.austral-intelligence.com/api/v1/auth/guest \
-H "Content-Type: application/json" -d '{}' | jq -r .access_token)
WID=$(curl -sS -H "Authorization: Bearer $TOKEN" \
https://platform.austral-intelligence.com/api/v1/workspaces | jq -r '.[0].id')
# (Optional) seed demo data
for t in fm risk; do
curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
"https://platform.austral-intelligence.com/api/v1/workspaces/$WID/$t/demo/seed"
done
# Pull the demo project's model_inputs
PID=$(curl -sS -H "Authorization: Bearer $TOKEN" \
"https://platform.austral-intelligence.com/api/v1/workspaces/$WID/projects?source=fm" \
| jq -r '.[0].id')
MI=$(curl -sS -H "Authorization: Bearer $TOKEN" \
"https://platform.austral-intelligence.com/api/v1/workspaces/$WID/projects/$PID" \
| jq '.project_data.model_inputs')
# Run the optimiser
PAYLOAD=$(jq -n --argjson mi "$MI" '{
project_inputs: $mi,
risk_baseline: {},
concessional_loan: {share_pct: 25, rate_pct: 3.5, tenor_years: 25, grace_years: 4},
capm: {country: "Ghana", bond_yield_pct: 6.5,
sectors: [{sector: "Engineering/Construction", weight_pct: 100}],
de_ratio_pct: 250, tax_rate_pct: 25,
beta_formula: "hamada", apply_blume: true,
illiquidity_pct: 1.0, cost_of_debt_pct: 8.0},
gating: {vfm_pct_min: 5, p_psc_gt_ppp_min: 0.6,
min_adscr_target: 1.05, equity_irr_min_pct: 18.0}
}')
curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "$PAYLOAD" \
"https://platform.austral-intelligence.com/api/v1/workspaces/$WID/workflow/optimize" \
| jq '.summary'
Expected output (using the seeded demo): a converged verdict with
the same KPI block the UI displays.
| Endpoint | Method | Purpose |
|---|---|---|
/handoff/targets/{source_module} |
GET | List valid handoff targets for a source module |
/handoff/equity/from-risk/{rid} |
GET | Risk → Equity prefill (additional_premium_pct + country + sector hint) |
/handoff/vfm/from-risk/{rid} |
GET | Risk → VfM prefill (PSC/PPP risk allocation hint) |
/handoff/vfm/from-equity/{eid} |
GET | Equity → VfM prefill (WACC as ppp_discount_rate) |
/handoff/fm/from-equity/{eid} |
GET | Equity → FM prefill (Ke as target equity IRR) |
/handoff/fm/from-vfm/{vid} |
GET | VfM → FM prefill (full model_inputs forward) |
/faro/affordability/from-fm |
POST | FM-to-affordability adapter — aggregates FM cash flows for the fiscal-ceiling check |
/workflow/optimize |
POST | Run the entire chain with auto-iteration |
The main text of §8 keeps the optimiser conceptual. This appendix restates the algorithm configuration and the HTTP contracts in implementation terms, suitable for reproducing the work on a different platform or for embedding the optimiser in a batch pipeline.
DISCRETE MODE — outer loop A (risk allocation, max 10 iterations):
premium = implied_premium(allocation)
Ke = CAPM(country, sectors, premium, …)
vfm = run_psc_ppp_and_mc(project, ke)
if VfM gate passes: break
else: flip the next transferable risk to Private → repeat
DISCRETE MODE — inner loop B (VGF, max 15 iterations bisected on [0%, vgf_cap%]):
inputs = inject(project, concessional_loan, vgf_mid)
fm = run_fm(inputs)
if bankable(fm, target_adscr, target_ke): vgf_hi = mid
else: vgf_lo = mid
break when |vgf_hi - vgf_lo| < tol (default 0.5%)
FINAL STAGE (both modes):
fccl = aggregate_per_jurisdiction(fm, jurisdictions, fx_rate)
return fiscal_effect(fccl), per-jurisdiction breach status
CONTINUOUS MODE — single 4-D search:
bounds = [(vgf_min, vgf_max), (share_min, share_max),
(rate_min, rate_max), (tenor_min, tenor_max)]
loss(x) = Big-M(adscr_violation, irr_violation, llcr_violation) + x[vgf]
result = differential_evolution(loss, bounds=bounds, seed=seed,
maxiter=maxiter, popsize=popsize,
init='sobol', polish=False)
return best_so_far(x), full per-evaluation trace
Configuration choices:
differential_evolution with init='sobol'
(low-discrepancy initial population), polish=False (skips the
local L-BFGS-B step which doesn't help with discontinuous gate-
violation losses), and workers=1 (sequential evaluation, keeping
the trace deterministic).M_adscr = 10⁶, M_irr = 10⁵, M_llcr = 10⁶, and a small
linear + vgf_pct so the search drives VGF down inside the
feasible region.seed is recorded in the audit row and the
signed PDF, so the same inputs + seed reproduce the same trace
bit-for-bit. Verified by automated regression tests.POST /workspaces/{wid}/workflow/optimize with body:
{
"project_inputs": { ... full ModelInputs },
"risk_baseline": { "risk_id": "Public" | "Private" | "Shared", ... },
"concessional_loan": { "share_pct": 25, "rate_pct": 3.5, "tenor_years": 25, "grace_years": 4 },
"capm": { "country": "Ghana", "bond_yield_pct": 6.5, "sectors": [...], "de_ratio_pct": 250, "tax_rate_pct": 25, "cost_of_debt_pct": 8.0 },
"gating": { "vfm_pct_min": 5, "p_psc_gt_ppp_min": 0.6, "min_adscr_target": 1.05, "max_outer_iter": 8, "max_inner_iter": 12, "vgf_cap_pct": 40 },
"jurisdictions": [{ "name": "federal", "share_pct": 100, "fiscal_ceiling_pct_gdp": 2.0, "gdp_nominal_m": 0 }],
"currency": { "reporting_currency": "USD", "financing_currency": "USD", "fx_rate": 1.0 }
}
returns:
{
"status": "converged" | "infeasible" | "max_outer_iter",
"trace": [ ... per-iteration snapshot ... ],
"summary": {
"verdict": "Bankable with VGF=12.5% of CapEx after 2 outer and 7 inner iterations.",
"outer_iterations": 2,
"inner_iterations": 7,
"final_allocation": { ... },
"final_ke_pct": 16.2,
"final_vfm_pct": 8.4,
"final_min_adscr": 1.31,
"final_vgf_pct": 12.5,
"fiscal_effect": { "max_contingent_peak": ..., "annual_availability_payments": [...], "by_jurisdiction": [...] },
"any_jurisdiction_breach": false,
"audit_id": "...",
"audit_row_hash": "...",
"elapsed_sec": 4.1
}
}
A live-streamed sibling endpoint at
POST /workspaces/{wid}/workflow/optimize/sse accepts the same body
and emits each iteration step as a Server-Sent Event so the UI can
render convergence progress in real time.
POST /workspaces/{wid}/workflow/optimize/continuous with body:
{
"project_inputs": { ... full ModelInputs },
"risk_baseline": { ... fixed allocation, the search does not flip risks },
"capm": { ... },
"gating": { ... same gates as discrete mode },
"bounds": {
"vgf_pct_min": 0, "vgf_pct_max": 30,
"conc_share_pct_min": 10, "conc_share_pct_max": 50,
"conc_rate_pct_min": 1.5, "conc_rate_pct_max": 6.0,
"conc_tenor_years_min": 18, "conc_tenor_years_max": 30
},
"concessional_grace_years": 4,
"jurisdictions": [...],
"currency": {...},
"seed": 42,
"max_evals": 60,
"objective": "min_vgf"
}
A converged response includes the winning candidate's design vector
(best_x), the full per-evaluation trace (with each evaluation's
x, loss, kpis, feasible flag, and best_so_far_loss), and
the same summary block as the discrete mode so the same downstream
tools (verdict card, fiscal-effect chart, audit log, signed PDF)
work unchanged.
POST /workspaces/{wid}/workflow/optimize/pareto with body:
{
"project_inputs": { ... },
"risk_baseline": { ... },
"capm": { ... },
"gating": { ... },
"jurisdictions": [...],
"currency": {...},
"concessional_grace_years": 4,
"bounds": { ... same shape as continuous mode ... },
"sovereign_discount_rate_pct": 6.0,
"seed": 42,
"population_size": 40,
"n_generations": 20,
"search_risk_allocation": false
}
Returns front (list of non-dominated candidates with full design
vector + KPIs + per-jurisdiction fiscal effect + (when
search_risk_allocation=true) per-candidate allocation_summary
and ke_pct), trace (every evaluation), evaluations,
n_generations, population_size, seed, n_dim (4 or 13).
The synchronous endpoint is soft-capped at
population_size × n_generations ≤ 250; larger searches must use
the async job-queue (§C.7).
POST /workspaces/{wid}/workflow/optimize/robust with body:
{
"project_inputs": { ... },
"risk_baseline": { ... },
"concessional_loan": { ... },
"capm": { ... },
"gating": { ... },
"jurisdictions": [...],
"currency": {...},
"stress_set": [
"rate_plus_200bps",
"traffic_minus_10pct",
"fx_depreciation_15pct",
"opex_overrun_5pct",
"capex_overrun_10pct"
],
"vgf_pct_min": 0.0,
"vgf_pct_max": 40.0,
"vgf_tol_pct": 0.5,
"max_inner_iter": 15
}
Returns baseline_vgf_pct, robust_vgf_pct (worst across all
shocks), binding_shock, vgf_uplift_pct, per_shock (array of
per-shock smallest-bankable-VGF + binding status),
n_shocks_evaluated, n_infeasible, plus the standard
fiscal_effect block on the binding (worst-case) candidate.
A companion GET /workflow/optimize/robust/catalogue lists the
built-in shock catalogue.
| Endpoint | Purpose |
|---|---|
POST /workflow/optimize/jobs |
Submit a long-running job. Body is {kind, body} where kind ∈ {"continuous", "pareto"} and body is the kind-specific payload. Returns immediately with {id, status: "queued", ...}. |
GET /workflow/optimize/jobs |
List the workspace's recent jobs, newest first. |
GET /workflow/optimize/jobs/{id} |
Poll one job. Status transitions: queued → running → completed | failed | cancelled. On completion, result_audit_id points at the audit row that records the search trace + verdict. |
POST /workflow/optimize/jobs/{id}/cancel |
Best-effort cancellation. The worker checks the cancelled set between phases. |
| Endpoint | Purpose |
|---|---|
GET /workflow/optimize/runs |
List every optimiser run for the workspace, newest first, with KPI summary and audit row hashes. |
GET /workflow/optimize/runs/{audit_id} |
Return one run's full audit detail (the row's recorded payload, including the stored inputs and trace hashes). |
GET /workflow/optimize/runs/{audit_id}/report.pdf |
Re-render the run as a signed A4 PDF with the row hash printed and embedded as a QR code in the page header. The response also carries an X-Replay-Match header indicating whether the deterministic replay produced the same trace hash as the chain. |
| Term | Meaning |
|---|---|
| ADSCR | Annual Debt Service Coverage Ratio — CFADS / annual debt service |
| FM-to-affordability adapter | The single hop from a bankable FM project to its fiscal-ceiling check |
| CAPM | Capital Asset Pricing Model — Ke = Rf + β·ERP + premiums |
| CFADS | Cash Flow Available for Debt Service |
| CRP | Country Risk Premium (from Damodaran's default-spread × volatility multiplier) |
| ERP | Equity Risk Premium (mature market) |
| FCCL | Fiscal Commitments and Contingent Liabilities |
| GIH | Global Infrastructure Hub — widely-used reference risk-allocation matrices (Public/Private/Shared by sector) |
| Handoff | Cross-module input transfer in Austral; triggered by "Send to..." button |
| Ke | Cost of equity (real or nominal, project-specific) |
| LLCR | Loan Life Coverage Ratio — NPV(CFADS over loan life) / debt outstanding |
| MRG | Minimum Revenue Guarantee |
| PFRAM | IMF/WB PPP Fiscal Risk Assessment Model — methodology for FCCL aggregation |
| PLCR | Project Life Coverage Ratio — NPV(CFADS over project life) / debt outstanding |
| PPP | Public–Private Partnership |
| PSC | Public Sector Comparator |
| SPV | Special Purpose Vehicle (the project company) |
| VfM | Value for Money |
| VGF | Viability Gap Funding (upfront grant from government, % of capex) |
| WACC | Weighted Average Cost of Capital |
Storn, R., & Price, K. (1997). "Differential Evolution — A Simple and Efficient Heuristic for Global Optimization over Continuous Spaces." Journal of Global Optimization, 11 (4), 341–359. The algorithm Austral's continuous mode invokes via SciPy's scipy.optimize.differential_evolution. https://doi.org/10.1023/A:1008202821328. ↩
Global Infrastructure Hub. Allocating Risks in Public-Private Partnership Contracts (PPP Risk Allocation Tool), World Bank Group / G20 Global Infrastructure Hub. The per-sector default Public/Private/Shared allocations used by the discrete-mode outer loop are taken from this catalogue's matrices; the transferability ordering in which the outer loop flips risks is Austral's own practitioner heuristic and is not part of the GIH catalogue (which does not rank risks by transferability). https://www.gihub.org/resources/publications/allocating-risks-in-public-private-partnership-contracts/. ↩↩
Hellowell, M., & Vecchi, V. (2012). "What return for risk? The price of equity capital in public-private partnerships." Paper presented at the International Research Society for Public Management (IRSPM) Annual Conference, 2012. Used as the conceptual reference for linking project risk allocation to the equity-side risk premium that the SPV prices into PPP equity returns. https://www.research.ed.ac.uk/en/publications/what-return-for-risk-the-price-of-equity-capital-in-public-privat. ↩↩
Damodaran, A. Country Risk Premiums, Equity Risk Premiums, and Industry Betas, NYU Stern School of Business, updated annually. The platform's CAPM block reads the latest annual tables for risk-free rate, ERP, country default spread, and unlevered industry beta. https://pages.stern.nyu.edu/~adamodar/. ↩↩
International Monetary Fund and World Bank. PPP Fiscal Risk Assessment Model (PFRAM 2.0), IMF/WB, 2019. The methodology Austral's FARO module implements for per-jurisdiction direct-commitment and contingent-liability aggregation against fiscal ceilings. https://www.imf.org/external/np/fad/publicinvestment/pdf/PFRAM.pdf. ↩
Deb, K., Pratap, A., Agarwal, S., & Meyarivan, T. (2002). "A fast and elitist multiobjective genetic algorithm: NSGA-II." IEEE Transactions on Evolutionary Computation, 6 (2), 182–197. The Pareto-front endpoint runs a numpy-only re-implementation of this algorithm. https://doi.org/10.1109/4235.996017. ↩