Improvement backlog¶
This document collects bugs, API additions, behavior changes, and code cleanups that were identified during reviews and while writing the documentation, many of them with the help of AI agents, but that are deliberately not implemented yet. Each entry names the owning class, a rough signature or the observed behavior, and the rationale, so it can be picked up as an independent piece of work later.
1. Promised in docs/design but missing (highest priority)¶
These are used or promised in the documentation today, so users following the docs hit errors.
Public equation removal and lookup on EquationRegistry¶
def unregister(self, name: str) -> None: ... # KeyError on unknown name
def get(self, name: str) -> Equation: ... # KeyError on unknown name
docs/user_guide/technology.md promises users can "remove or add equations", but removal and
single-equation lookup currently require the private _remove_equation /
_equations_by_name.
2. Custom registry plumbing¶
Technology.calculate_parameters(and theTechnologyCollectionmirror) should acceptequations: EquationRegistry | None = Nonelikecheck_consistencyalready does; today the default registry is hard-imported, so custom registries (documented indocs/user_guide/equations.md) cannot drive derivation.EquationRegistry.__contains__(name),__len__(),__iter__()andequations_for(parameter: str) -> list[Equation]. Besides user convenience, this would letTechnology.check_consistency/calculate_parametersstop reaching into the private_equations_by_parameterindex.
3. Parameter conveniences¶
__rmul__(and__neg__):2 * paramcurrently raisesTypeErrorwhileparam * 2works; scalar multiplication should be symmetric. (__radd__is only sensible for Parameter + Parameter, which already works — skip it.)- Ordering comparisons
__lt__/__le__/__gt__/__ge__via pint quantity comparison after unit harmonization, mirroring the compatibility checks inisclose(useful for screening, e.g.tech["lifetime"] > threshold). add_provenance(entry: str) -> Self: clean, supported way to append one history entry now thatprovenanceis alist[str].
4. Collection and technology conveniences¶
Technology.__delitem__would complete the mapping protocol (the documented way to drop a parameter isdel tech.parameters[name]).
5. Deferred code simplifications (behavior-preserving)¶
Cleanups identified in review but scoped out of the equations-focused simplification pass:
Parameter: a private_replace(**overrides)helper — six methods (to,to_currency,change_heating_value, the scalar branches of__truediv__/__mul__,__pow__) rebuildParameter(...)passing the same seven fields. Must go through the real constructor, notmodel_copy, to keep the pint canonicalization in__init__.Parameter.__add__/__sub__are identical except for the operator, and all four arithmetic operators repeat the provenance/note/sources merge block — factor into_additive_opand_merged_metadatahelpers.Parameter.change_heating_value: remove thehv_ratios = hv_ratiosno-op branch and the unreachableNotImplementedError(the first loop always fillshv_ratiosfor every carrier dimension), merge the two loops overdimensionality, and collapse thehv_unitsderivation. Separately worth deciding: anelse: raisefor a target heating value compatible with neither LHV nor HHV (currently silently behaves like HHV) — that is a behavior change.TechnologyCollection.get(): replace the five copy-pasted regex filter blocks with one loop over a{field: pattern}dict usinggetattr+str().TechnologyCollection.to_currency(): drop the unusedenumerateindex; use a list comprehension like the adjacentcalculate_parameters.TechnologyCollection.project(): bindself.technologies[0]once (used six times), and fix the'closest'NotImplementedErrormessage that is missing itsfprefix (the{param}placeholder is never interpolated).Technology.to_currency(): replace deep-copy-then-mutate with a dict comprehension +model_copy(update=...)likecalculate_parameters, andcountry = overwrite_country or self.region.
6. Issues found while writing the tutorials¶
Found while writing the Overview and the Tutorial; each was worked around in the docs.
Heating values block multiplication with carriers that have none¶
Parameter.__mul__ and __truediv__ require both operands to have the same heating value.
Electricity has none, so the most natural electrolyser calculation fails:
electricity = Parameter(magnitude=10, units="MW", carrier="el")
efficiency = Parameter(magnitude=0.65, units="MWh/MWh", carrier="H2/el", heating_value="LHV")
electricity * efficiency # ValueError: different heating values: None and lower_heating_value
Proposal: treat a missing heating value as compatible and let the result take the one that is
set. The rule for two set heating values also needs a decision: multiplying LHV by LHV
currently yields lower_heating_value ** 2. This is a design decision and a behavior change.
Wrong units for eac¶
calculate_parameters("eac") from specific_investment, wacc and lifetime returns
EUR_2020 / kilowatt instead of EUR_2020 / kilowatt / year. Parameters in exponent positions
are passed as plain magnitudes (see _get_exponent_symbols in equations.py), so the year
of lifetime is lost. The unit of the result has to be restored, e.g. by dividing by the unit
of the exponent parameter where the formula implies it, or by declaring the target unit per
equation.
Derived units are not simplified¶
A derived total_investment_cost from EUR_2020/kW and MW is reported as
EUR_2020 * megawatt / kilowatt. Results of equations (and possibly of __mul__ /
__truediv__) should be passed through pint's to_reduced_units().
Misleading error message for incompatible units¶
Parameter._check_parameter_compatibility compares currencies before dimensions, so any
mismatch where one side has a currency is reported as
different currencies or currency years: 'megawatt_hour' and 'EUR_2020 / kilowatt'.
Check dimensionality first, and only report a currency mismatch when both sides have
compatible dimensions.
Parameter names differ between datasets and equations¶
The equations identify parameters by name (specific_investment, total_investment_cost),
while the parsers keep the source's names (specific investment in dea_energy_storage,
investment in manual_input_usa). Equations can therefore not be applied to the bundled
datasets. Options: harmonise parameter keys in the parsers to the equation names (and document
it under "Naming conventions" in the fact sheets), or add an alias mapping to the registry.
TechnologyCollection.to_dataframe() is not a flat table¶
All parameters end up in one nested parameters column. A long format with one row per
technology and parameter (magnitude, units, carrier, heating_value) would make the
output usable for filtering and export.
DataPackage.to_json() docstring promises a schema that is not written¶
The docstring says both files are exported "together with the corresponding data schema", but
TechnologyCollection.to_json is called with output_schema=False. Either pass
output_schema=True or add a parameter and fix the docstring.
DataAccessor.download() writes into the installed package by default¶
Since data_path defaults to the package's parsers directory (so that load() works from any
working directory), download() writes there too. In a non-editable install this is inside
site-packages and may not be writable.
Considered and rejected¶
Parameter.__hash__: the class is mutable (pint attributes, provenance list), so hashing would be unsafe. Defining__eq__without__hash__(unhashable) is the correct state.TechnologyCollection.from_csv:to_csvserializes nested structures as strings, so a faithful round-trip needs real design work;from_jsoncovers loading.- A logging flag on
check_consistency(design.md's "warnings are logged" alternate flow): the returned status dict already carries the information; callers can log it as they see fit.