Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
56d586f
Move schema components out of tutorial database for SSOT
idelder Jun 29, 2026
591d261
Create a framework for extending model components
idelder Jun 29, 2026
16fe576
Move growth rate constraints to an extension
idelder Jul 6, 2026
1fbc8c8
Fix bugs for Bugs
idelder Jul 6, 2026
5ba582d
Add integer capacity extension
idelder Jul 7, 2026
0d1f4cf
Stop ignoring extensions in ruff
idelder Jul 8, 2026
45ff154
Bypass duals in highs because they dont work and raise errors for MIL…
idelder Jul 8, 2026
a0d0b93
Move index set loading to manifest
idelder Jul 8, 2026
dd9ba65
Tidy up for Bugs
idelder Jul 8, 2026
a015319
Build main extension components for economies of scale
idelder Jul 8, 2026
feaa99e
Enable output cost results for economies of scale extension
idelder Jul 8, 2026
f9c02f6
Create documentation for economies of scale extension
idelder Jul 8, 2026
82b73f4
Cleanup for Bugs
idelder Jul 8, 2026
0ce8cc0
Add economies of scale and discrete capacities to myopic capacities test
idelder Jul 9, 2026
b538041
Optimise O(n2) group tech-region lookups
idelder Jul 14, 2026
b70f1e1
Remove season ramp constraints and refactor
idelder Jul 14, 2026
5e448e7
Some miscellaneous refactoring/tidy up
idelder Jul 14, 2026
c797b1d
Refactor existing components in preparation for unit_commitment conne…
idelder Jul 22, 2026
1a0da45
Add unit commitment extension
idelder Jul 22, 2026
0d9ab4a
Add a test for unit_commitment
idelder Jul 22, 2026
b7418e9
Miscellaneous tidying for the rabbit
idelder Jul 22, 2026
d79e278
Update mediumville set and make empty sets more obvious for diagnosis
idelder Jul 22, 2026
2ec18c5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 22, 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
19 changes: 12 additions & 7 deletions docs/source/computational_implementation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ which determines the valid indices from other model components. This second meth
when the indices of a constraint do not exactly align with the indices of the database
table. For example, the database table :code:`ramp_up_hourly` is indexed by :code:`region`,
:code:`tech` in the database but one of its inheriting constraints,
:code:`ramp_up_day_constraint`, is indexed by :code:`region`, :code:`period`,
:code:`ramp_up_constraint`, is indexed by :code:`region`, :code:`period`,
:code:`season`, :code:`time_of_day`, :code:`tech`, :code:`vintage` in the model. In this case,
an index function is needed to determine the valid indices for the constraint.

Expand All @@ -277,19 +277,19 @@ an index function is needed to determine the valid indices for the constraint.
.. code-block:: python
:linenos:

self.ramp_up_day_constraint_rpsdtv = Set(
dimen=6, initialize=operations.ramp_up_day_constraint_indices
self.ramp_up_constraint_rpsdtv = Set(
dimen=6, initialize=operations.ramp_up_constraint_indices
)
self.ramp_up_day_constraint = Constraint(
self.ramp_up_day_constraint_rpsdtv, rule=operations.ramp_up_day_constraint
self.ramp_up__constraint = Constraint(
self.ramp_up_constraint_rpsdtv, rule=operations.ramp_up_constraint
)

.. topic:: in ``temoa/components/operations.py`` (return valid indices)

.. code-block:: python
:linenos:

def ramp_up_day_constraint_indices(
def ramp_up_constraint_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Season, TimeOfDay, Technology, Vintage]]:
indices = {
Expand All @@ -302,7 +302,7 @@ an index function is needed to determine the valid indices for the constraint.

return indices

With the sparse set (:code:`demand_constraint_rpc` or :code:`ramp_up_day_constraint_rpsdtv`)
With the sparse set (:code:`demand_constraint_rpc` or :code:`ramp_up_constraint_rpsdtv`)
created, we can now can use it in place of the sets specified in the non-sparse
implementation. Pyomo will now call the constraint implementation rule the
minimum number of times.
Expand All @@ -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
54 changes: 0 additions & 54 deletions docs/source/database_schema.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -275,33 +275,6 @@ limit_capacity_share {
TEXT notes
REAL share
}
limit_degrowth_capacity {
TEXT operator PK
TEXT region PK
TEXT tech_or_group PK
TEXT notes
REAL rate
REAL seed
TEXT seed_units
}
limit_degrowth_new_capacity {
TEXT operator PK
TEXT region PK
TEXT tech_or_group PK
TEXT notes
REAL rate
REAL seed
TEXT seed_units
}
limit_degrowth_new_capacity_delta {
TEXT operator PK
TEXT region PK
TEXT tech_or_group PK
TEXT notes
REAL rate
REAL seed
TEXT seed_units
}
limit_emission {
TEXT emis_comm PK
TEXT operator PK
Expand All @@ -311,33 +284,6 @@ limit_emission {
TEXT units
REAL value
}
limit_growth_capacity {
TEXT operator PK
TEXT region PK
TEXT tech_or_group PK
TEXT notes
REAL rate
REAL seed
TEXT seed_units
}
limit_growth_new_capacity {
TEXT operator PK
TEXT region PK
TEXT tech_or_group PK
TEXT notes
REAL rate
REAL seed
TEXT seed_units
}
limit_growth_new_capacity_delta {
TEXT operator PK
TEXT region PK
TEXT tech_or_group PK
TEXT notes
REAL rate
REAL seed
TEXT seed_units
}
limit_new_capacity {
TEXT operator PK
INTEGER period PK
Expand Down
249 changes: 249 additions & 0 deletions docs/source/extensions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
.. _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
extensions/unit_commitment
Loading
Loading