Browse the docs
Example 8 of 13
Oriole solar and storage, Mozambique
Based on oriole-solar-storage-model.xlsm
A 50 MWp build-out in three phases, the first with 7.5 MW of battery storage. Each phase has its own financial close, S-curve, tranches and returns, copied across three model sheets. A state utility holds a carried 5%, and a Parallel Model sheet uses a VBA function to size debt without a circular reference. Design example: written in the full v0.5 language. The compiler builds the working core today, so this one doesn't compile yet. Source: examples/design/oriole/.
- Workbook
- 14 sheets: three copies of a 450-row phase model, a Parallel Model driven by a VBA function, EPC quotes in English and Chinese
- Timeline
- Rulers from Jan 2019, monthly until phase 1 COD (Aug 2021), six-monthly after; phases reach FC Aug 2020, Aug 2021 and Aug 2022
- Plant
- 15 + 15 + 20 MWp single-axis tracking, P50 1,902 kWh/kWp; phase 1 adds 7.5 MW / 30 MWh of storage
- Capital
- Phase 1 EPC USD 29.1m (USD 1,940/kW with storage); 40% of equity up front, then pro rata; senior debt 75%
- Debt
- Three senior tranches and a mezzanine: 16 to 22 years, 6.5% to 8.5% all-in, 1% commitment, 1.25% front-end, 360-day interest
- Rebuild shown
- 6 files, 181 lines
Where each sheet goes
| Workbook sheets | Rufineer |
|---|---|
| Inputs (timing, operations, capex, opex, PPA, tax, financing for three phases) | inputs.rfn, and sheets of oriole_inputs.xlsx keyed by stage |
| Phase 1 Model, Phase 2 Model, Phase 3 Model | one module for s in stages: three sheets from one source |
| Parallel Model (VBA user-defined function) | one solve per stage; debt sizing written as ordinary lines |
| Summary | the dashboard with a column per stage |
| Scenarios, No Tracking, Two Axis Tracking, Performance Ratio, Yield Back-up | the Tracking sheet and a yield_case option |
| EPC Proposal, EPC Backup | the EPC sheet, one column per stage |
Source 6 files shown, 181 lines
model "Oriole Solar and Storage, Mozambique" {
currency USD
display thousands
day_count act/360 // every tranche accrues on 360 days
data "oriole_inputs.xlsx"
}
list stages = inputs.stages.rows // phase_1, phase_2, phase_3
timeline {
start 2019-01-01
step month until operations[phase_1].start, then half
for s in stages {
construction[s] from inputs.stages[s].financial_close for inputs.stages[s].build
operations[s] inputs.stages[s].life
}
}/// One row per phase
table stages {
rows stage "Phase"
columns financial_close: date "Financial close",
build: month "Construction period",
life: y "Operating life",
capacity: MWp "Nominal capacity",
battery: MW "Battery capacity",
battery_hours: h "Battery storage",
tariff: USD/MWh "Base price",
escalation_month: text "Escalation month",
alpha "S-curve alpha", beta "S-curve beta",
fixed_debt: USD "Fixed senior debt"
}
table development: USD sheet "Development cost" { rows item; columns by stages }
table epc: USD sheet "EPC" { rows item; columns by stages }
table tranches {
rows tranche
columns share: % "Share of senior debt", maturity: y "Maturity",
grace: month "Grace period", rate: %/y "All-in interest",
commitment: %/y "Commitment fee", front_end: % "Front-end fee",
repayment: text "Repayment", pri: %/y "PRI premium", pri_cover: % "PRI cover"
}
table tracking { // Scenarios sheet
rows option
columns cost: USD/kW "Cost", base_yield: kWh/kWp/y "Base yield", pr: % "Performance ratio"
}
inputs "Plant" {
transmission_loss = 2 %
degradation = 0.4 %/y
yield = { p50: 1_902.5, p90: 1_744.1, p99: 1_636.1 } kWh/kWp/y
yield_case = option p50 | p90 | p99
battery_loss = 10 % // round trip
cycles = 1 /d
land_per_mw = 2.22 ha/MW
}
inputs "Operating costs" {
om = 12.18 USD/kW/y
admin = 5.00 USD/kW/y
land_lease = 850 USD/ha/y
insurance = 0.48 %/y // of EPC
escalation = 1.5 %/y
success_fee = 2 %
}
inputs "Tax" {
tax_rate = 32 %
depreciation_life = 20y
loss_expiry = 10y
dividend_wht = 10 %
}
inputs "Financing" {
upfront_equity = 40 % // of each phase's equity, before any debt
senior_gearing = 75 %
mezzanine = 0 %
dsra_cover = 6m
sizing = option gearing | dscr | fixed
target_dscr = 1.30 x
}
table partners {
rows partner
columns share: %, carried: text "Carried"
} // Main Investor 95%, EDM 5% (carried)/// One sheet per phase, like Phase 1, 2 and 3 Model
module phase[s] "Phase {s}" for s in stages {
st = inputs.stages[s]
// Operations
capacity: MWp = st.capacity * (1 - inputs.transmission_loss)
* decline(inputs.degradation) | during operations[s]
solar: MWh = capacity * inputs.yield[inputs.yield_case] * operations[s].share
battery: MWh = st.battery * st.battery_hours * inputs.cycles
* (1 - inputs.battery_loss) | during operations[s]
// Revenue: the tariff steps up in the phase's own anniversary month
price: USD/MWh = st.tariff | grow inputs.escalation yearly in st.escalation_month
from operations[s].start
revenue: USD = (solar + battery) * price
// Capital expenditure on a beta S-curve
epc: USD = spread(inputs.epc[*, s], over: construction[s],
profile: beta(st.alpha, st.beta))
development: USD = sum(inputs.development[*, s]) | at construction[s].start
success_fee: USD = gross_up(sum(epc) + sum(development), inputs.success_fee)
| at construction[s].start
// Operating costs
opex: USD = st.capacity * (inputs.om + inputs.admin
+ inputs.land_lease * inputs.land_per_mw)
| grow inputs.escalation from operations[s].start | during operations[s]
+ sum(epc) * inputs.insurance | during operations[s]
ebitda: USD = revenue - opex
}
function gross_up(base, rate) = base * rate / (1 - rate)module finance[s] "Financing, phase {s}" for s in stages {
/// Replaces the Parallel Model's VBA function: one visible loop break per phase
solve uses: USD = phase[s].epc + phase[s].development + phase[s].success_fee
+ sum(loan[*].idc) + sum(loan[*].fees) + dsra.initial
equity_commitment: USD = (1 - inputs.senior_gearing - inputs.mezzanine) * sum(uses)
funding uses {
first equity up to inputs.upfront_equity * equity_commitment
then pro_rata {
senior share inputs.senior_gearing
mezzanine share inputs.mezzanine
equity rest
}
}
cfads: USD = phase[s].ebitda - tax.paid[s]
senior_size: USD = match inputs.sizing {
gearing => inputs.senior_gearing * sum(uses)
dscr => npv(cfads / inputs.target_dscr | during operations[s],
rate: inputs.tranches[senior_1].rate)
fixed => inputs.stages[s].fixed_debt
}
// Senior 1, 2, 3 and mezzanine, each with its own terms
for t in inputs.tranches {
loan[t] = term_loan(
amount: inputs.tranches[t].share * senior_size,
draws: funding.senior * inputs.tranches[t].share,
rate: inputs.tranches[t].rate,
window: operations[s].start + inputs.tranches[t].grace
for inputs.tranches[t].maturity,
profile: inputs.tranches[t].repayment, // "Flat DS" in the base case
fees: { commitment: inputs.tranches[t].commitment,
front_end: inputs.tranches[t].front_end }
)
pri[t]: USD = loan[t].balance.opening * inputs.tranches[t].pri
* inputs.tranches[t].pri_cover
}
debt_service: USD = sum(loan[*].debt_service) + sum(pri[*])
dscr: x = cfads / debt_service | during operations[s]
dsra = reserve(target: debt_service | next inputs.dsra_cover)
to_equity: USD = cfads - debt_service - dsra.net
}module tax "Tax" {
for s in stages {
depreciation[s]: USD = sum(finance[s].uses) / inputs.depreciation_life | during operations[s]
profit[s]: USD = phase[s].ebitda - depreciation[s] - sum(finance[s].loan[*].interest)
losses[s] = tax_losses(profit: profit[s], expiry: inputs.loss_expiry)
paid[s]: USD = losses[s].taxable * inputs.tax_rate
}
}
module owners "Partners" {
equity_draws: USD = sum(finance[*].funding.equity)
to_equity: USD = sum(finance[*].to_equity) * (1 - inputs.dividend_wht)
/// EDM holds 5% without contributing capital
split to_equity {
main_investor inputs.partners[main_investor].share
edm inputs.partners[edm].share carried
funded by equity_draws
}
overall_irr = xirr(to_equity - equity_draws)
main_irr = xirr(to_equity.main_investor) // below overall: it funds EDM's 5%
}
dashboard "Summary" {
columns for s in stages
row "Capacity (MWp)" inputs.stages[s].capacity
row "EPC cost per kW" sum(phase[s].epc) / inputs.stages[s].capacity
row "Pre-tax project IRR" xirr(phase[s].ebitda - phase[s].epc - phase[s].development)
row "Min DSCR" min(finance[s].dscr)
}scenario base "Base case: tracking, P50" {}
scenario fixed_tilt "Fixed tilt" {
inputs.yield = { p50: 1_450.5 } kWh/kWp/y // Scenarios sheet, base yield
}
scenario lenders "Lenders' P90" extends base {
inputs.yield_case = p90
}
scenario dscr_sized "Sized on DSCR" {
inputs.sizing = dscr
}What this model needed from the language
- Stages. Three phases with their own dates become
construction[s]andoperations[s], and one modulefor s in stagesreplaces three hand-copied 450-row sheets. - A switch point in the ruler. Monthly until the first phase starts operating, six-monthly after:
step month until operations[phase_1].start, then half. - Tranches from a table. Senior 1, 2 and 3 and the mezzanine loan are rows of one sheet. The loop builds a loan per row, each with its own repayment profile and political risk cover.
- Up-front equity, then pro rata.
first … then pro_rata { … }in the funding block. - A carried partner.
splitwithcarriedgives EDM 5% of distributions while the main investor funds everything, which is why its IRR sits below the overall equity IRR. - A beta S-curve. The workbook's alpha and beta inputs become
profile: beta(9, 2). - No VBA. The Parallel Model sheet's user-defined function becomes one visible
solveper phase.