Skip to content
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

Clean and isolate model/backend attributes #635

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ddba9eb
Add cross-refs, defaults and unit data into markdown math docs
brynpickering Feb 8, 2024
8e9725e
Update how empty variable/expression array items are handled
brynpickering Feb 8, 2024
03218a3
Add parameters to math docs; add to refs if used in where string
brynpickering Feb 9, 2024
6b9fec4
Fixes for tests
brynpickering Feb 9, 2024
d79916c
Fix logging order
brynpickering Feb 9, 2024
722ae01
Increase coverage
brynpickering Feb 9, 2024
dcef6ae
Merge branch 'main' into update-math-docs
brynpickering Feb 9, 2024
5fdee2f
Post merge fixes; fix math string in schema string
brynpickering Feb 9, 2024
70b516e
Update parameter list in storage_inter_cluster docs
brynpickering Feb 9, 2024
401ecf2
Move guide to math docs; remove noqa F811
brynpickering Feb 12, 2024
4870f94
Add gurobi backend interface
brynpickering Jan 26, 2024
fd3b738
More tests and associated core fixes
brynpickering Jan 26, 2024
853f4d2
Attempt to increase constraint setting efficiency
brynpickering Feb 12, 2024
a709d1b
Update elementwise function calls
brynpickering Feb 12, 2024
4ff46d0
remove use_inf_as_na; gurobipy tests in CI; add sense check
brynpickering Feb 12, 2024
e391438
Merge branch 'main' into feature-gurobi-interface
brynpickering Feb 13, 2024
81a1167
Minor fix
brynpickering Feb 14, 2024
6e6d597
Update gurobi result processing
brynpickering Feb 14, 2024
d473e74
Update to universal use of np.frompyfunc
brynpickering Feb 15, 2024
6a15c3a
Merge branch 'main' into feature-gurobi-interface
brynpickering Feb 15, 2024
50f57e1
Test fixes
brynpickering Feb 15, 2024
45cd3e8
Update coverage
brynpickering Feb 19, 2024
6ff5c23
Update test to work on windows
brynpickering Feb 19, 2024
73a986b
Increase coverage; minor fix
brynpickering Feb 19, 2024
2a7b5f1
Update documentation and changelog
brynpickering Feb 19, 2024
b372250
Fix dead links
brynpickering Feb 19, 2024
6bc72b0
Clean up markdown whitespace
brynpickering Mar 18, 2024
b4d93f7
Merge branch 'main' into feature-gurobi-interface
brynpickering May 13, 2024
1be6ac5
Post-merge fixes
brynpickering May 13, 2024
39099ed
Pin pyomo
brynpickering May 14, 2024
859613a
Merge branch 'main' into feature-gurobi-interface
brynpickering Jul 1, 2024
fdfd60e
Move cross-backend tests to one place; respond to new ruff config
brynpickering Jul 1, 2024
857634d
Merge branch 'main' into feature-gurobi-interface
brynpickering Jul 2, 2024
7cd08ae
Response to review
brynpickering Jul 5, 2024
59c0ac5
Merge branch 'main' into feature-gurobi-interface
brynpickering Jul 5, 2024
ffc0429
Post-merge fixes
brynpickering Jul 5, 2024
c85e8a4
Revert installation instruction
brynpickering Jul 5, 2024
c127c4e
clean and
irm-codebase Jul 9, 2024
bf4c2fd
rework ._timings and .attrs[timestamps] into model.timestamps
irm-codebase Jul 9, 2024
b2431ca
Improved attribute to netCDF method. Extracted math mode function fro…
irm-codebase Jul 9, 2024
7492ac4
Merge remote-tracking branch 'origin/main' into rework-config
irm-codebase Jul 10, 2024
3b66648
fix rebase issue
irm-codebase Jul 10, 2024
fb404e5
remove model._model_def_dict
irm-codebase Jul 10, 2024
6169e68
_model_def_path -> _def_path
irm-codebase Jul 10, 2024
0dab725
Avoid duplication of model.math and isolate it from xarray object
irm-codebase Jul 10, 2024
e39c1f0
Removed model.math_documentation, is now postprocessing module
irm-codebase Jul 10, 2024
55d3cad
fix 'clusters' dimension disappearing from Latex backend
irm-codebase Jul 11, 2024
5bcd083
avoid model.name duplication
irm-codebase Jul 14, 2024
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ This reduces memory footprint of parameter arrays.

|changed| Moved from black formatting to the Ruff formatter (black-based, but faster).

|fixed| Removed duplicated attribute: `model._timings` | `model._model_data.attrs["timestamps"]` -> `model.timestamps`

## 0.7.0.dev3 (2024-02-14)

### User-facing changes
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced/backend_choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ If you have access to a Gurobi license, this does not require any extra effort o
1. Install the Gurobi Python library into your Calliope environment: `mamba install gurobi::gurobi`.
1. Select the Gurobi backend in your YAML configuration (`!#yaml config.build.backend: gurobi`) or at build time if running in a Python script or interactively (`!#python model.build(backend="gurobi")`).

You can still [interface with your optimisation problem](backend_interface.md), but some methods will raise an exception when the Gurobi Python API does not allow for something that the Pyomo API does.
You can still [interface with your optimisation problem](backend_interface.md), but some methods will raise an exception when the Gurobi Python API does not allow for something that the Pyomo API does.
27 changes: 9 additions & 18 deletions docs/examples/calliope_model_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,41 +37,32 @@
print(m.info())

# %% [markdown]
# ## Model definition dictionary
#
# `m._model_def_dict` is a python dictionary that holds all the data from the model definition YAML files, restructured into one dictionary.
#
# The underscore before the method indicates that it defaults to being hidden (i.e. you wouldn't see it by trying a tab auto-complete and it isn't documented)

# %%
m._model_def_dict.keys()

# %% [markdown]
# `techs` hold only the information about a technology that is specific to that node
# `techs` parameters can be accessed by selecting a specific technology.
# Note how only the relevant carrier holds information for this parameter.

# %%
m._model_def_dict["techs"]["pv"]
m.inputs.sel(techs="pv")["flow_cap_max"]

# %% [markdown]
# `nodes` hold only the information about a technology that is specific to that node
# `nodes` parameters can be accessed in a similar way, and you can even combine both for specific searches:

# %%
m._model_def_dict["nodes"]["X2"]["techs"]["pv"]
m.inputs.sel(techs="pv", nodes="X2")["flow_cap_max"]

# %% [markdown]
# ## Model data
#
# `m._model_data` is an xarray Dataset.
# Like `_model_def_dict` it is a hidden prperty of the Model as you are expected to access the data via the public property `inputs`
# `m._model_data` is an xarray Dataset containing the full Model data, and is a hidden property.
# You are expected to access the data via the public property `inputs`

# %%
m.inputs
m._model_data

# %% [markdown]
# Until we solve the model, `inputs` is the same as `_model_data`

# %%
m._model_data
m.inputs

# %% [markdown]
# We can find the same PV `flow_cap_max` data as seen in `m._model_run`
Expand Down
73 changes: 38 additions & 35 deletions docs/hooks/generate_math_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pathlib import Path

import calliope
from calliope.postprocess.math_documentation import MathDocumentation
from mkdocs.structure.files import File

logger = logging.getLogger("mkdocs")
Expand Down Expand Up @@ -42,7 +43,7 @@ def on_files(files: list, config: dict, **kwargs):
"""Process documentation for pre-defined calliope math files."""
model_config = calliope.AttrDict.from_yaml(MODEL_PATH)

base_model = generate_base_math_model()
base_documentation = generate_base_math_documentation()
write_file(
"base.yaml",
textwrap.dedent(
Expand All @@ -51,22 +52,24 @@ def on_files(files: list, config: dict, **kwargs):
This math is _always_ applied but can be overridden with pre-defined additional math or [your own math][adding-your-own-math-to-a-model].
"""
),
base_model,
base_documentation,
files,
config,
)

for override in model_config["overrides"].keys():
custom_model = generate_custom_math_model(base_model, override)
custom_documentation = generate_custom_math_documentation(
base_documentation, override
)
write_file(
f"{override}.yaml",
textwrap.dedent(
f"""
Pre-defined additional math to apply {custom_model.inputs.attrs['name']} math on top of the [base mathematical formulation][base-math].
Pre-defined additional math to apply {custom_documentation.name} math on top of the [base mathematical formulation][base-math].
This math is _only_ applied if referenced in the `config.init.add_math` list as `{override}`.
"""
),
custom_model,
custom_documentation,
files,
config,
)
Expand All @@ -77,7 +80,7 @@ def on_files(files: list, config: dict, **kwargs):
def write_file(
filename: str,
description: str,
model: calliope.Model,
math_documentation: MathDocumentation,
files: list[File],
config: dict,
) -> None:
Expand All @@ -86,12 +89,10 @@ def write_file(
Args:
filename (str): name of produced `.md` file.
description (str): first paragraph after title.
model (calliope.Model): calliope model with the given math.
math_documentation (MathDocumentation): calliope math documentation.
files (list[File]): math files to parse.
config (dict): documentation configuration.
"""
title = model.inputs.attrs["name"] + " math"

output_file = (Path("math") / filename).with_suffix(".md")
output_full_filepath = Path(TEMPDIR.name) / output_file
output_full_filepath.parent.mkdir(exist_ok=True, parents=True)
Expand Down Expand Up @@ -122,7 +123,9 @@ def write_file(

nav_reference["Pre-defined math"].append(output_file.as_posix())

math_doc = model.math_documentation.write(format="md", mkdocs_tabbed=True)
md_doc = math_documentation.write(format="md", mkdocs_tabbed=True)

title = math_documentation.name
file_to_download = Path("..") / filename
output_full_filepath.write_text(
PREPEND_SNIPPET.format(
Expand All @@ -131,69 +134,69 @@ def write_file(
math_type=title.lower(),
filepath=file_to_download,
)
+ math_doc
+ md_doc
)


def generate_base_math_model() -> calliope.Model:
"""Generate model with documentation for the base math.

Args:
model_config (dict): Calliope model config.
def generate_base_math_documentation() -> MathDocumentation:
"""Generate model documentation for the base math.

Returns:
calliope.Model: Base math model to use in generating math docs.
MathDocumentation: model math documentation with latex backend.
"""
model = calliope.Model(model_definition=MODEL_PATH)
model.math_documentation.build()
return model
return MathDocumentation(model)


def generate_custom_math_model(
base_model: calliope.Model, override: str
) -> calliope.Model:
"""Generate model with documentation for a pre-defined math file.
def generate_custom_math_documentation(
base_documentation: MathDocumentation, override: str
) -> MathDocumentation:
"""Generate model documentation for a pre-defined math file.

Only the changes made relative to the base math will be shown.

Args:
base_model (calliope.Model): Calliope model with only the base math applied.
base_documentation (MathDocumentation): model documentation with only the base math applied.
override (str): Name of override to load from the list available in the model config.

Returns:
MathDocumentation: model math documentation with latex backend.
"""
model = calliope.Model(model_definition=MODEL_PATH, scenario=override)

full_del = []
expr_del = []
for component_group, component_group_dict in model.math.items():
for name, component_dict in component_group_dict.items():
if name in base_model.math[component_group]:
if name in base_documentation.math[component_group]:
if not component_dict.get("active", True):
expr_del.append(name)
component_dict["description"] = "|REMOVED|"
component_dict["active"] = True
elif base_model.math[component_group].get(name, {}) != component_dict:
elif (
base_documentation.math[component_group].get(name, {})
!= component_dict
):
_add_to_description(component_dict, "|UPDATED|")
else:
full_del.append(name)
else:
_add_to_description(component_dict, "|NEW|")

model.math_documentation.build()
math_documentation = MathDocumentation(model)
for key in expr_del:
model.math_documentation._instance._dataset[key].attrs["math_string"] = ""
math_documentation.backend._dataset[key].attrs["math_string"] = ""
for key in full_del:
del model.math_documentation._instance._dataset[key]
for var in model.math_documentation._instance._dataset.values():
del math_documentation.backend._dataset[key]
for var in math_documentation.backend._dataset.values():
var.attrs["references"] = var.attrs["references"].intersection(
model.math_documentation._instance._dataset.keys()
math_documentation.backend._dataset.keys()
)
var.attrs["references"] = var.attrs["references"].difference(expr_del)

logger.info(
model.math_documentation._instance._dataset["carrier_in"].attrs["references"]
)
logger.info(math_documentation.backend._dataset["carrier_in"].attrs["references"])

return model
return math_documentation


def _add_to_description(component_dict: dict, update_string: str) -> None:
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ plugins:
- https://docs.python.org/3/objects.inv
- https://pandas.pydata.org/docs/objects.inv
- https://docs.xarray.dev/en/stable/objects.inv
- https://pyomo.readthedocs.io/en/stable/objects.inv
markdown_extensions:
- admonition
- attr_list
Expand Down
21 changes: 7 additions & 14 deletions src/calliope/attrdict.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,11 @@ def set_key(self, key, value):
# want to overwrite so stop and warn the user
else:
raise KeyError("Cannot set nested key on non-dict key.")
elif key in self and isinstance(value, AttrDict):
for k, v in value.items():
self[key].set_key(k, v)
else:
if key in self and isinstance(value, AttrDict):
for k, v in value.items():
self[key].set_key(k, v)
else:
self[key] = value
self[key] = value

def get_key(self, key, default=_MISSING):
"""Looks up the given ``key``.
Expand All @@ -258,12 +257,10 @@ def get_key(self, key, default=_MISSING):
return default
else:
value = self[key].get_key(remainder)
elif default != _MISSING:
return self.get(key, default)
else:
# Single, non-nested key of form "foo"
if default != _MISSING:
return self.get(key, default)
else:
return self[key]
return self[key]
return value

def del_key(self, key):
Expand Down Expand Up @@ -374,7 +371,6 @@ def union(
other: Self | dict,
allow_override: bool = False,
allow_replacement: bool = False,
allow_subdict_override_with_none: bool = False,
):
"""In-place merge with another AttrDict.

Expand All @@ -384,9 +380,6 @@ def union(
already defined keys. Defaults to False.
allow_replacement (bool, optional): allow "_REPLACE_" key to replace an
entire sub-dict. Defaults to False.
allow_subdict_override_with_none (bool, optional): if False, keys in the
form `this.that: None` in `other` will be ignored if subdicts exist in
self like `this.that.foo: 1`, rather than wiping them. Defaults to False.

Raises:
KeyError: `other` has an already defined key and `allow_override == False`
Expand Down
16 changes: 10 additions & 6 deletions src/calliope/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@

import xarray as xr

from calliope.backend.backend_model import BackendSetup
from calliope.backend.gurobi_backend_model import GurobiBackendModel
from calliope.backend.latex_backend_model import MathDocumentation
from calliope.backend.latex_backend_model import (
ALLOWED_MATH_FILE_FORMATS,
LatexBackendModel,
)
from calliope.backend.parsing import ParsedBackendComponent
from calliope.backend.pyomo_backend_model import PyomoBackendModel
from calliope.exceptions import BackendError

MODEL_BACKENDS = ("pyomo",)
MODEL_BACKENDS = ("pyomo", "gurobi")

if TYPE_CHECKING:
from calliope.backend.backend_model import BackendModel


def get_model_backend(name: str, data: xr.Dataset, **kwargs) -> "BackendModel":
def get_model_backend(name: str, setup: BackendSetup, **kwargs) -> "BackendModel":
"""Assign a backend using the given configuration.

Args:
name (str): name of the backend to use.
data (Dataset): model data for the backend.
setup (BackendSetup): standard backend inputs.
**kwargs: backend keyword arguments corresponding to model.config.build.

Raises:
Expand All @@ -32,8 +36,8 @@ def get_model_backend(name: str, data: xr.Dataset, **kwargs) -> "BackendModel":
"""
match name:
case "pyomo":
return PyomoBackendModel(data, **kwargs)
return PyomoBackendModel(setup, **kwargs)
case "gurobi":
return GurobiBackendModel(data, **kwargs)
return GurobiBackendModel(setup, **kwargs)
case _:
raise BackendError(f"Incorrect backend '{name}' requested.")
Loading