-
Notifications
You must be signed in to change notification settings - Fork 71
Add economies of scale extension #349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
idelder
wants to merge
28
commits into
TemoaProject:unstable
Choose a base branch
from
idelder:extension/economies_of_scale
base: unstable
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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] 403e4d3
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] 7c56696
Load early retirements from previous planning periods
idelder 02663b5
Adjust existing capacity for past early retirement in myopic
idelder 5c91331
Fix handling of existing capacity for p0
idelder 1fee42f
Pull lifetimes data for previously retired existing capacities for ac…
idelder f8f55dd
Actually load growthrate constraints
idelder d3bcea4
Add a broad stress test for myopic that includes survival curves, ear…
idelder e36fed6
Update test set hashes
idelder a79fbb0
Improve custom loader filtering for lifetime data
idelder b8b7def
Update existing capacity check to specifically look for tiny dropped …
idelder 49c82b3
Try to fix python 3.12 type error
idelder ae023f7
Check if output retirement table exists
idelder be2287b
Move schema components out of tutorial database for SSOT
idelder ddaf3cf
Create a framework for extending model components
idelder 8da5857
Move growth rate constraints to an extension
idelder 6dabfb1
Fix bugs for Bugs
idelder bdfa92b
Add integer capacity extension
idelder 5f6fd64
Stop ignoring extensions in ruff
idelder 34c7136
Bypass duals in highs because they dont work and raise errors for MIL…
idelder 7ae32ba
Move index set loading to manifest
idelder 9b4c0e9
Tidy up for Bugs
idelder 66a39d3
Build main extension components for economies of scale
idelder de1086b
Enable output cost results for economies of scale extension
idelder b311769
Create documentation for economies of scale extension
idelder 3123bf9
Cleanup for Bugs
idelder 04f9a16
Add economies of scale and discrete capacities to myopic capacities test
idelder 35e928b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
|
|
||
| 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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.