Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
62045a7
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Jun 15, 2026
403e4d3
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Jun 22, 2026
7c56696
Load early retirements from previous planning periods
idelder Jun 24, 2026
02663b5
Adjust existing capacity for past early retirement in myopic
idelder Jun 24, 2026
5c91331
Fix handling of existing capacity for p0
idelder Jun 24, 2026
1fee42f
Pull lifetimes data for previously retired existing capacities for ac…
idelder Jun 26, 2026
f8f55dd
Actually load growthrate constraints
idelder Jun 26, 2026
d3bcea4
Add a broad stress test for myopic that includes survival curves, ear…
idelder Jun 26, 2026
e36fed6
Update test set hashes
idelder Jun 26, 2026
a79fbb0
Improve custom loader filtering for lifetime data
idelder Jun 26, 2026
b8b7def
Update existing capacity check to specifically look for tiny dropped …
idelder Jun 26, 2026
49c82b3
Try to fix python 3.12 type error
idelder Jun 26, 2026
ae023f7
Check if output retirement table exists
idelder Jun 26, 2026
be2287b
Move schema components out of tutorial database for SSOT
idelder Jun 29, 2026
ddaf3cf
Create a framework for extending model components
idelder Jun 29, 2026
8da5857
Move growth rate constraints to an extension
idelder Jul 6, 2026
6dabfb1
Fix bugs for Bugs
idelder Jul 6, 2026
bdfa92b
Add integer capacity extension
idelder Jul 7, 2026
5f6fd64
Stop ignoring extensions in ruff
idelder Jul 8, 2026
34c7136
Bypass duals in highs because they dont work and raise errors for MIL…
idelder Jul 8, 2026
7ae32ba
Move index set loading to manifest
idelder Jul 8, 2026
9b4c0e9
Tidy up for Bugs
idelder Jul 8, 2026
66a39d3
Build main extension components for economies of scale
idelder Jul 8, 2026
de1086b
Enable output cost results for economies of scale extension
idelder Jul 8, 2026
b311769
Create documentation for economies of scale extension
idelder Jul 8, 2026
3123bf9
Cleanup for Bugs
idelder Jul 8, 2026
04f9a16
Add economies of scale and discrete capacities to myopic capacities test
idelder Jul 9, 2026
35e928b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.11.19
rev: 0.11.23
hooks:
# Dependency management
- id: uv-lock
Expand Down Expand Up @@ -47,7 +47,7 @@ repos:
# Python Linting & Formatting with Ruff
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: "v0.15.16"
rev: "v0.15.18"
hooks:
- id: ruff
name: ruff (linter)
Expand Down
5 changes: 5 additions & 0 deletions docs/source/computational_implementation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,11 @@ dimensionality of 3, and (following the :ref:`naming scheme
:code:`region`, the second an element of :code:`time_optimize`,
and third a demand commodity.

.. seealso::
The same component pattern (index set, parameter, and constraint declared in a
``model.py`` and implemented in component modules) can be packaged as an
optional, self-contained add-on. See :ref:`extensions` for the extension
framework and a copy-from template.



Expand Down
248 changes: 248 additions & 0 deletions docs/source/extensions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
.. _extensions:

Extension Framework
===================

Temoa includes a lightweight framework for adding **optional model components**
without modifying the core model. An extension can contribute its own database
tables, Pyomo components (sets, parameters, and constraints), and data-loading
rules. Extensions are declared once and then enabled per run through
configuration.

This page describes how the framework works and how to author a new extension.
A ready-to-copy scaffold lives at ``temoa/extensions/template``.

Overview
--------

Use an extension when you want to add modeling capability that is:

* **Optional** -- only active when explicitly enabled.
* **Self-contained** -- owns its own tables and model components.
* **Non-invasive** -- adds to the core model rather than editing it.

If a feature is fundamental to every Temoa run, it belongs in
``temoa/components`` (a core component) instead of an extension.

Each extension is described by a single :class:`ExtensionSpec` (declarative
metadata plus hook functions). The spec is registered with the framework, after
which users can enable the extension by id.

Lifecycle
---------

When a run is configured with ``extensions = ["..."]``, the framework threads the
enabled specs through configuration, model construction, and data loading:

.. code-block:: text

config: extensions = ["my_ext"]
|
v
resolve_extension_specs() validate ids -> ExtensionSpec list
|
v
TemoaModel(extensions=[...])
|
+--> apply_model_extension_hooks()
| calls spec.register_model_components(model)
| -> attaches Params / Sets / Constraints to the model
|
v
HybridLoader
+--> ensure_enabled_extension_tables_exist() (offer to append schema)
+--> assert_disabled_extension_tables_are_empty()
+--> merge_regional_group_tables()
+--> build_manifest() -> appends spec.build_manifest_items(model)
-> loads each owned table into its component

The relevant code lives in :mod:`temoa.extensions.framework`,
:mod:`temoa.core.model`, and :mod:`temoa.data_io.hybrid_loader`.

``ExtensionSpec`` reference
---------------------------

.. list-table::
:header-rows: 1
:widths: 30 70

* - Field
- Purpose
* - ``extension_id``
- Unique, lowercase id. This is what users put in ``extensions = [...]``.
* - ``owned_tables``
- Tuple of database tables owned exclusively by this extension. Used by the
disabled/enabled table guards.
* - ``regional_group_tables``
- Map of ``table -> column`` for tables whose region column may hold a
regional *group* name. Merged into the loader's regional-group handling.
* - ``register_model_components``
- Hook ``Callable[[TemoaModel], None]`` that attaches model components.
* - ``build_manifest_items``
- Hook ``Callable[[TemoaModel], list[LoadItem]]`` describing how to load the
extension's data.
* - ``schema_sql_path``
- Path to a ``.sql`` file applied (with consent) when the extension is
enabled but its tables are missing.
* - ``fail_if_tables_populated_when_disabled``
- When ``True``, loading fails if the extension is disabled but its owned
tables contain data, preventing silently-ignored inputs.

Recommended package layout
--------------------------

Mirror the structure of the core model (``temoa/core`` + ``temoa/components``) so
extension code is organized the same way as the rest of the codebase:

.. code-block:: text

temoa/extensions/<your_extension>/
__init__.py # re-export the ExtensionSpec
extension.py # the ExtensionSpec definition
data_manifest.py # build_manifest_items()
tables.sql # CREATE TABLE IF NOT EXISTS for owned tables
core/
__init__.py
model.py # typing subtype + register_model_components()
components/
__init__.py
<family>.py # one module per constraint family

Centralize the component *declarations* in ``core/model.py`` (just as
``temoa/core/model.py`` does) and keep the index-set and constraint-rule logic in
``components/`` modules (just as ``temoa/components`` does).

.. _extensions-typing:

The typing pattern
------------------

Core model components are declared as attribute assignments inside
``TemoaModel.__init__`` (for example ``self.time_optimize = Set(...)``), which is
why the type checker knows about them. An extension instead adds attributes from
*outside* the class, so without help the type checker reports
``"TemoaModel" has no attribute ...`` and provides no autocomplete.

Three rules make typing carry over cleanly:

1. **Declare a ``TYPE_CHECKING``-only subtype.** In ``core/model.py``, define a
subclass of ``TemoaModel`` that annotates every component the extension adds.
It inherits all core attributes, so component code sees both core and
extension members.

.. code-block:: python

if TYPE_CHECKING:
from temoa.core.model import TemoaModel

class ExampleModel(TemoaModel):
example_new_capacity_limit: Param
example_new_capacity_limit_constraint_rpt: Set
example_new_capacity_limit_constraint: Constraint

2. **Annotate component functions with the subtype.** Index-set and
constraint-rule functions take ``model: ExampleModel``. This restores both
mypy coverage and editor autocomplete.

3. **Keep spec hooks on the base type and ``cast`` internally.** Functions stored
on the ``ExtensionSpec`` (``register_model_components`` and
``build_manifest_items``) must keep ``model: TemoaModel`` to match the hook
callable types. Narrowing the parameter is a contravariance error. ``cast``
once at the top:

.. code-block:: python

def register_model_components(model: TemoaModel) -> None:
m = cast('ExampleModel', model)
m.example_new_capacity_limit = Param(...)

.. note::
Extension attribute names share the single ``TemoaModel`` namespace at
runtime. Keep them unique across extensions to avoid collisions.

Adding a new extension
----------------------

#. **Copy the template.** Duplicate ``temoa/extensions/template`` to
``temoa/extensions/<your_extension>/``.
#. **Rename ids and tables.** Update ``extension_id``, ``owned_tables``,
``regional_group_tables``, params, sets, and constraints to your domain.
#. **Declare components.** Add annotations to the typing subtype in
``core/model.py`` and create the components in ``register_model_components``.
#. **Write the constraint logic.** Add index-set and rule functions under
``components/`` and annotate them with your subtype.
#. **Describe data loading.** Add one ``LoadItem`` per owned table in
``data_manifest.py``.
#. **Define the schema.** Add a ``CREATE TABLE IF NOT EXISTS`` per owned table to
``tables.sql``.
#. **Register the spec.** Import your spec in
:func:`temoa.extensions.framework.get_known_extension_specs` and add it to the
``specs`` list. (Until you do this, the extension is inert -- enabling it
raises an "Unknown extension id" error.)
#. **Enable it.** Add the id to your configuration TOML:

.. code-block:: toml

extensions = ["<your_extension>"]

Data loading and ``LoadItem``
-----------------------------

``build_manifest_items`` returns one :class:`temoa.data_io.loader_manifest.LoadItem`
per database table the extension reads. Common fields:

.. list-table::
:header-rows: 1
:widths: 30 70

* - Field
- Purpose
* - ``component``
- The Pyomo ``Set`` or ``Param`` to populate.
* - ``table``
- Source table name in the database.
* - ``columns``
- Columns to select; for a ``Param`` the final column is the value.
* - ``index_length``
- Number of leading columns that form the index.
* - ``validator_name`` / ``validation_map``
- Source-trace validation: a viable-set name on the loader and which index
columns it applies to.
* - ``is_table_required``
- Set ``False`` for optional extension inputs so a missing table is not an
error.

Verification
------------

After authoring an extension, confirm:

#. **Types** -- ``mypy temoa/extensions/<your_extension>`` reports no issues.
#. **Imports** -- ``python -c "import temoa.extensions.<your_extension>.extension"``.
#. **Wiring** -- a model built with the extension enabled attaches the expected
components, and the test suite passes.

The template extension
-----------------------

``temoa/extensions/template`` is a complete, type-checked, but deliberately
**unregistered** scaffold. Because it is not listed in
``get_known_extension_specs``, it cannot be enabled until you register it, so it
never affects normal runs. Copy the folder as the starting point for a new
extension; every file carries ``# TEMPLATE:`` comments explaining what to change.

.. _extension-catalog:

Available extensions
--------------------

The extensions that ship with Temoa are documented on their own pages below.
Each page describes the extension's parameters and constraints. This list grows
as new extensions are added.

.. toctree::
:maxdepth: 1

extensions/growth_rates
extensions/discrete_capacity
extensions/eos
93 changes: 93 additions & 0 deletions docs/source/extensions/discrete_capacity.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
.. _extension-discrete-capacity:

Discrete Capacity
================================

The **discrete_capacity** extension adds optional constraints that force the
capacity of a technology or technology group to be deployed in discrete,
indivisible units rather than as a continuous quantity. This is useful for
modeling lumpy investments or retirements such as a power plant, reactor, or
other facility that can only exist in whole units of a fixed size.

Enabling the extension introduces integer decision variables, which turns the
model into a mixed-integer program (MIP). It is disabled by default and enabled
per run through configuration:

.. code-block:: toml

extensions = ["discrete_capacity"]

Its parameters live in the extension-owned tables of the same name and are loaded
only when the extension is enabled. See :ref:`extensions` for how the extension
framework wires these components into the model.

Parameters
----------

.. csv-table::
:header: "Parameter", "Database Table", "Model Element", "Notes"
:widths: 15, 20, 25, 40

":math:`\text{LDNC}_{r,t}`", ":code:`limit_discrete_new_capacity`", ":code:`limit_discrete_new_capacity`", "unit size for new capacity additions; new capacity is forced to an discrete multiple of this size; :code:`tech_or_group` column accepts a technology name or group name"
":math:`\text{LDC}_{r,t}`", ":code:`limit_discrete_capacity`", ":code:`limit_discrete_capacity`", "unit size for total available (retirement-adjusted) capacity; net capacity is forced to an integer multiple of this size; :code:`tech_or_group` column accepts a technology name or group name"


limit_discrete_new_capacity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:math:`{LDNC}_{r \in R, t \in T}`

The :code:`limit_discrete_new_capacity` parameter specifies the unit size for new
capacity additions of a technology (or group). The new capacity built in each
vintage is forced to be an integer multiple of this unit size. The
:code:`tech_or_group` column accepts a technology name or group name, and the
:code:`capacity` column gives the size of a single unit.


limit_discrete_capacity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:math:`{LDC}_{r \in R, t \in T}`

The :code:`limit_discrete_capacity` parameter specifies the unit size for the
total available (retirement-adjusted) capacity of a technology (or group). The
net capacity available in each period is forced to be an integer multiple of this
unit size. The :code:`tech_or_group` column accepts a technology name or group
name, and the :code:`capacity` column gives the size of a single unit.
Comment thread
idelder marked this conversation as resolved.


Decision Variables
------------------

Enabling the extension adds two integer decision variables, one per constraint.
Each counts the number of discrete units, which is multiplied by the
corresponding unit size to recover the capacity quantity.

v_discrete_new_capacity
~~~~~~~~~~~~~~~~~~~~~~~~

:math:`DNCAP_{r, t, v}`

The number of discrete units of new capacity built for technology (or group)
:math:`t` in region :math:`r` and vintage :math:`v`. It is restricted to
non-negative integers, and the total new capacity equals this count multiplied by
the unit size :math:`LDNC_{r, t}`.


v_discrete_capacity
~~~~~~~~~~~~~~~~~~~~~~~~

:math:`DCAP_{r, p, t}`

The number of discrete units of total available (retirement-adjusted) capacity
for technology (or group) :math:`t` in region :math:`r` and period :math:`p`. It
is restricted to non-negative integers, and the net available capacity equals this
count multiplied by the unit size :math:`LDC_{r, t}`.


Constraints
-----------

.. autofunction:: temoa.extensions.discrete_capacity.components.discrete_capacity.limit_discrete_new_capacity_constraint_rule

.. autofunction:: temoa.extensions.discrete_capacity.components.discrete_capacity.limit_discrete_capacity_constraint_rule
Loading
Loading