<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Energy modelling blog</title>
<link>https://gkrivtchik.com/posts/</link>
<atom:link href="https://gkrivtchik.com/posts/index.xml" rel="self" type="application/rss+xml"/>
<description>A personal website and blog about energy systems modelling</description>
<generator>quarto-1.9.38</generator>
<lastBuildDate>Tue, 14 Jul 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Interactions between behaviours in capacity expansion and dispatch models</title>
  <link>https://gkrivtchik.com/posts/interaction_multiple_behaviours/</link>
  <description><![CDATA[ 




<p>This post explains how the capacity expansion and dispatch model <a href="https://github.com/oecd-nea/Nosy.jl">Nosy</a> manages interactions between behaviours when building model components. For a more general introduction to Nosy’s architecture and the reasons why I designed it that way, you can check <a href="../designing_models/">this post</a>.</p>
<p>In this post, I use “behaviour” to mean a simple component feature that serves as a building block. There are many different behaviours, including variable capacity, fixed capacity, variable cost, fixed cost, unit commitment, ramping etc. The exhaustive list is available in the appendix of this post.</p>
<p>In capacity expansion and dispatch models, non-trivial components result from the application of multiple behaviours<sup>1</sup>. For instance, Nosy uses explicit composition of one model archetype<sup>2</sup> and as many behaviours as required. This architecture makes the interaction of behaviours more explicit than other architectures, but this interaction has to be handled in every capacity expansion and dispatch model, one way or another.</p>
<p>Behaviours encapsulate a single facet of how a component exists or interacts with the rest of the system. However, designing their scope is not always trivial. Some of the behaviours only target a single notion e.g.&nbsp;fixed capacity, while others manage a small bundle of parameters e.g.&nbsp;unit commitment also manages minimum uptimes and downtimes. I wanted behaviours to be as small and conceptually orthogonal as possible, to minimize the complexity of the building blocks of components - at least for the user. Of course, splitting functionality into small, simple behaviours does not reduce the mathematical complexity; instead it shifts the responsibility for coordinating those behaviours from the user to the internal model-building logic.</p>
<p>Before discussing how behaviours interact, I also need to define the concept of port: a <code>Port</code> is a connection point on a component; it carries a specific carrier and represents its flow as a time series. In Nosy, most behaviours actually target specific ports of a component. For instance, the capacity is not defined for the whole component; it is applied specifically to one of its ports.</p>
<p>Nosy currently has 16 user-facing behaviours. The full list is provided in the appendix; their documentation is available <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/#Behaviors">here</a>.</p>
<p>Let’s consider the simple example of a PV field with optimisable capacity. We will model it as</p>
<ul>
<li>a <code>ProfileSource</code> model archetype associated with a time series, which defines something that outputs a flow following a predefined pattern;</li>
<li>a <code>VariableCapacity</code> behaviour that will create a capacity variable and tie it to the component’s capacity;</li>
<li>a <code>FixedCost</code> behaviour that will generate an annualized investment cost proportional to capacity.</li>
</ul>
<p>Let <img src="https://latex.codecogs.com/png.latex?C"> be the installed PV capacity, <img src="https://latex.codecogs.com/png.latex?%5Cphi_t"> the availability profile, <img src="https://latex.codecogs.com/png.latex?p_t"> the output at time <img src="https://latex.codecogs.com/png.latex?t">, and <img src="https://latex.codecogs.com/png.latex?c%5E%5Ctext%7Binv%7D"> the annualized investment cost per unit of capacity. Together, the archetype and behaviours add the following terms to the model:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Ap_t%20=%20%5Cphi_t%20C%20%5Cqquad%20%5Cforall%20t,%0A"></p>
<p><img src="https://latex.codecogs.com/png.latex?%0Acost%20=%20c%5E%5Ctext%7Binv%7D%20C.%0A"></p>
<p>The <code>ProfileSource</code> provides <img src="https://latex.codecogs.com/png.latex?%5Cphi_t">, <code>VariableCapacity</code> creates <img src="https://latex.codecogs.com/png.latex?C"> and uses it to define the output profile, and <code>FixedCost</code> retrieves the same <img src="https://latex.codecogs.com/png.latex?C"> to construct the investment cost. The capacity must therefore be built before the fixed cost.</p>
<p>The actual implementation of this component is detailed <a href="https://oecd-nea.github.io/Nosy.jl/dev/examples/pv-battery-storage/">here</a> in the examples section of the docs.</p>
<p>Components can be more complex than that, and in detailed studies like the system cost study of Sweden (available <a href="https://www.oecd-nea.org/upload/docs/application/pdf/2026-03/system_cost_study_of_sweden.pdf">here</a>), they can routinely include up to 20 behaviours, often with multiple instances of fixed and variable costs.</p>
<p>The dependency in the PV example is small, but it establishes the general problem: behaviours cannot always be built in the order supplied by the user. Nosy therefore needs a deliberate build order.</p>
<p>There are multiple ways to manage such interactions. One elegant way would have been a dependency-resolution mechanism. For instance, I could have implemented the dependencies below:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode julia code-with-copy"><code class="sourceCode julia"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">abstract type</span> Dependency <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">end</span></span>
<span id="cb1-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">abstract type</span> Capacity <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;:</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;"> Dependency </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">end</span></span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">requires</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">FixedCost</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (Capacity,)</span>
<span id="cb1-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">provides</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">FixedCost</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> () <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># no provision</span></span>
<span id="cb1-5"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">requires</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">VariableCapacity</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> () <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># no requirement</span></span>
<span id="cb1-6"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">provides</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">VariableCapacity</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (Capacity,)</span></code></pre></div></div>
<p>I could then build a DAG (hopefully) and topologically sort it. I like it, it looks fancy, but it is clearly overkill for Nosy. I decided to go against it because the total number of behaviours is small enough that another layer of abstraction was not necessary - the dependency graph is simple enough that it can be explicitly written.</p>
<p>I hard-coded the build order shown below<sup>3</sup>. For all model archetypes except <code>ProfileSource</code> and <code>ProfileSink</code>, the build order is:</p>
<p><code>CapacityMultiplier</code> → capacity behaviours → <code>Duration</code> → <code>UnitCommitment</code> → all remaining behaviours</p>
<p>This build order is quite simple and understandable. The general flow is that capacities are required to design unit commitment, and the other behaviours’ constructors<sup>4</sup> are independent of one another but may depend on capacities and unit commitment. The build order’s maintenance cost is explicit: whenever a new behaviour is added, its interactions with the existing behaviours must be examined before it can be placed in the sequence.</p>
<p>However, the build order itself does not explain why one behaviour must precede another. To reason about these interactions, I find it useful to distinguish two kinds of dependencies: hard requirements and conditional requirements. A hard requirement means that a behaviour is invalid when another behaviour is missing; a conditional requirement means that the behaviour remains valid on its own, but changes when another behaviour is present.</p>
<p>The table below names these dependencies directly.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Behaviour</th>
<th>Hard requirement</th>
<th>Conditional requirement</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>VariableCapacity</code></td>
<td>–</td>
<td><code>CapacityMultiplier</code></td>
</tr>
<tr class="even">
<td><code>VariableComposedCapacity</code></td>
<td>–</td>
<td><code>CapacityMultiplier</code></td>
</tr>
<tr class="odd">
<td><code>FixedCapacity</code></td>
<td>–</td>
<td><code>CapacityMultiplier</code></td>
</tr>
<tr class="even">
<td><code>FixedComposedCapacity</code></td>
<td>–</td>
<td><code>CapacityMultiplier</code></td>
</tr>
<tr class="odd">
<td><code>CapacityMultiplier</code></td>
<td>a capacity</td>
<td>–</td>
</tr>
<tr class="even">
<td><code>Duration</code></td>
<td>a capacity</td>
<td>–</td>
</tr>
<tr class="odd">
<td><code>UnitCommitment</code></td>
<td>a capacity</td>
<td>–</td>
</tr>
<tr class="even">
<td><code>Ramping</code></td>
<td>–</td>
<td>a capacity, <code>UnitCommitment</code></td>
</tr>
<tr class="odd">
<td><code>YearlySum</code></td>
<td>–</td>
<td>–</td>
</tr>
<tr class="even">
<td><code>ReserveUp</code></td>
<td>a capacity</td>
<td><code>UnitCommitment</code>, <code>Ramping</code></td>
</tr>
<tr class="odd">
<td><code>ReserveDown</code></td>
<td>a capacity</td>
<td><code>UnitCommitment</code>, <code>Ramping</code></td>
</tr>
<tr class="even">
<td><code>VariableCost</code></td>
<td>–</td>
<td>–</td>
</tr>
<tr class="odd">
<td><code>FixedCost</code></td>
<td>a capacity</td>
<td>–</td>
</tr>
<tr class="even">
<td><code>ConstantCost</code></td>
<td>–</td>
<td>–</td>
</tr>
<tr class="odd">
<td><code>NoLoadCost</code></td>
<td><code>UnitCommitment</code></td>
<td>–</td>
</tr>
<tr class="even">
<td><code>StartupCost</code></td>
<td><code>UnitCommitment</code></td>
<td>–</td>
</tr>
</tbody>
</table>
<p>These dependencies are handled at different stages: most hard requirements are checked during construction, whereas most conditional requirements alter constraint generation. Julia’s multiple dispatch makes these interactions straightforward to implement.</p>
<p>There are some more requirements than the table shows. For instance, <code>UnitCommitment</code> requires that a capacity defines the unit size; in addition, most behaviours are not properties of the whole component but of a port in particular, and the requirements are actually bound to the port and not the component. But for a simple mental model, the table is accurate enough.</p>
<p>All in all, for capacity expansion and dispatch, a behaviour should not be designed in isolation. Adding a behaviour requires identifying what it needs from other behaviours, as well as how its equations change when other behaviours are present, and vice versa.</p>
<p>Composing components from small behaviours makes the interactions explicit. In Nosy, the dependency graph is sparse and largely structured around capacity and unit commitment. As such, a fixed build order is easier to understand and maintain than a dependency-resolution mechanism. The explicit build order approach might eventually stop scaling if the number of behaviours grows substantially and new behaviours interact heavily with the others, but I doubt that this will happen. For now, the explicit build order, coupled with the requirement matrix, seems to provide the right level of abstraction: it is simple enough to inspect, and structured enough to manage components composed of many interacting behaviours.</p>
<section id="appendix-behaviours-in-nosy" class="level2">
<h2 class="anchored" data-anchor-id="appendix-behaviours-in-nosy">Appendix: Behaviours in Nosy</h2>
<p>Nosy currently provides the following 16 user-facing behaviours:</p>
<ol type="1">
<li><code>VariableCapacity</code>: constrain the flow through a target port by a single capacity variable.</li>
<li><code>VariableComposedCapacity</code>: constrain the weighted sum of the flows through target ports by a single capacity variable.</li>
<li><code>FixedCapacity</code>: constrain the flow through a target port by a fixed capacity.</li>
<li><code>FixedComposedCapacity</code>: constrain the weighted sum of the flows through target ports by a fixed capacity.</li>
<li><code>CapacityMultiplier</code>: multiply a capacity by a time-dependent vector.</li>
<li><code>Duration</code>: define a linear relationship between port capacities and the maximum level.</li>
<li><code>UnitCommitment</code>: apply the unit commitment equations to a port.</li>
<li><code>Ramping</code>: set the maximum ramping rate of a port.</li>
<li><code>YearlySum</code>: impose an upper bound, lower bound, or equality constraint on the sum of a target port’s flow.</li>
<li><code>ReserveUp</code>: allow a component port to contribute to upward reserve.</li>
<li><code>ReserveDown</code>: allow a component port to contribute to downward reserve.</li>
<li><code>VariableCost</code>: add a cost based on the flow through a component port.</li>
<li><code>FixedCost</code>: add a cost based on a component’s capacity.</li>
<li><code>ConstantCost</code>: add a constant amount to a component’s cost.</li>
<li><code>NoLoadCost</code>: add a cost based on a component’s <code>up</code> unit commitment state.</li>
<li><code>StartupCost</code>: add a cost based on a component’s <code>starting</code> unit commitment switch.</li>
</ol>
<p>So far, this set has been sufficient to create hundreds of components for various studies.</p>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>There actually is another type of building block for components in Nosy, adjacent to behaviours: the <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/#Joint-Flows">joint flows</a>, that will be described in a separate post.↩︎</p></li>
<li id="fn2"><p>The list of model archetypes in Nosy is detailed <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/#Model-Archetype">here</a>.↩︎</p></li>
<li id="fn3"><p>Small caveat: for <code>ProfileSource</code> and <code>ProfileSink</code> model archetypes, the build order is slightly changed. So there are actually two build orders. I still find that the hard-coded chain is adequate.↩︎</p></li>
<li id="fn4"><p>The build order concerns the construction of behaviours. First, all behaviours are constructed, and only after that some behaviours generate constraints. When generating constraints, some behaviours also look for the existence of other behaviours. For instance, reserve behaviours intuitively depend on ramping behaviour; however they don’t require it at construction, only during constraints generation.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>energy systems</category>
  <category>MILP</category>
  <category>optimization</category>
  <category>architecture</category>
  <category>Julia</category>
  <guid>https://gkrivtchik.com/posts/interaction_multiple_behaviours/</guid>
  <pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Snapshot and pathway formalisms for capacity expansion</title>
  <link>https://gkrivtchik.com/posts/snapshot_vs_pathway/</link>
  <description><![CDATA[ 




<p>The most common formalism for modelling capacity expansion is the static, or single-year, formulation. When modelling a capacity expansion problem for the year 2050 using this formalism, the question is: “What would the optimal system look like in 2050?” Compared to modelling a full pathway, this formalism is convenient because it simplifies the assumptions and reduces the computational burden, at the cost of a simple economic operation: the annualization of the investment costs (transforming one-time costs into equivalent recurring costs).</p>
<p>To my knowledge, most capacity expansion studies use this formalism. However, this simplification comes with assumptions that are sometimes overlooked. As the name indicates, this formalism is static: it does not consider the path leading to the target year. Depending on the system, the path might be costly, or create vulnerabilities. The formulation also omits the dynamic aspect of investment and capacity expansion.</p>
<p>One difficult question is the choice of cost reference year. Models consume cost data such as overnight / connection / O&amp;M / fuel / decommissioning / etc., in addition to time-related inputs such as lead time. When modelling year <img src="https://latex.codecogs.com/png.latex?y">, should the model incorporate cost data associated with year <img src="https://latex.codecogs.com/png.latex?y">, or a prior date corresponding to the non-modelled year when capacity is actually deployed? And should this hypothetical date be the same for each technology<sup>1</sup>?</p>
<p>In my opinion, more so than correctness, one of the biggest drawbacks of modelling snapshots is that they do not answer the question “What should I do now?”, or “What happens if I don’t do X now”. A normative snapshot of year 2050 does not answer that question about short-term priorities. It might give clues, or a general direction, but it does not prioritize actions. This is detrimental to the usefulness of the modelling effort: the reason many policy reports exist is because decision-makers need / want to know what they could do now. This drawback is reinforced by discounting: investment decisions in the near term have higher present-value weight. In other words, their relative importance is higher than that of distant-future investments. However, they are indistinguishable in a snapshot model. Of course, reports can use expert judgement or complementary analysis to try and answer the question of near-term investments, but there is often a large leap between the model outputs and the conclusions, and the near-term recommendations are usually not backed directly by a modelling effort.</p>
<p>The “pathway” formalism explicitly models the steps leading up to the target year. In general, the assumption is perfect foresight across years: the decisions taken at year <img src="https://latex.codecogs.com/png.latex?y%20-%20%5CDelta%20y"> are made with year <img src="https://latex.codecogs.com/png.latex?y"> in sight<sup>2</sup>. This is the formulation used by <a href="https://github.com/GKrivtchik/EnergyPathway.jl">EnergyPathway.jl</a>, a pathway model built on top of the composable energy system modelling toolkit <a href="https://github.com/oecd-nea/Nosy.jl">Nosy.jl</a>.</p>
<p>Modelling a scenario under the pathway formalism starts with the definition of the decision years. Assuming you wish to model the transition between 2026 and 2040, a first attempt might be to explicitly model every year in between, which would mean 15 snapshots in total. As the number of nonzeros in the optimization matrix scales roughly linearly<sup>3</sup> with the number of snapshot-years, the size of the pathway problem might quickly become too large.</p>
<p>An interesting concept is the decision year. It is generally possible to define a subset of the pathway years, and only model the snapshots in this subset. If a year is modelled as a snapshot, investment decisions can be made in that year; otherwise the year has no investment decision, and its capacity mix and other metrics are carried forward from the previous decision year: EnergyPathway.jl treats capacities and other parameters as piecewise constant between decision years. Using this concept, it is generally feasible to reduce the size of the problem. Also, using a higher density of decision years in the near-term future and reducing the density in the more distant future helps focus the model on short-term decisions.</p>
<p>Nevertheless, the introduction of decision years is not trivial. For instance, there might not be a decision year matching the exact end of a component’s lifetime. In this case, tricks must be used to minimize the impact. In EnergyPathway.jl, such tricks rely on evaluating residual economic value relative to the mismatch between the closest snapshot and the expected end of life. That is to say: pathway modelling is not exempt from assumptions either.</p>
<p>As mentioned above, the general assumption for pathway models is perfect foresight across years. This is a strong hypothesis. As with Snapshot models, sensitivity studies should be run in order to understand the most important parameters and their impact. Modelling to Generate Alternatives (MGA) and <a href="../near_optimality/">near-optimality analysis</a> are also very useful for pathways. Pathways also interact with multi-stage stochastic programming in an intuitive manner: decision years can also be used to model years in which new information becomes available, even though the complexity of the optimization model grows quickly with the number of decision years.</p>
<p>Let’s illustrate both snapshot and pathway formalisms on the reduction of CO2 emissions of a facility that consumes electricity. The full example, including hypotheses and results, is available in <a href="https://gkrivtchik.com/EnergyPathway.jl/dev/examples/facility-transition/">EnergyPathway.jl’s examples</a>. We assume a constant electricity consumption from the facility, associated with progressively more stringent CO2 emissions targets. The target year is 2040. The snapshot only models year 2040, the pathway models a subset of years: [2026, 2030, 2035, 2040]. Available technologies are: PV, battery storage, CCGT. For simplicity, the costs are assumed to be constant between 2026 and 2040. The optimization objective of the snapshot formulation is the annualized cost; the objective of the pathway approach is the discounted system cost from 2026 to 2060, 2060 being the economic end of the project.</p>
<p>The capacity expansion is shown in the graph below. After 2040, the pathway capacities are carried forward to 2060 to show the interpolation interval. The x-axis extends until 2060 because it is the pathway model’s economic horizon. However, the last decision year is 2040, so no additional capacity is deployed after 2040.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://gkrivtchik.com/posts/snapshot_vs_pathway/capacity-trajectories-dual-axis.webp" class="img-fluid figure-img" style="width:100.0%"></p>
<figcaption>Capacity expansion in the snapshot and pathway solutions</figcaption>
</figure>
</div>
<p>The capacities of all three technologies are different between the snapshot and the final pathway decision year. The PV and battery capacity are quite similar between both, but the CCGT capacity is larger in the end-state of the pathway than in the snapshot. The pathway minimizes the cost by deploying more CCGT when CO2 emissions constraint is looser, then retires some but not all of that additional capacity relative to the snapshot solution. The pathway’s last decision year, in isolation, is more costly than the snapshot, but is cost-effective under the dynamic approach.</p>
<p>It is not surprising that the end state of the pathway does not match the snapshot. The reason is that the capacity deployment has to be optimal for the whole pathway, not just for the end state. In particular, the weight of the end-state is not necessarily dominant because of discounting. The economic end date of the project (here 2060) can be used to tune the weight of the future against the present in the objective. In particular, extending the economic project end date gives more weight to the carried-forward final system, and would generally be expected to move the pathway result closer<sup>4</sup> to the snapshot optimization, assuming the same assumptions are used to model the lifetime of components.</p>
<p>All in all, pathway modelling is a powerful tool to analyze the effects of a transition, including the management of existing capacity, the timing for deployment of new technologies based on cost and demand, and time-dependent policies. In particular, it is very helpful to identify the decisions that should be made in the near term. However, even more than snapshots, pathways are very data-intensive. The amount of assumptions and data required to build a pathway is very high. But this is not necessarily a disadvantage relative to snapshots: it is rather that the snapshot formalism, and the interpretation of snapshots, imply many implicit hypotheses that rarely are validated, whereas pathways force the modeller to explicitly formulate such hypotheses.</p>




<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>In addition to the economic correctness of using specific years for cost data, there is also the question of trust in projections. Some economists prefer using present-day costs even for projections, as projections are generally, if not always, wrong. However, this is a different question that will not be discussed further in this post.↩︎</p></li>
<li id="fn2"><p>Some pathway models are myopic, meaning that decisions are made year after year. This alternative formulation is not discussed in this post.↩︎</p></li>
<li id="fn3"><p>Slightly worse than that, due to temporal coupling and decision year machinery. These additional constraints are not many, but they are difficult linking constraints.↩︎</p></li>
<li id="fn4"><p>With a discount rate <img src="https://latex.codecogs.com/png.latex?r%20%3E%200">, a cost repeated forever does not receive an infinite weight in present value. If a unit cost is repeated every period starting in the reference year, the discounted sum is a geometric series: <img src="https://latex.codecogs.com/png.latex?1%20+%20%5Cfrac%7B1%7D%7B1+r%7D%20+%20%5Cfrac%7B1%7D%7B(1+r)%5E2%7D%20+%20%5Ccdots%20=%20%5Cfrac%7B1+r%7D%7Br%7D."> In other words, carrying the final system forward to an infinite horizon gives it a finite weight - preventing the actual convergence.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>energy systems</category>
  <category>MILP</category>
  <category>optimization</category>
  <category>snapshot</category>
  <category>pathway</category>
  <guid>https://gkrivtchik.com/posts/snapshot_vs_pathway/</guid>
  <pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Multiple time meshes for MILP energy systems modelling</title>
  <link>https://gkrivtchik.com/posts/multiple_time_meshes/</link>
  <description><![CDATA[ 




<p>This is a direct continuation of the post on <a href="../heterogeneous_time_meshes/">heterogeneous time meshes</a>, which exploited the fact that temporal resolution is not equally valuable at every time of the year. In this post, we exploit the fact that temporal resolution is not equally valuable in every part of the system.</p>
<p>Let’s first define a node as an abstract, passive place where the balance of flows is enforced. A node can represent many things including a bidding zone in a regional power system, a hydrogen pipe in a hydrogen system, or a wire in an electric device. One node is only associated with one carrier. The active counterpart of the node is the component, which generates and/or consumes flows. There are many systems that require modelling of multiple nodes. In one system, multiple nodes can represent multiple geographic or market zones, multiple parts of a system supporting different carriers, etc.</p>
<p>A convention that I like, and that is applied in <a href="https://github.com/oecd-nea/Nosy.jl">Nosy</a>, is that components are connected only to nodes, and vice versa.</p>
<p>Let’s model an electricity and hydrogen system composed of PV, battery, electricity consumption, electrolyser, long-term hydrogen storage, and hydrogen consumption. The capacity of every component is an investment decision, except for consumption. Let’s also assume, for simplicity, that the electricity and hydrogen consumption profiles are flat. The full example is available in <a href="https://oecd-nea.github.io/Nosy.jl/dev/examples/power-hydrogen-mixed-mesh/">Nosy’s documentation</a>.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://gkrivtchik.com/posts/multiple_time_meshes/electricity_hydrogen_system.drawio.svg" class="img-fluid figure-img" style="width:100.0%"></p>
<figcaption>Hydrogen and electricity system</figcaption>
</figure>
</div>
<p>The levels of the battery and hydrogen storage during the first week of June are shown below, both evaluated using hourly time meshes. The data is synthetic, so the shape of the curves is more regular than with real-world data, but it still captures the essence of the dynamics.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://gkrivtchik.com/posts/multiple_time_meshes/power-hydrogen-storage-levels.svg" class="img-fluid figure-img" style="width:100.0%"></p>
<figcaption>Electricity and hydrogen storage levels</figcaption>
</figure>
</div>
<p>Let’s analyze the behaviour of storage in both nodes. On the electricity side, the characteristic time scale is shorter than a day, with large-amplitude hourly variations. In particular, the battery is used at full capacity every day. It is a well-known result that using a coarse time description when modelling systems that include PV leads to underestimating the optimal battery storage capacity, as shown in the previous post on <a href="../heterogeneous_time_meshes/">heterogeneous time meshes</a>.</p>
<p>On the hydrogen side, the H2 storage characteristic time is mostly seasonal, as it inherits from the seasonality of the PV capacity factor: hydrogen storage is charged in sunny seasons, and discharged in seasons with less sun. The intra-day variations exist, but are quite limited. In particular, slightly misrepresenting the intra-day variations would likely not lead to underestimating the optimal hydrogen storage capacity. The only times it may misrepresent the optimal capacity is when the level is close to zero or maximum level, which is expected to happen a very limited number of times per year, and may therefore have a reasonably low impact. Based on that, we can infer that representing with satisfactory precision the hydrogen node might not actually require a fine hourly time mesh. However, we do not wish to use a coarser mesh for the whole model, as that would misrepresent the role of battery storage.</p>
<p>Some models, including <a href="https://github.com/oecd-nea/Nosy.jl">Nosy</a> offer the possibility to use different time meshes. Nosy supports defining a different mesh to each component and node. The tricky part is enforcing node balance across meshes; it is enforced via a constraint: the components’ meshes must be at least as fine<sup>1</sup> as the meshes of the nodes they are connected to. Under Nosy’s formalism, the behaviour of components is not different from a single-mesh formalism: each component is associated with a single mesh, and all its flows, levels and inner variables will follow the same mesh. However, the behaviour of nodes is less intuitive, as they must manage the interaction of flows associated with multiple time meshes to apply the balance constraint. Effectively, the flows are projected on the node’s mesh. But the projection must make sense: it is defined so that the average flow is conserved for each timestep of the coarse mesh. Defining another projection e.g.&nbsp;sampling the finer mesh over the coarse would break the consistency as the flow balance seen from the component and the node sides would differ.</p>
<p>Nodes generally do not carry variables, but their mesh defines the way the node balance is enforced. To be more accurate, the gain is not coming from modelling nodes with a coarser mesh, as the number of non-zeros in the optimization matrix is not really impacted by modelling nodes on a coarse or fine mesh. The gain is coming from modelling components using a coarser mesh. A component using a coarser mesh generally has a lower number of optimization variables and constraints. The exact reduction depends on the nature of the component, and the way its behaviour is encoded, but the reduction of the number of variables is often almost proportional with the ratio of timesteps in the meshes.</p>
<p>In the present case, we can try using two meshes: <img src="https://latex.codecogs.com/png.latex?m_1"> is hourly and <img src="https://latex.codecogs.com/png.latex?m_4"> uses 4-hour time steps, and assign them the following way:</p>
<ul>
<li><img src="https://latex.codecogs.com/png.latex?m_1"> for PV, battery, electricity consumption, electricity node and electrolyser.</li>
<li><img src="https://latex.codecogs.com/png.latex?m_4"> for H2 storage, H2 consumption and the H2 node.</li>
</ul>
<p>In particular, the electrolyser uses the fine mesh, although it is connected to both nodes. This is an interesting feature, because it keeps the temporal consistency of everything happening on the electricity side, while being compatible with the coarser hydrogen node.</p>
<p>We run the model using the combination of fine and coarse meshes (denoted as “mixed”) and compare the results with a reference case using only the fine mesh. Solve time is based on the HiGHS solver.</p>
<table class="caption-top table">
<caption>Comparison of the results: capacity of each component. Units are unimportant.</caption>
<colgroup>
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
</colgroup>
<thead>
<tr class="header">
<th style="text-align: left;">Mesh</th>
<th style="text-align: right;">PV capacity</th>
<th style="text-align: right;">Battery storage capacity</th>
<th style="text-align: right;">Electrolyser capacity</th>
<th style="text-align: right;">H2 storage capacity</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Fine</td>
<td style="text-align: right;">13.397</td>
<td style="text-align: right;">16.050</td>
<td style="text-align: right;">6.086</td>
<td style="text-align: right;">1,967.50</td>
</tr>
<tr class="even">
<td style="text-align: left;">Mixed</td>
<td style="text-align: right;">13.397</td>
<td style="text-align: right;">16.050</td>
<td style="text-align: right;">6.086</td>
<td style="text-align: right;">1,962.88</td>
</tr>
</tbody>
</table>
<table class="caption-top table">
<caption>Comparison of the objective and problem complexity indicators. Units are unimportant except for solve time.</caption>
<thead>
<tr class="header">
<th style="text-align: left;">Mesh</th>
<th style="text-align: right;">Objective value</th>
<th style="text-align: right;">Variables</th>
<th style="text-align: right;">Constraints</th>
<th style="text-align: right;">Solve time (s)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Fine</td>
<td style="text-align: right;">4.299740</td>
<td style="text-align: right;">61,324</td>
<td style="text-align: right;">78,840</td>
<td style="text-align: right;">29.982</td>
</tr>
<tr class="even">
<td style="text-align: left;">Mixed</td>
<td style="text-align: right;">4.299735</td>
<td style="text-align: right;">41,614</td>
<td style="text-align: right;">59,130</td>
<td style="text-align: right;">19.032</td>
</tr>
</tbody>
</table>
<p>First, the results show that capacities of PV, battery storage, and electrolyser are the same as for the fine mesh, within the reported precision. The hydrogen storage capacity evaluated using a coarse mesh for hydrogen, is reasonably close to the reference (-0.2%). The objective value also is very close to the reference case (-0.0001%). The number of variables and constraints is reduced thanks to the coarser mesh, which reduced the solve time by 37%.</p>
<p>Depending on the goals of the study, this can be a desirable simplification, with substantial reduction of the solve time at the cost of a slight optimistic bias in the objective value.</p>
<p>Using a coarser mesh for specific components and nodes is a viable strategy to improve the tractability of a model. Some systems are more favourable to this approximation than others. The main characteristics of components and nodes that are likely to be compatible with a coarser mesh are the domination of a long-term pattern over short-term variations. Using multiple meshes is also compatible with <a href="../heterogeneous_time_meshes/">heterogeneous time meshes</a>; using a combination of both takes advantage of the fact that resolution is more valuable at some times and in some parts of the system.</p>




<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>I define that mesh <img src="https://latex.codecogs.com/png.latex?m_1"> is finer or equal to mesh <img src="https://latex.codecogs.com/png.latex?m_2"> if <img src="https://latex.codecogs.com/png.latex?m_1"> includes all the instants of <img src="https://latex.codecogs.com/png.latex?m_2"> (plus optionally others).↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>energy systems</category>
  <category>MILP</category>
  <category>optimization</category>
  <guid>https://gkrivtchik.com/posts/multiple_time_meshes/</guid>
  <pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Heterogeneous time meshes for MILP energy systems modelling</title>
  <link>https://gkrivtchik.com/posts/heterogeneous_time_meshes/</link>
  <description><![CDATA[ 




<p>In MILP optimization of energy systems, the computational burden of the optimization, generally referred to as “solve time”, is a prime pain point. Outside of simple examples or benchmark cases, the modelling accuracy of most real problems is limited by the solve time. This is by design, as one can see the maximum solve time as a budget, and add as much information and complexity as possible while remaining under the budget. Every addition or change to a model has to be carefully implemented and tested to check the impact on the solve time.</p>
<p>Solve time in MILP problems is difficult to estimate, in particular because they’re extremely difficult problems solved with extremely smart heuristics, and finding a solution feels like luck. Nevertheless, reducing the number of variables without introducing complexity generally decreases the solve time<sup>1</sup>.</p>
<p>One large source of variables is the time scope. Modelling one full year with hourly resolution generates 8760 variables per series. 100 such series are almost sufficient to reach one million variables, which is one of the fuzzy limits to start calling a problem “difficult”.</p>
<p>In what follows, I will call “time mesh” the vector of time step durations. For instance, for the hourly modelling of a year, the time mesh will be <img src="https://latex.codecogs.com/png.latex?%5Cmathrm%7Bmesh%7D%20=%20%5B1,%201,%20%5Cldots,%201%5D"> with length 8760. There are cases where modelling time with a coarser time mesh is a good approximation, and there are multiple degrees of complexity associated with that task<sup>2</sup>.</p>
<p>I will also call “collapsing” the time mesh the process of reducing the number of time steps while maintaining the total duration. The reflections below are based on <a href="https://oecd-nea.github.io/Nosy.jl">Nosy</a>, which uses the power formalism<sup>3</sup>, under which power (or other flows) are linearly interpolated over the intervals. In particular, for a model under power formalism, the effect of mesh collapsing is quite transparent: a time step longer than one hour will simply imply that power will be linearly interpolated for the duration above one hour.</p>
<p>The first example of time mesh reduction is using a uniformly coarser time mesh. For instance, modelling 1 hour time steps, one can model 2 hours time steps: <img src="https://latex.codecogs.com/png.latex?%5Cmathrm%7Bmesh%7D%20=%20%5B2,%202,%20%5Cldots,%202%5D"> with length 4380. It may be a valid approximation, however the precision of some phenomena at the hourly scale will be degraded, such as variability of PV production, or ramping constraints.</p>
<p>A more involved example detailed in <a href="https://oecd-nea.github.io/Nosy.jl/dev/examples/heterogeneous-mesh/">Nosy’s docs</a> is using heterogeneous time meshes. The system is composed of PV, battery storage and consumption with a flat profile, on a one-year horizon; it performs greenfield capacity expansion and dispatch to match consumption. It is modelled under three time meshes: one is a fine hourly resolution over the full year, the second is a coarser two-hour resolution and the last one is heterogeneous and uses 18 hourly time steps during the day and 3 two-hour time steps at night. The results are in the table below.</p>
<p>The results show that the coarse time mesh returns different results and objective from the fine time mesh, but the heterogeneous time mesh returns the same results (within reported precision) as the fine mesh (same objective value, same capacity expansion for battery and PV). This result is interesting because the problem with heterogeneous time mesh has a 12.5% lower number of variables, directly resulting in a reduced solve time<sup>4</sup> (solve time based on using HiGHS solver).</p>
<table class="caption-top table">
<caption>Comparison of three time meshes for the PV-battery example. Units are unimportant except for solve time.</caption>
<colgroup>
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
</colgroup>
<thead>
<tr class="header">
<th style="text-align: left;">Mesh</th>
<th style="text-align: right;">PV capacity</th>
<th style="text-align: right;">Storage capacity</th>
<th style="text-align: right;">Objective value</th>
<th style="text-align: right;">Variables</th>
<th style="text-align: right;">Solve time (s)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Hourly</td>
<td style="text-align: right;">1.768</td>
<td style="text-align: right;">1.504</td>
<td style="text-align: right;">2.069</td>
<td style="text-align: right;">26,282</td>
<td style="text-align: right;">3.284</td>
</tr>
<tr class="even">
<td style="text-align: left;">2-hour</td>
<td style="text-align: right;">1.770</td>
<td style="text-align: right;">1.404</td>
<td style="text-align: right;">2.050</td>
<td style="text-align: right;">13,142</td>
<td style="text-align: right;">0.757</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Heterogeneous</td>
<td style="text-align: right;">1.768</td>
<td style="text-align: right;">1.504</td>
<td style="text-align: right;">2.069</td>
<td style="text-align: right;">22,997</td>
<td style="text-align: right;">2.451</td>
</tr>
</tbody>
</table>
<p>A plausible reason is that at night, the variation from one hour to the next of PV production is zero, therefore the system works in a static manner at night, the only exception being the battery storage level getting lower. Static behavior is compatible with using larger time steps at some hours without degrading the results.</p>
<p>The example above was a very clear case of when to use heterogeneous time meshes. However, all problems are not simple enough to contain periods with static behavior. In particular, when modelling country-scale power systems, consumption is not flat, even at night. However, a static behavior is not a required condition for the time collapsing to work.</p>
<p>One target is to find when the system is most stable. But that alone is generally not sufficient. Let’s assume that the problem is a cost-optimization. When you collapse hours, you also introduce bias, because complex systems rarely are purely static. And the bias you introduce is related to the marginal cost of operating the system at the time of collapsing, or more precisely, the shadow price. Intuitively, the reason is that the shadow price is the local derivative of your system’s objective against variation of consumption minus production. Should your approximation misrepresent the actual consumption or production, the bias generated will, in the vicinity of the optimal solution, be proportional to the shadow price.</p>
<p>Price can be used as an indicator for when to collapse time steps. When the price is stable, it hints at good stability, and when the absolute value of the price is low, it hints at low impact of bias. Obviously, having access to the shadow price generally implies that you have already solved a version of the problem. But there are many workflows where the solution is not one-shot, but incrementally refined by screening out unused technologies, removing relaxation etc. If you can afford to solve one initial LP relaxation of the MILP problem, you can derive the (shadow) price from it and use it to feed a time mesh collapsing process that will facilitate the next stages of the workflow, e.g.&nbsp;solving the MILP with the heterogeneous time mesh.</p>
<p>This is generally not a straightforward process though. One reason is that there are likely to be multiple nodes with shadow prices, so it is necessary to design an aggregation method that works for the case. Also, since these are two distinct targets (lowest prices and most stable prices), you will need to find the correct recipe, which also is problem-dependent.</p>
<p>A practical workflow is:</p>
<ol type="1">
<li>Solve an LP relaxation at full hourly resolution.</li>
<li>Extract nodal shadow prices and relevant exogenous profiles.</li>
<li>Aggregate prices across nodes if the model has several relevant shadow prices.</li>
<li>Identify candidate periods to collapse, for instance adjacent hours in the 20% quantile of most stable prices while price remains under a threshold.</li>
<li>Collapse only those intervals, with a maximum allowed step length.</li>
<li>Solve the MILP on the resulting heterogeneous mesh.</li>
</ol>
<p>Obviously validation of the results has to be part of the workflow too.</p>
<p>I will summarize this post by saying that the value of temporal resolution is uneven. Heterogeneous time meshes exploit that unevenness: they keep detail where the system is sensitive, and collapse periods where the approximation is unlikely to affect investment or dispatch decisions.</p>




<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Although many factors beyond variables influence the solve time of a MILP problem.↩︎</p></li>
<li id="fn2"><p>I will not detail the “time slices” practice, which is a different exercise where one horizon is divided into multiple non-contiguous representative sub-horizons.↩︎</p></li>
<li id="fn3"><p>As opposed to the “energy” formalism, where the basic quantity is the amount of energy flowing during a time step.↩︎</p></li>
<li id="fn4"><p>MILP solve time is noisy, so results may vary.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>energy systems</category>
  <category>MILP</category>
  <category>optimization</category>
  <guid>https://gkrivtchik.com/posts/heterogeneous_time_meshes/</guid>
  <pubDate>Sun, 14 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Architecting components in energy system models</title>
  <link>https://gkrivtchik.com/posts/designing_models/</link>
  <description><![CDATA[ 




<p>I’ve always worked in small teams. The direct consequence is that I’ve generally occupied multiple team functions at the same time, in particular model builder and model user.</p>
<p>A few years back, at the OECD Nuclear Energy Agency, I built <a href="https://git.oecd-nea.org/posy/posy">POSY</a><sup>1</sup>, a capacity expansion and dispatch energy systems model written in Julia. My most serious programming project before that was a discrete event simulation written in Java. However, as I was new to linear programming models, I was advised by colleagues at another institution to take a look at the programming language Julia, in particular because of its very strong operations research framework <a href="https://jump.dev/">JuMP</a>. In fact, two of our closest partners at the time were using Julia.</p>
<p>I was initially confused by Julia’s deliberate avoidance of class-style inheritance: abstract types cannot hold fields, concrete types cannot be subtyped, and much of what inheritance would usually do is handled through multiple dispatch<sup>2</sup>. I found an alternative to inheritance with traits<sup>3</sup>, even though their implementation felt slightly awkward to me.</p>
<p>Anyway, driven by my previous experiences, I started building POSY using a top-down approach. My target was to build a system where components interact, so I implemented each component type. I found some general patterns that helped me organize the components into families that component types would implement. For instance, the “storage” family would be used for both battery storage and large hydro reservoirs with natural intake. The “dispatchable” family would work for non-level-based plants whose output is an optimization variable, e.g.&nbsp;nuclear plants and gas plants.</p>
<p>At the beginning, that worked very well. I could rapidly prototype the first components and make them interact together in simple least-cost optimizations. So I decided to keep the model, improving it little by little when I needed a new component or a new constraint.</p>
<p>Unfortunately, I happened to need many more components, and I patched the oddities onto the edifice. Every family accumulated optional features until it stopped meaning one thing. For instance, the “storage” abstract type was initially designed for batteries, but soon enough I had to model large hydro reservoirs, so I added fields describing natural intake in addition to charging variables. Another example is that unit commitment became part of every dispatchable plant, but was quite often unused for tractability reasons. Some types became severely bloated, a substantial amount of the code was duplicated, and the model became difficult to maintain and expand without breaking things. My architecture was inadequate. In the end, the model did the job, the project was successful, but I was relieved that POSY would not have to grow any bigger.</p>
<p>Retrospectively, I believe that my mental model was suboptimal because it was overfitting my previous projects: the objects my models had to manipulate generally were more similar. I was used to dealing with complexity in algorithms, not heterogeneity of objects. Also, for some reason, “composition over inheritance” had never really clicked with me at that time.</p>
<p>A few years later, I needed to work again on a similar problem, except even more heterogeneous. In particular, the system I studied with POSY contained a single node (a single electricity market bidding zone, surrounded by neighbours modelled as interconnector capacity and price time series). This time, the system contained 4 nodes with capacity expansion, plus 10-15 other power nodes without capacity expansion, and just as many hydrogen nodes. After estimating the volume of work it would take to adapt POSY to this new problem, I made the decision to implement a different model. The new model would bear the same equations, but would have to be more flexible, more extendable.</p>
<p>Having worked for a few years with Julia, I decided to write the new model in a Julian way, by being composition-heavy. This time, I designed the model bottom-up: I wanted to isolate every little piece of business logic, and allow these pieces to interact at a small scale. This led me to design a new grammar for modelling components in energy systems (or any component-based commodity-flow graph, actually). The vocabulary of this grammar is: <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/#Model-Archetype"><code>model archetype</code></a>, <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/#Joint-Flows"><code>joint flow</code></a>, <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/#Behaviors"><code>behavior</code></a>, and <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/#Components"><code>component</code></a>.</p>
<p>Model archetypes are a small family of simple models, each having only one function. They create the “core” variables. For instance: dispatchable source (one output flow that is a series of independent variables), profile source (one output flow is one variable multiplied by a time series), basic storage (one input flow, one output flow, one level, all are a series of independent variables), basic converter (convert an input flow into an output flow), etc. There are about ten model archetypes so far, and I have not felt the need to add more after multiple full-scale studies with the model. What’s important is that they don’t do anything complex: they just do one thing. They don’t have intrinsic capacity, cost, unit commitment, or any non-trivial feature.</p>
<p>Joint flows constitute the second element of components. Joint flows are additional flows that can be:</p>
<ul>
<li>Linked to other component flows via functions, such as CO2 emissions by a gas plant proportional to the energy output;</li>
<li>Partly linked to other component flows, such as heat production by a CHP plant, that may be linked or not to the power output, depending on type;</li>
<li>Independent from other component flows, such as constant consumption of electricity by a system, independently of its use.</li>
</ul>
<p>Joint flows are useful because they make it easy to extend the interaction of a model. Their existence is one of the reasons why it was possible to keep the set of model archetypes small.</p>
<p>Model archetypes and their joint flows are complemented by behaviors. Behaviors are a family of mostly orthogonal features, which refine how the model operates. They do not add flows, but they may add constraints, auxiliary variables, etc. They will generally constrain how the flows behave. For instance, the “capacity” behaviors add a notion of capacity (fixed capacity generally bounds the flow, variable capacity ties it to a variable, etc.). <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.UnitCommitment"><code>UnitCommitment</code></a> is another example of behavior; it exists to constrain one of the flows of a model to obey the unit commitment rules (state, startup and shutdown, min downtime and uptime etc.). Another family of behaviors is costs, which add different types of cost to a model (fixed cost relative to capacity, variable cost relative to flows).</p>
<p>So, to summarize, a component is made of the three following items:</p>
<ul>
<li>Exactly one model archetype, which accomplishes a basic, essential, minimal function.</li>
<li>As many joint flows as necessary, which add more input or output flows to the component.</li>
<li>As many behaviors as necessary, which refine how the component works.</li>
</ul>
<p>As an example, one can define a gas plant fleet the following way, for a capacity expansion and dispatch study in a CO2-limited setting:</p>
<ul>
<li><a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.DispatchableSource"><code>DispatchableSource</code></a> model archetype, generating the variables for variable output power.</li>
<li><a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.LinkedJointFlow"><code>LinkedJointFlow</code></a> generating a CO2 joint flow, proportional to the power output.</li>
<li><a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.VariableCapacity"><code>VariableCapacity</code></a> behavior, which defines that the capacity of the gas plant can be optimized within boundaries.</li>
<li>If one needs accurate flexibility analysis, <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.UnitCommitment"><code>UnitCommitment</code></a> behavior will give state as well as startup / shutdown durations to the plant; and <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.Ramping"><code>Ramping</code></a> (up and down) will constrain the ramping of the free output flow.</li>
<li>Multiple <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.FixedCost"><code>FixedCost</code></a> behaviors: an annualized investment cost, an annualized decommissioning cost, and a fixed O&amp;M cost proportional to the capacity.</li>
<li>Multiple <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.VariableCost"><code>VariableCost</code></a> behaviors: a variable O&amp;M cost proportional to the output flow, a fuel cost proportional to the output flow, and one targeting the CO2 flow to represent the impact of CO2 price.</li>
<li>Optionally <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.ReserveUp"><code>ReserveUp</code></a> and <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.ReserveDown"><code>ReserveDown</code></a> to model contribution of the fleet to operational reserves.</li>
</ul>
<p>This grammar is very helpful because it provides a very high level of freedom in defining components, and does so in a very contained way. Choosing composition over inheritance allowed this parsimonious approach: the component owns just as many refinements as necessary.</p>
<p>All is not perfectly simple though. In addition to interacting with flows, behaviors may also interact with other behaviors. This is not without challenges, because adding one behavior requires assessing the interaction with all existing behaviors.</p>
<p>A simple example of behaviors interacting is that capacity-related costs (e.g.&nbsp;overnight cost) will look for a capacity behavior matching the requirements of the cost behavior. If there is none, the behavior cannot be applied and throws an error.</p>
<p>A more complex example is how <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.Ramping"><code>Ramping</code></a> behavior equations depend on whether the target component bears a <a href="https://oecd-nea.github.io/Nosy.jl/dev/api/#Nosy.UnitCommitment"><code>UnitCommitment</code></a> behavior connected to the ramping target flow. If not, ramping is applied to the flow directly. But if it does, ramping must be applied to the free-flow of the unit commitment, not the flow itself.</p>
<p>The implementation of these interactions is based on precedence rules. The instantiation of behaviors to a component follows a pre-established order that guarantees that any behavior possibly influencing another behavior is instantiated before the behavior that is influenced.</p>
<p>So far all the behaviors required for modelling power systems or industrial systems with usual requirements have been implemented and the interactions seem clean: no production bugs related to behavior handling have happened in a long time.</p>
<p>I think that the current scope of model archetype, joint flows and behaviors works well for Nosy. One question remaining, at least conceptually, is how far that concept of separation could have been pushed. In particular, I think that model archetypes now have the least clearly defined scope. I mentioned that a model archetype is something simple with exactly one essential function. It is likely that the current model still is a suboptimal level of abstraction: perhaps it could have been something less “physical”, and more mathematical, such as a set of variables, e.g.&nbsp;“vector of independent variables”, or “one variable multiplied with a numeric vector”, that one would then connect to an empty box either as an input or an output. And some features currently implemented as model archetypes, e.g.&nbsp;“having a level” (for storage), could be shifted to behaviors. In the future I might experiment with that thought, even though the current API of Nosy has the advantage of being relatively intuitive to modelers.</p>
<p>NB: for the sake of simplicity, I deliberately omitted the concept of <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/#Ports"><code>port</code></a> from the description of components. Ports are the way flows are emitted or targeted. For more information, please refer to <a href="https://oecd-nea.github.io/Nosy.jl/dev/concepts/building-snapshot/">Nosy’s documentation</a>.</p>




<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Soon to be replaced by <a href="https://github.com/oecd-nea/Nosy.jl">Nosy</a> (open-source) + POSY2 (to be published).↩︎</p></li>
<li id="fn2"><p>Multiple dispatch is actually an amazing concept, and super practical for writing composable code.↩︎</p></li>
<li id="fn3"><p>Traits are not native in Julia, I would qualify them as Julia-adjacent.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <category>energy systems</category>
  <category>architecture</category>
  <category>Julia</category>
  <guid>https://gkrivtchik.com/posts/designing_models/</guid>
  <pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Optimization and near-optimality</title>
  <link>https://gkrivtchik.com/posts/near_optimality/</link>
  <description><![CDATA[ 




<p>Much of engineering revolves around optimization. Start with a system, an object, a plan, and make it better - less expensive, more capable, more robust etc.</p>
<p>For most of my career, I’ve had to optimize. I’ve changed domains (energy systems, nuclear materials management, supply chain) and method (LP/MILP, multiobjective, metaheuristics with surrogate modelling) multiple times, but optimization has stayed a constant.</p>
<p>Some disciplines are even seemingly shaped by optimization. For instance, energy systems economics revolves around optimization - in general cost optimization. Capacity expansion analysis studies how to deploy and use capacity so as to reach the least-cost strategy.</p>
<p>Optimization is conceptually simple: you’re looking for the minimum of a function. In general, one looks for a combination of parameters that will lead to minimizing a function. But sometimes, one mistakenly interprets that as looking for “the best” combination of parameters.</p>
<p>Mathematically, this is a mistake because optimization does not generally guarantee a unique argmin unless the problem has specific uniqueness properties. There is no guarantee that you have one “best” minimizer - you may have infinitely many. If you look at the function <img src="https://latex.codecogs.com/png.latex?f(x,y)%20=%20%7Cx-y%7C">, you will immediately notice that all tuples <img src="https://latex.codecogs.com/png.latex?(x,x)"> will minimize the function.</p>
<p>This is an important point from a practical perspective too. Depending on the method you use, you may only obtain one result, that is an arbitrary minimizer, <img src="https://latex.codecogs.com/png.latex?(x,y)"> in our example – for instance (3,3). What the method said is that (3,3) minimizes the function. What one would incorrectly infer is that (3,3) is “the” (implicitly unique or significant) optimal tuple.</p>
<p>Some optimization methods tackle this point in particular. For instance, lexicographic optimization lets you define a ranked set of objectives <img src="https://latex.codecogs.com/png.latex?(f_1,%20f_2,%20...,%20f_n)">, and minimizes <img src="https://latex.codecogs.com/png.latex?f_1">, then minimizes <img src="https://latex.codecogs.com/png.latex?f_2"> among the minimizers of <img src="https://latex.codecogs.com/png.latex?f_1"> and so on. This method conceptually solves the problem of mathematically equivalent solutions - even though in practice making this claim is actually difficult, as you would need a full objective hierarchy to discriminate solution. In addition, to my knowledge, these methods generally don’t provide the full set of solutions at each step.</p>
<p>But stopping here would have us miss the more important point, which originates from the fact that systems modelling is full of bias and uncertainty. If you have two tuples of solutions to a problem, and one barely has an infinitesimally lower objective value than the other, should you legitimately exclude the worse solution?</p>
<p>There are many cases where your system has near-optimal solutions, with parameters quite different from the optimal solution. Depending on how you define near-optimality, a near-optimal solution is just as desirable as an optimal one. The existence of this near-optimal space is fundamental, because it characterizes choice: instead of just a tuple of optimal parameters, you may choose among a range of tuples.</p>
<p>Near-optimality is relative to your problem. Maybe you accept that 1% variation is good enough. Maybe the uncertainty of your input data is such that 2% variation on your objective value is well below the possible impact of uncertainty. Maybe you actively decide to trade some optimality for choice and flexibility.</p>
<p>In any case, the near-optimal set, with relative tolerance <img src="https://latex.codecogs.com/png.latex?%5Cepsilon">, is a valuable piece of information (for a strictly positive minimization objective):</p>
<p><img src="https://latex.codecogs.com/png.latex?%0AS_%5Cepsilon%20=%20%5C%7Bz%20%5Cin%20X%20:%20f(z)%20%5Cle%20(1+%5Cepsilon)%20f_%7B%5Cmin%7D%5C%7D%0A"></p>
<p>A quick algorithm I generally try when studying a system is to scan the range of the near-optimal set, one meaningful variable at a time. Let’s assume I’m interested in optimizing a function <img src="https://latex.codecogs.com/png.latex?f(x,y)">, but I also want to know if I have some leeway when choosing <img src="https://latex.codecogs.com/png.latex?y">, or if my system is quickly deoptimized when <img src="https://latex.codecogs.com/png.latex?y"> changes from its optimal solution value. First, I evaluate <img src="https://latex.codecogs.com/png.latex?%5Cmin%20f(x,y)"> and obtain <img src="https://latex.codecogs.com/png.latex?f_%7B%5Cmin%7D">. Then, I allow a tolerance <img src="https://latex.codecogs.com/png.latex?%5Cepsilon">, and run the following optimizations:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cbegin%7Baligned%7D%0A%5Cmin%20%5Cquad%20&amp;%20y%20%5C%5C%0A%5Ctext%7Bsubject%20to%7D%20%5Cquad%20&amp;%20(x,y)%20%5Cin%20X%20%5C%5C%0A&amp;%20f(x,y)%20%5Cleq%20(1+%5Cepsilon)%20f_%7B%5Cmin%7D%0A%5Cend%7Baligned%7D%0A"></p>
<p>and the same thing for max <img src="https://latex.codecogs.com/png.latex?y">. Interestingly, this snippet is actually quite close to what lexicographic algorithms actually do under the hood, but with a different philosophy.</p>
<p>In my experience, this is enlightening when applied to capacity expansion problems. When you solve your problem, you obtain what is commonly referred to as the “optimal” capacity – actually a minimizer capacity. But because of uncertainty and modelling assumptions, costs with one percent difference are indistinguishable to me. For example, overnight costs tend to be both impactful parameters and highly uncertain ones. So after I optimize the cost, I generally ask next “what is the range of technology x so that cost is near-optimal”. What I’ve seen is that if your system is behaving smoothly, with progressively more expensive flexibility resources (in particular: if you’re modelling a well-interconnected network), and technologies with different advantages (e.g.&nbsp;baseload for one, lower LCOE for the other), then the range of near-optimal capacity for a competitive technology can be wide.</p>
<p>Recently, I had to evaluate least-cost capacity mixes at country scale, through a capacity expansion study of multiple scenarios. I applied this technique to check the range of the near-cost-optimal deployment of two technologies (nuclear and onshore wind for that case). Then, I fixed the capacity of one technology at sampled values within its near-optimal range, and re-optimized the system. This gave me the matching capacity of the other technology. It turns out that there was a substantially wider range of near-optimal solutions than expected. What was also quite interesting is that the system’s behavior would shift from net exporter to net importer while shifting from one technology to the other - which also showed that both being an importer or an exporter could be cost-optimal strategies, which leaves room for choice.</p>
<p>The concept of “choice” matters here. This type of choice is not about applying a subjective personal preference. It is the way one can factor in additional information not present in the modelling effort. For instance, for strategic reasons, the country would prefer being an exporter than an importer. Or they have uncertainty regarding future energy consumption by industry, and are looking for a mix compatible with a rapidly rising demand. They can also make a no-regrets decision to invest now up to the lower bound for that technology’s capacity in the near-optimal set, and decide whether to invest more or not later.</p>
<p>All in all, the point is that optimization is often too small an object to support real-world decisions. In systems with uncertainty, modelling assumptions, and strategic constraints outside the objective function, the near-optimal set is often where the useful information lives. Optimization tells you the minimum value of your function and gives you one minimizer, but near-optimality tells you what choices remain.</p>



 ]]></description>
  <category>energy systems</category>
  <category>optimization</category>
  <guid>https://gkrivtchik.com/posts/near_optimality/</guid>
  <pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Simplifying a unit commitment problem with masks and state propagation</title>
  <link>https://gkrivtchik.com/posts/uc_masks/</link>
  <description><![CDATA[ 




<p>Unit commitment (UC) is a useful tool to model the behavior of almost any unit with states, in particular on and off. It is conceptually very simple: the state of a unit at time <img src="https://latex.codecogs.com/png.latex?t"> (in particular: on or off) depends on its previous state, and on whether it was turned on or turned off at the previous stage (time index may vary based on formalism):</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Astate_t%20=%20state_%7Bt-1%7D%20+%20startup_%7Bt-1%7D%20-%20shutdown_%7Bt-1%7D%0A"></p>
<p>And then, based on the state, you can derive the output or any operation that the unit is allowed to perform.</p>
<p>In addition to its conceptual simplicity, what is also great about it is that it is very modular at its core, and many small bits can be added based on what you need to model:</p>
<ul>
<li>Clustered unit commitment (CUC) models a fleet of units, using numbers instead of booleans.</li>
<li>Variable flow can be added, for instance when the unit has a minimum output level (e.g.&nbsp;minimum power is 20% of nominal power).</li>
<li>Startup and shutdown procedures can be added, by adding flows at <img src="https://latex.codecogs.com/png.latex?t+%5CDelta%20t"> that are proportional to <img src="https://latex.codecogs.com/png.latex?startup_%7Bt%7D"> and <img src="https://latex.codecogs.com/png.latex?shutdown_%7Bt%7D">.</li>
<li>Startup cost or flows can be added, again proportional to <img src="https://latex.codecogs.com/png.latex?t+%5CDelta%20t">.</li>
<li>You can model other states than on and off, e.g.&nbsp;by replacing <img src="https://latex.codecogs.com/png.latex?shutdown"> with a vector of different ways to <img src="https://latex.codecogs.com/png.latex?shutdown"> with <img src="https://latex.codecogs.com/png.latex?%5Csum%20ways%5C_to%5C_shutdown_t%20=%20shutdown_t">.</li>
<li>Minimum uptime and downtime can be enforced by constraining, at each timestep <img src="https://latex.codecogs.com/png.latex?t">, the switches that occurred during the preceding duration <img src="https://latex.codecogs.com/png.latex?L">: <img src="https://latex.codecogs.com/png.latex?%5Csum_%7B%5Ctau%20%5Cin%20%5Bt-L,%5C,t)%7D%20shutdown_%5Ctau%20%5Cle%20N%20-%20state_t%20+%20startup_t"> with <img src="https://latex.codecogs.com/png.latex?N"> as the number of units in the fleet and <img src="https://latex.codecogs.com/png.latex?L%20=%20min%20downtime%20+%20shutdown%5C_duration%20+%20startup%5C_duration"> (the formula is an approximation, the actual expression is a little too cumbersome to be presented here).</li>
</ul>
<p>Unfortunately, UC also tends to make the optimization process significantly more difficult for the solver.</p>
<p>First, it adds many variables: at least 3 per timestep as you need to express the state and the state switches, that is 26280 variables if you assume one year of 8760 hours. Some of the unit commitment flavors add many constraints to your problem (in particular lengthy minimum uptime/downtime in my experience). Also, it is common to model multiple technologies using the UC formalism in the same problem. And finally, when you use unit commitment, you’re generally doing it because you need some degree of accuracy, which correlates it with using integer constraints.</p>
<p>However, there are ways to simplify the problem.</p>
<p>Let’s take the example of modelling a nuclear plant. The example I’m taking is simplified to focus on the relevant points, but I will still emphasize some strong hypotheses:</p>
<ul>
<li>No ramping for load following (not all operators perform load following with nuclear, although it’s technically feasible on various plant types).</li>
<li>No short-duration shutdown for load-following purposes.</li>
<li>Minimum downtime is 30 days (720 hours) for refuelling. We assume that the operator can schedule refuelling optimally during the year.</li>
<li>Most large PWR reactors have to refuel every 1.5 years, that we interpret as “the sum of refuelling shutdowns over the year must be greater than or equal to 2/3 of the number of units”</li>
</ul>
<p>One of the methods I recently implemented in the energy systems modelling toolkit <a href="https://github.com/oecd-nea/Nosy.jl">Nosy</a> is to reduce the number of timesteps at which you declare UC variables or enforce UC constraints. It consists in applying masks to the startup and shutdown variable vectors so that we only allow starting or shutting down at specific timesteps. For instance: instead of allowing your plants to shut down for refuelling at any hour, they only are allowed to do so once a week e.g.&nbsp;the first hour of every week.</p>
<p>Why are you allowed to use masks? There are multiple answers, depending on the problem but also on the results. Maybe it makes no sense for the operator to optimize the refuelling schedule on a sub-weekly basis: once teams and materials are ready, delaying the operations seems economically detrimental. Maybe because analysis of the results shows that the solution you obtain is reasonably close to not using masks, which constitutes an a posteriori justification of your hypothesis. Maybe you don’t need a perfect solution, and you will use masks as a tool to make an in-house heuristic to get an acceptable solution quick enough. One last reason is that maybe this is not your “reference” run, just an intermediate step that you are using to warm-start your non-simplified problem. A low-cost high-quality initialization gives you a head start for your non-simplified reference run. Still, using masks shrinks the feasible region: you need to make sure you have a valid reason for using it.</p>
<p>Let’s assume that we make a shutdown mask vector <img src="https://latex.codecogs.com/png.latex?sdm">, length 8760 hours, so that <img src="https://latex.codecogs.com/png.latex?sdm_h%20=%20true"> for any <img src="https://latex.codecogs.com/png.latex?h"> multiple of 24*7 (shifted by one hour, so that <img src="https://latex.codecogs.com/png.latex?sdm_1"> is true, which will simplify our reasoning later), false otherwise. We only create shutdown variables when <img src="https://latex.codecogs.com/png.latex?sdm_h"> is true, meaning that we create 53 shutdown variables (h in {1, 169, …, 8737}) instead of 8760.</p>
<p>For the startup, it is slightly more complex. We assumed that the minimum downtime was 30*24=720 hours. We have to make sure that we can allow the reactors to start exactly 720 hours after shutdown. That gives a 720-hour shift of <img src="https://latex.codecogs.com/png.latex?sdm">. We can also give some more freedom e.g.&nbsp;add mid-week startup slots, but not in this simple case. All in all, we now have 48 startup variables (h in {721, 889, …, 8617}).</p>
<p>So we’ve managed to reduce the number of variables for unit commitment from 26280 difficult and generally integer variables to 8861 (8760 for state + 53 for shutdown + 48 for startup), which is already a substantial reduction. However, we still have to apply the most interesting part of the mask simplification: the state propagation.</p>
<p>Let’s assume that we don’t even know the number of units, because that could also be a variable. At the first hour, the state is variable <img src="https://latex.codecogs.com/png.latex?state_1">. We’ve made above the hypothesis that reactors are allowed to shut down at the first hour. Because of the equality <img src="https://latex.codecogs.com/png.latex?state_t%20=%20state_%7Bt-1%7D%20+%20startup_%7Bt-1%7D%20-%20shutdown_%7Bt-1%7D">, we don’t know the state at hour 2 before solving the problem, so let’s use variable <img src="https://latex.codecogs.com/png.latex?state_2"> to represent it. Now, the reactors aren’t allowed to start up or shut down at hour 2, because of the masks. This tells us that by definition, <img src="https://latex.codecogs.com/png.latex?state_3"> is equal to <img src="https://latex.codecogs.com/png.latex?state_2">. Instead of a variable + constraint duo, we can just express this by referencing <img src="https://latex.codecogs.com/png.latex?state_3"> as <img src="https://latex.codecogs.com/png.latex?state_2">, and never introduce a <img src="https://latex.codecogs.com/png.latex?state_3"> variable. Same thing for <img src="https://latex.codecogs.com/png.latex?state_4"> and so on, until you meet a timestep where reactors are either allowed to start up or shut down. If you assume that time is linear and not circular, that gives 102 different states (one plus as many as startup and shutdown variables, as they never switch at the same time), and the rest are simply propagated.</p>
<p>All in all, we’re now down to 203 variables (53+48+102) instead of 26280. We’ve also avoided many constraints. In particular, the extremely long expressions usually generated by the minimum downtime constraint have been substantially simplified, because many of the duplicate state switches have now transformed into just a coefficient before a small number of switches.</p>
<p>The implementation of the masks in Nosy is <a href="https://github.com/oecd-nea/Nosy.jl/blob/main/src/system/components/behaviors/regular/archetypes/commitment/fleet.jl">here</a>.</p>
<p>When not to use this? There are many situations where you will need more granularity, so this method must always be used with reasonable impact assessment. The weekly mask is a particularly aggressive presolving tactic, which works well for demonstration purposes but may be ill-suited for your everyday problem. You can tune this method to adapt it to the shape of your problem and make it less blind and extreme. For instance, your business knowledge may give you a good idea of when it’s interesting to start up or shut down units - maybe you’ve inferred this from a preliminary LP solve, maybe you’ve read it in the price time series, or maybe you have another idea of when it’s possible or impossible to simplify the behavior of your unit.</p>
<p>Using masks and state propagation generally is one of the first steps I make when I need to make a difficult unit-commitment problem more tractable. Depending on your problem, it can make the problem significantly easier. However, it is not a free approximation, and will likely require efforts in both business logic and validation.</p>



 ]]></description>
  <category>MILP</category>
  <category>unit commitment</category>
  <category>energy systems</category>
  <guid>https://gkrivtchik.com/posts/uc_masks/</guid>
  <pubDate>Mon, 01 Jun 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
