Skip to content

[Option Appraisal Module] [Package 1: Measures] Split 1: Adds MeasureConfig dataclasses for serializable measure configuration.#1276

Open
spjuhel wants to merge 26 commits intodevelopfrom
feature/option-appraisal-dataclasses
Open

[Option Appraisal Module] [Package 1: Measures] Split 1: Adds MeasureConfig dataclasses for serializable measure configuration.#1276
spjuhel wants to merge 26 commits intodevelopfrom
feature/option-appraisal-dataclasses

Conversation

@spjuhel
Copy link
Copy Markdown
Collaborator

@spjuhel spjuhel commented Apr 7, 2026

Changes proposed in this PR:

This PR introduces a new module defining a family of dataclasses that encode adaptation measure parameters in a serializable, declarative form. This is a preparatory step for the broader Measure/MeasureSet rework.

The objective is to retain the possibility to define measures from modifiers in addition to the upcoming system that will use python functions directly, and provide a (better) user interface for this.

no existing code is changed.

New dataclasses:

  • MeasureConfig top-level configuration for a single measure, aggregating all modifier configs below.
  • ImpfsetModifierConfig parameters for modifying impact functions (intensity, MDD, PAA scaling, optional replacement)
  • HazardModifierConfig parameters for modifying hazard (intensity scaling, frequency cutoff, optional replacement)
  • ExposuresModifierConfig parameters for modifying exposures (impact function remapping, zeroing regions, optional replacement)
  • CostIncomeConfig serializable representation of cost/income cash flow parameters

All configs inherit from the abstract _ModifierConfig base, which provides shared to_dict/from_dict and a __repr__ that highlights non-default fields.

MeasureConfig supports full serialization to/from dict, YAML, and legacy Excel rows (from_row), which will serve as the migration path for the existing file-based I/O in MeasureSet.

PR Author Checklist

PR Reviewer Checklist

Comment thread climada/entity/measures/measure_config.py Outdated
Comment thread climada/entity/measures/measure_config.py
@chahank
Copy link
Copy Markdown
Member

chahank commented Apr 7, 2026

Excellent idea!

spjuhel added 7 commits April 7, 2026 10:22
Cleans-up, Docstringfies

Better to_dict, color parser, and post_inits

Removes duplicate docstring
…MADA-project/climada_python into feature/option-appraisal-dataclasses
@spjuhel spjuhel changed the title [Option Appraisal Module] - Adds MeasureConfig dataclasses for serializable measure configuration. [Option Appraisal Module] [Package 1: Measures] Split 1: Adds MeasureConfig dataclasses for serializable measure configuration. Apr 7, 2026
@spjuhel
Copy link
Copy Markdown
Collaborator Author

spjuhel commented Apr 8, 2026

@chahank

Question: Should measures (defined by MeasureConfig) be specific to a hazard? (I see reasons for both yes and no)

Currently, they are specific (thus configs require you to provide haz_type).

I see how you could want modifications of impact function to be for multiple hazards
But also you should probably not be able to apply a shift in intensity for multiple hazards?

Any opinion on that?

@spjuhel spjuhel marked this pull request as ready for review April 8, 2026 14:04
@spjuhel
Copy link
Copy Markdown
Collaborator Author

spjuhel commented Apr 8, 2026

This PR is officially open for review! Review is advised after #1274 is merged.

Actual changes to be reviewed are in:

  • climada/entity/measures/measure_config.py
  • climada/entity/measures/test/test_measure_config.py
  • doc/user-guide/climada_measure_config.ipynb (and a few other tutos where a target was added at the top)

@chahank
Copy link
Copy Markdown
Member

chahank commented Apr 9, 2026

@chahank

Question: Should measures (defined by MeasureConfig) be specific to a hazard? (I see reasons for both yes and no)

Currently, they are specific (thus configs require you to provide haz_type).

I see how you could want modifications of impact function to be for multiple hazards But also you should probably not be able to apply a shift in intensity for multiple hazards?

Any opinion on that?

Good question. I would then to keep it hazard type specific. But maybe best is to ask core users of the model :).
@ChrisFairless : any opinion?

Copy link
Copy Markdown
Member

@peanutfun peanutfun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed mostly on a technical level. Very nice code quality overall, nice class structure! I found a few issues. Sorry for the brevity 😬 ✌️

Comment on lines +67 to +68
if defined_field.default_factory is not field().default_factory:
default = defined_field.default_factory()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs from dataclasses import MISSING:

Suggested change
if defined_field.default_factory is not field().default_factory:
default = defined_field.default_factory()
if default is MISSING:
default = defined_field.default_factory()

See https://docs.python.org/3/library/dataclasses.html#dataclasses.MISSING and https://docs.python.org/3/library/dataclasses.html#dataclasses.Field:

default, default_factory, init, repr, hash, compare, metadata, and kw_only have the identical meaning and values as they do in the field() function.

be instantiated directly.
"""

def _filter_out_default_fields(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def _filter_out_default_fields(self):
def _filter_out_default_fields(self) -> dict[str, Any]:



@dataclass
class _ModifierConfig(ABC):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do not define abstract methods, so you can omit deriving from ABC:

Suggested change
class _ModifierConfig(ABC):
class _ModifierConfig:

Also, why private? Just exclude from all?

non_defaults.pop("haz_type")
return non_defaults, defaults

def to_dict(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Judging from its name, I would not expect such a method to omit default values. Even if this is never used, maybe add a parameter?

Suggested change
def to_dict(self):
def to_dict(self, omit_default : bool = True) -> dict[str, Any]:

Comment on lines +132 to +135
filtered = dict(
filter(lambda k: k[0] in [f.name for f in fields(cls)], to_filter.items())
)
return filtered
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dict comprehension is easier to read. Also, I am not sure if the list comprehension is evaluated once or each time the lambda runs.

Suggested change
filtered = dict(
filter(lambda k: k[0] in [f.name for f in fields(cls)], to_filter.items())
)
return filtered
fields = [f.name for f in fields(cls)]
return {key: val for key, val in to_filter.items() if key in fields}

Comment on lines +375 to +377
freq : str, optional
Pandas offset alias defining the period length (e.g. ``"Y"`` for
yearly, ``"M"`` for monthly). Default is ``"Y"``.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure exactly how these values are used, but Y and M are deprecated as frequencies. You need to also specify start or end, like YS or ME, see https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases

Destination file path. Will be created or overwritten.
"""

import yaml
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add a dependency, preferably ruamel.yaml. Why no top-level import?

``measures``.
"""

import yaml
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

Comment on lines +126 to +133
def test_modifier_config_filter_dict_to_fields_filters_extra_keys():
d = {"haz_type": "TC", "impf_mdd_mult": 0.5, "not_a_field": 123}
filtered = ImpfsetModifierConfig._filter_dict_to_fields(d)
assert "not_a_field" not in filtered
assert "haz_type" in filtered
assert "impf_mdd_mult" in filtered
assert filtered["haz_type"] == "TC"
assert filtered["impf_mdd_mult"] == 0.5
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private methods are private also in the context of tests. Unit tests should not call private methods. A linter will also complain. Private methods must be implicitly tested via calling public methods.

Comment on lines +162 to +164
# ---------------------------------------------------------------------------
# ImpfsetModifierConfig
# ---------------------------------------------------------------------------
Copy link
Copy Markdown
Member

@peanutfun peanutfun Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a class to group tests? Would make test names shorter. Pytest creates a sub-suite, grouping the tests accordingly:

class TestImpfsetModifierConfig:

    def test_defaults(self):
        ...
    def test_from_dict_roundtrip(self):
        ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants