-
Notifications
You must be signed in to change notification settings - Fork 93
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
Rework math handling #639
base: main
Are you sure you want to change the base?
Rework math handling #639
Conversation
Tried to improve Ultimately I decided it was not worth the hassle, but this leads to unnecessary code ( |
@brynpickering requesting a preeliminary review of this, with some open questions: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few comments to help with the review. Let me know what you think!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed chunks of code thanks to the init
improvements in Model
.
expected_math.union(user_math, allow_override=True) | ||
flat = expected_math.as_dict_flat() | ||
assert all( | ||
model_math_w_mode_user.data.get_key(i) == flat[i] for i in flat.keys() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checks every single math combination!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice cleanup. Just a few comments:
- math documentation building isn't really a postprocessing step, it's a parallel process to building/solving the optimisation problem. Not sure where it should go, tbh.
- This is getting us one step closer to removing math from
calliope.Model
entirely, and I think I'm in favour of that. It just requires moving thevalidating_math_strings
method to somewhere more suitable. - I like being able to add a math dictionary and not just a reference to a math YAML. It's effectively equivalent to adding
scenarios
vsoverride_dict
atcalliope.Model
instatiation. Ideally we'd add this tocalliope.Model.build
and allow that dict to completely replace the math dict or to be added as another override. If the approach matchesscenarios
/override_dict
thenadd_math
list would be applied first, followed by themath_dict
. It just then needs a flag for ignoringmath/base.yaml
(/math/plan.yaml
). - Shall we use
CalliopeMath
as the class name? Many libraries seem to do this to avoid name clashes when using their lib as a dependency.
model_def_with_overrides["nodes"] = model_def_with_overrides["locations"] | ||
del model_def_with_overrides["locations"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
model_def_with_overrides["nodes"] = model_def_with_overrides["locations"] | |
del model_def_with_overrides["locations"] | |
model_def_with_overrides["nodes"] = model_def_with_overrides.pop("locations") |
def _combine_overrides(overrides: AttrDict, scenario_overrides: list): | ||
combined_override_dict = AttrDict() | ||
for override in scenario_overrides: | ||
try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to find try/except to be less readable. if override not in overrides
is much more explicit
src/calliope/model.py
Outdated
@@ -561,8 +526,8 @@ def validate_math_strings(self, math_dict: dict) -> None: | |||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it should move to the parsing module? It just needs a list of possible parameters passed to it so that it can use that in the parsing.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #639 +/- ##
=======================================
Coverage 95.97% 95.98%
=======================================
Files 26 29 +3
Lines 3980 4014 +34
Branches 836 771 -65
=======================================
+ Hits 3820 3853 +33
- Misses 70 72 +2
+ Partials 90 89 -1
|
- clustering had representative days that didn't represent themselves
@irm-codebase I've made a bunch of changes, partly following offline discussions between us.
|
@brynpickering fantastic! It's a lot of changes, so I'll review them once I'm back this Monday. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went through the changes. I'm quite happy with them!
I've added some suggestions to avoid bugs catched by mypy
, and to improve logic in some parts.
"objectives", | ||
]: | ||
self._add_all_inputs_as_parameters() | ||
if self.inputs.attrs["config"]["build"]["pre_validate_math_strings"]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice idea!
component = components.removesuffix("s") | ||
for name in self.math.data[components]: | ||
for name, dict_ in self.math.data[components].items(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General note: it'd be better if we used more descriptive names than dict_
.
It makes it a bit harder to know what is being fetched and why.
@@ -280,7 +279,7 @@ def _add_component( | |||
this name must be available in the input math provided on initialising the class. | |||
component_dict (Tp): unparsed YAML dictionary configuration. | |||
component_setter (Callable): function to combine evaluated xarray DataArrays into backend component objects. | |||
component_type (Literal["variables", "global_expressions", "constraints", "objectives"]): | |||
component_type (Literal["variables", "global_expressions", "constraints", "piecewise_constraints", "objectives"]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could change Literal...
to ORDERED_COMPONENTS_T
to make this smaller.
Users would see it though, so a more descriptive name (MATH_COMPONENTS_T
?) might be a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See proposal to move this to a TypedDict
in CalliopeMath
below.
@@ -491,7 +483,7 @@ def generate_math_doc( | |||
] | |||
if getattr(self, objtype).data_vars | |||
} | |||
if not components["parameters"]: | |||
if "parameters" in components and not components["parameters"]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential bug:
This will also eliminate parameters with False
, 0
and similar values.
We may want this due to sparsity, but I am not sure. If you only want to target None
, do if not None
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brynpickering do you still consider this not a postprocessing
feature after going through the changes?
should we change something here?
end_math_list = [] if add_math_dict is None else [add_math_dict] | ||
full_math_list = init_math_list + backend_config["add_math"] + end_math_list | ||
LOGGER.debug(f"Math preprocessing | Loading math: {full_math_list}") | ||
model_math = preprocess.CalliopeMath(full_math_list, self._def_path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "math mode salad" is quite hard to follow because of its "mixed value" nature.
init_math_list
will have either[]
or[str]
- the middle (
backend_config["add_math"]
) will have[]
[str]
end_math_list
(add_math_dict
) will have eitherNone
or[dict]
How about:
math_list = [backend_config.get("mode"), backend_config.get("add_math"), add_math_dict]
math_list = list(filter(bool, math_list)) # Removes False equivalents None, "", [], {}
model_math = preprocess.CalliopeMath(math_list, self._def_path)
self._init_from_dict(math_to_add) | ||
self.data: AttrDict = AttrDict( | ||
{ | ||
"variables": {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be TypedDict
above?
The backend also refers to through ORDERED_COMPONENTS_T
, so maybe the order should be defined here so that we have it in one tidy place.
|
||
ATTRS_TO_SAVE = ("history", "data") | ||
ATTRS_TO_LOAD = ("history",) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we not loading back the math data?
I was doing it because the file structure might have changed, meaning the "rebuild" would fail since the reference directories would be different...
def __repr__(self) -> str: | ||
"""Custom string representation of class.""" | ||
return f"""Calliope math definition dictionary with: | ||
{len(self.data["variables"])} decision variable(s) | ||
{len(self.data["global_expressions"])} global expression(s) | ||
{len(self.data["constraints"])} constraint(s) | ||
{len(self.data["piecewise_constraints"])} piecewise constraint(s) | ||
{len(self.data["objectives"])} objective(s) | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect tabs trigger a mypy
error.
def __repr__(self) -> str: | |
"""Custom string representation of class.""" | |
return f"""Calliope math definition dictionary with: | |
{len(self.data["variables"])} decision variable(s) | |
{len(self.data["global_expressions"])} global expression(s) | |
{len(self.data["constraints"])} constraint(s) | |
{len(self.data["piecewise_constraints"])} piecewise constraint(s) | |
{len(self.data["objectives"])} objective(s) | |
""" | |
def __repr__(self) -> str: | |
"""Custom string representation of class.""" | |
return f""" | |
Calliope math definition dictionary with: | |
{len(self.data["variables"])} decision variable(s) | |
{len(self.data["global_expressions"])} global expression(s) | |
{len(self.data["constraints"])} constraint(s) | |
{len(self.data["piecewise_constraints"])} piecewise constraint(s) | |
{len(self.data["objectives"])} objective(s) | |
""" |
@@ -788,7 +789,7 @@ def as_array(self) -> xr.DataArray: # noqa: D102, override | |||
evaluated = backend_interface._dataset[self.name] | |||
except KeyError: | |||
evaluated = xr.DataArray(self.name, attrs={"obj_type": "string"}) | |||
if "default" in evaluated.attrs: | |||
if "default" in evaluated.attrs and pd.notnull(evaluated.attrs["default"]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pd.notnull
here should change.
- Generally,
pd.isna
andpd.notna
is preferred overnull
(https://docs.astral.sh/ruff/rules/pandas-use-of-dot-not-null/) - This will generate odd behavior if there is a
list
in"default"
, since it will return an object and the check will pass if there is anything in it.
@brynpickering what's next for this feature? |
Partially fixes #608 #619
Summary of changes in this pull request
This PR aims to improve the way in which math is handled across a model init->build->solve->postprocess pipeline.
MathDocumentation
is now inpostprocess
.Model
no longer includes it (instead, it's an input to it).model._model_def_path
->model._def_path
: the processing of this was made clearer to remove unnecessary code during__init__
. As a result,load.py
is nowscenarios.py
, which is what it was really doing all along.ModelMath
introduced to handle math better. Thispreprocess
class contains the following:_model_data.attrs["applied_additional_math"]
was eliminated in favor ofmath.history
, which is also saved/read from netCDFs.model._model_data.attrs
no longer contains"math"
.base.yaml
->plan.yaml
. This is to avoid extra logic when checking math modes)... plus its clearer, imo.Additional discussion
Stuff that I could add to this PR very easily, with permission.
Removal of double
math
objectsRemoving
model.math
is very possible now, because all the logic needed to process files is contained inModelMath
. Users can still reach it if we declare a@property
that returnsmodel.backend.math.data
, or a warning if the backend has not been built / initialized.I have not done this because it needs discussing (e.g., should we partially initialise the backend during
init
?).This resulted in some additional attributes to the
ModelBackendGenerator
, which can be easily removed.Enabling no math ('clean') models
This PR also allows us to fulfill #606 very easily. I believe that changing
config.init.add_math::default::[]
toconfig.init.added_math::default::['plan']
is the most transparent way and the easiest to maintain.However, this might lead to some users accidentally omitting
plan
math if they add their own math... but this could hint at a documentation problem on our side.Another option is to add
config.init.default_math::default:true
, but it's not my favorite because it seems intransparent...Reviewer checklist