Skip to content

Advanced Concepts

Penalties

FlexTool uses penalty (slack) variables to relax constraints that might otherwise cause infeasibility. Each penalty has a very high cost in the objective function that makes the optimizer try to avoid these violations unless no other option is viable.

How penalties work

When a constraint cannot be satisfied (e.g., demand exceeds available supply), a slack variable absorbs the deficit. The cost of this violation is added to the objective function. The penalty cost should therefore be set high enough to prevent violations unless there is genuinely no feasible alternative, but not so high that it causes numerical issues. The penalty could also be based on perceived cost of the violation: e.g. value of lost load in case of node balance violation.

Penalty scaling with time

Most penalties scale with the duration of the violation -- a 3-hour violation costs 3 times as much as a 1-hour violation. The table below summarizes how each penalty is treated:

Penalty parameter Unit Constraint type Scales with step_duration Notes
penalty_up (node) CUR/MWh Energy balance Yes Cost per MWh of energy not served
penalty_down (node) CUR/MWh Energy balance Yes Cost per MWh of excess energy
penalty_reserve (reserve group) CUR/MW Reserve requirement Yes Cost per MW-hour of reserve shortfall
penalty_inertia (group) CUR/MWs Inertia requirement Yes Cost per MWs-hour of inertia shortfall
penalty_non_synchronous (group) CUR/MWh Non-synchronous limit Yes Cost per MWh of non-sync limit violation
penalty_capacity_margin (group) CUR/kW Capacity margin No Cost per kW of capacity shortfall per period. Analogous to investment cost -- but not annualized. It still has operational inflation adjustment.

Capacity margin penalty

The capacity margin penalty is different from the others: it represents the cost of not having sufficient installed capacity. It is analogous to an investment cost (CUR/kW) but is NOT annualized over a lifetime like actual investments. Instead, it applies as a lump cost per period. This means a penalty_capacity_margin of 1000 CUR/kW is much more expensive than an invest_cost of 1000 CUR/kW (which would be annualized to roughly 50-100 CUR/kW/year depending on lifetime and discount rate). Set the penalty to reflect the annual cost of capacity shortfall, not the total investment cost.

Economic Modelling

Overview

FlexTool minimizes total system cost over a planning horizon. The economic framework uses two key rates:

  • inflation_rate (model-level): Adjusts for general price level changes over time. Whether to use inlfation_rate depends on whether you use real or nominal values elsewhere in the input data - be consistent.
  • discount_rate (per technology): Reflects the financing cost and risk of each investment

Real vs nominal values

Economic values can be expressed in two ways:

  • Real (constant prices): All values are expressed in today's money. A power plant that costs 1000 EUR/kW today still costs 1000 EUR/kW in 2040 in real terms -- the purchasing power is the same.
  • Nominal (current prices): Values include inflation. The same plant might cost 1300 EUR/kW in 2040 at 2% annual inflation, even though its real cost hasn't changed.

Example: A coal plant costs 1500 EUR/kW today. At 2% inflation:

  • Real cost in 2035: 1500 EUR/kW (unchanged)
  • Nominal cost in 2035: 1500 x (1.02)^10 = 1829 EUR/kW approximately

Rule: Never mix real and nominal values in the same model. If inflation_rate = 0, all inputs must be in real terms. If inflation_rate > 0, all inputs must be in nominal terms.

The inflation rate

The model-level inflation_rate parameter (default: 0) adjusts all future costs to a common price base. It applies uniformly to all cost types: investment costs, fuel costs, fixed O&M, penalties, etc.

  • Real inputs (most common): Set inflation_rate = 0. All costs in all years use today's prices. A fuel cost of 50 EUR/MWh means the same purchasing power whether it occurs in 2025 or 2040. To discount future costs with a social discount rate, set inflation_rate to that rate instead (see Discounting between milestone years).
  • Nominal inputs: Set inflation_rate to expected inflation (e.g., 0.02 for 2%). The model deflates future costs: a nominal cost of 60 EUR/MWh in 2035 is treated as 60 / (1.02)^10 = 49.2 EUR/MWh approximately in today's money.

The inflation rate applies via the factor 1 / (1 + inflation_rate)^years. Investment costs are assumed to occur at the beginning of each year; operational costs at the middle of the year (adjustable with inflation_offset_investment and inflation_offset_operations).

Discounting between milestone years

Only inflation_rate discounts. FlexTool discounts the costs of later periods (milestone years) with the model parameter inflation_rate and nothing else. Each period's costs are multiplied by a discount factor: the sum, over the years the period represents, of 1 / (1 + inflation_rate)^years_from_start. The per-technology discount_rate does not discount between periods: it only turns an invest_cost into a yearly payment (see The discount rate).

How the years are counted. The years from the start of the run come from years_represented: a period starts after the years that the periods before it represent. The period names are only labels, so p2030 is not read as the year 2030. Give each milestone the number of years until the next milestone (for example 5 for p2025 when the next period is p2030). Investment costs are discounted from the start of a year (inflation_offset_investment, default 0), operating costs from the middle of it (inflation_offset_operations, default 0.5).

With inflation_rate 0 there is no discounting. A cost in 2040 then weighs the same as the same cost in 2025: each period's yearly costs are only multiplied by its years_represented. This is right for a model in real terms that should not prefer earlier or later costs, but it is not what most planning studies want.

A social discount rate. In a model with real (constant-price) inputs, enter the social discount rate as inflation_rate, for example 0.05 for 5 %. The model applies the same factor whether the rate stands for inflation or for time preference, so the discounted costs are then present values at that rate. Keep the per-technology discount_rate at the real cost of capital of each technology. In a model with nominal inputs, use the combined rate: (1 + inflation) × (1 + social discount rate) − 1.

Where to see it. summary_solve.csv lists the Operational discount factor and the Investment discount factor of each period. The cost results come both annualized (one year of the period, not discounted) and discounted (present value over the years the periods represent); see Costs.

The discount rate (per technology)

Each unit, connection, and storage node can have its own discount_rate parameter (default: 0.05, i.e., 5%). This represents the weighted average cost of capital (WACC) -- the return that investors require to finance the technology.

The discount rate converts a lump-sum investment cost into annual payments over the technology's lifetime using the standard annuity formula:

annual_payment = invest_cost * discount_rate / (1 - (1 / (1 + discount_rate))^lifetime)

A discount_rate of 0 gives the straight-line annuity invest_cost / lifetime (no interest). Without a value the rate is 0.05; a negative rate is an input error. The lifetime has no default: it is required when the entity invests or retires with a cost (invest_cost, salvage_value), and when its lifetime_method is reinvest_choice or no_investment (also for existing capacity without investment), since the capacity then ends after the lifetime.

An invest_method other than not_allowed offers investment (retirement) in every investment period, also without an invest_cost (salvage_value) or where it is 0: that capacity has no annuity, and its fixed_cost still counts (a retirement saves it). A 0 in an invest_cost map therefore does not block investment in that period; to forbid investment in a period, use a period method (invest_period, invest_period_total or their invest_retire_ forms) and set invest_max_period 0 for it; the other methods do not use invest_max_period. With neither invest_cost nor fixed_cost any invested amount is optimal, so the result is arbitrary. A zero-cost investment needs a lifetime only with lifetime_method reinvest_choice or no_investment.

Example: A solar plant costs 800 EUR/kW with a 25-year lifetime and 5% discount rate:

annual_payment = 800 * 0.05 / (1 - 1/1.05^25) = 800 * 0.0710 = 56.8 EUR/kW/year approximately

Different technologies can have different discount rates reflecting their risk profiles:

  • Low risk (e.g., established solar PV): 3-5%
  • Medium risk (e.g., natural gas turbine): 5-7%
  • High risk (e.g., novel technology): 8-12%

Consistency between rates

The discount_rate and inflation_rate must use the same price basis across one model instance (either real or nominal):

Inputs inflation_rate discount_rate (entity)
Real (constant prices) 0, or the social discount rate Real WACC (e.g., 5%)
Nominal (current and future prices) Expected inflation (e.g., 2%) Nominal WACC (e.g., 7%)

The relationship between real and nominal rates follows the Fisher equation:

(1 + nominal_rate) = (1 + real_rate) * (1 + inflation_rate)
Approximation: nominal = real + inflation approximately (e.g., 5% + 2% = 7%).

Fixed costs

Fixed costs (fixed_cost parameter on units, connections, and nodes) represent annual operation and maintenance costs that are incurred regardless of how much the asset is used. They are expressed in CUR/kW/year (or CUR/kWh/year for storage).

For existing assets, fixed costs are applied each year with inflation adjustment.

For invested assets, the fixed cost is paid in every period the new capacity operates within the model horizon: until its lifetime ends, or in every later period with lifetime_method reinvest_automatic. Each period's fixed cost is discounted like the other costs of that period. Years after the model horizon are not counted, the same as for the investment annuity, so the investment decision weighs the costs of the years the model covers. In the results, these costs are reported as a separate line item (fixed cost invested) from the investment annuity.

Summary of economic parameters

Parameter Level Default Description
inflation_rate Model 0 General inflation rate. Set to 0 for real inputs.
inflation_offset_investment Model 0 When in the year investment costs occur (0 = start)
inflation_offset_operations Model 0.5 When in the year operational costs occur (0.5 = middle)
discount_rate Entity 0.05 Technology-specific WACC for annualizing investments; 0 gives invest_cost / lifetime
invest_cost Entity - Overnight investment cost [CUR/kW]
lifetime Entity - Economic lifetime [years]; required when the entity invests or retires with a cost
fixed_cost Entity - Annual fixed O&M cost [CUR/kW/year]
salvage_value Entity - Residual value at end of life [CUR/kW]

How multi-year periods are handled internally

A period can represent multiple years via the years_represented parameter on the solve entity. For example, a period p2025 with years_represented = 5 means that the modelled timeline (e.g., one representative week) stands in for 5 full years (2025-2029).

Internally, FlexTool expands this into per-year entries. A period with years_represented = 5 becomes 5 rows in the solve data, each representing 1 year at a different distance from the start of the run (the first period of the model's first solve; a later solve counts the years of the earlier solves' periods first). The inflation factor then sums the per-year inflation adjustments:

inflation_factor = Σ_y  p_years_represented[d,y] × 1/(1 + inflation_rate)^(years_from_run_start[d,y] + p_years_represented[d,y] × offset)

where years_from_run_start[d,y] is the start of year y of period d (a fractional year counts as its fraction: after a period of 2.5 years the next one starts at 2.5) and offset is inflation_offset_investment or inflation_offset_operations.

With inflation_rate = 0, each term contributes p_years_represented[d,y] × 1 = 1, and the sum equals years_represented (i.e., 5). With inflation_rate = 2%, each year is deflated slightly more, giving a sum of approximately 4.81.

This mechanism also supports fractional values. With years_represented = 0.5 (half-year resolution for investment decisions), the inflation factor is 0.5 × 1/(1+π)^y, correctly counting only half a year of costs.

In the objective function, operational costs follow this pattern:

cost_per_timestep × step_duration × inflation_factor / period_share_of_year

where period_share_of_year = hours_in_period / 8760 converts the modelled period to an annual equivalent, and the inflation factor then scales across all represented years. Investment costs follow a similar pattern using a separate inflation factor that accounts for investment timing.

Using periods that are not one year long

A period does not have to represent exactly one year. It can be shorter (e.g. a half-year period for six-monthly investment decisions) or stand in for several years (see How multi-year periods are handled internally). FlexTool keeps the accounting correct for any period length — you mostly just need to know what is scaled and what is not.

What a period's length means

Each period has a length derived from its modelled timeline, period_share_of_year = hours_in_period / 8760. Investment granularity is set separately with years_represented on the solve — including fractional values such as years_represented = 0.5 for half-year steps.

What stays correct automatically

  • Operating costs and energy balances are absolute. They are summed over the timesteps actually in the period, so a shorter period simply incurs less; a longer one, more. No year constant enters these totals.
  • Annual (per-year) parameters are prorated by period length. Annual fixed_cost, for instance, is applied as fixed_cost × period_share, so a half-year period pays half a year of fixed cost. Investment annuities and discounting likewise use the period's (possibly fractional) year count.

What "annualised" in the results means

The ..._annualized result columns divide a period's totals by period_share_of_year to express them as a per-year rate. This is only a reporting convenience so that periods of different lengths can be compared on the same footing — it does not change the optimisation or the period totals. If you want the actual amount incurred in the period, read the plain (non-annualised) total.

When NOT to rely on inflow scaling

The inflow_method options scale_to_annual_flow, scale_in_proportion and scale_to_annual_and_peak_flow assume the modelled timeline is a representative year: they normalise the inflow by its sum over (one year of) the timeline. If your timeline is not a representative year — a non-representative sub-year slice, or several years with genuinely different shapes — do not use these methods. Instead provide an accurate absolute inflow time series and leave inflow_method at its default (use_original); then no annual assumption is applied to your inflow.

(There is no equivalent "scale to annual" for profile-based limits such as capacity factors. A capacity factor is bounded by installed capacity, so there is no easy way to scale it accurately.)

One approximation to be aware of

When a single period represents multiple years (years_represented > 1), costs within that period are discounted at the period's single point in time rather than year by year. The totals remain correct; only the within-period discounting is approximated. Use shorter periods (more of them) if you need finer discounting resolution.