Skip to content

[Option Appraisal Module] [Package 1: Measures] Split 3: Adds new base Measure class#1279

Draft
spjuhel wants to merge 69 commits into
developfrom
feature/option-appraisal-new-measure-base
Draft

[Option Appraisal Module] [Package 1: Measures] Split 3: Adds new base Measure class#1279
spjuhel wants to merge 69 commits into
developfrom
feature/option-appraisal-new-measure-base

Conversation

@spjuhel

@spjuhel spjuhel commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator

Changes proposed in this PR:

This PR brings the basis of the refactored Measure class, using closures (functions that return a function) to define the effects of a measure.

Important points:

  • Effects are separated by effects on Exposures, effects on Impfset, and on Hazard
  • The closures need to accept keyword arguments to enable effects that rely on something else than what the effect is applied on (i.e. year, other part of risk, etc...)
  • We always pass the Exposures, Impfset and Hazard objects as kwargs to the closures by default
  • We pass user-defined function through an allow_kwargs to make sure closures always accept kwargs.
  • Measure instances optionally have a _config attribute that contains a MeasureConfig if the measure was defined from such an object (and is therefore serializable).

PR Author Checklist

PR Reviewer Checklist

spjuhel added 30 commits April 7, 2026 09:34
Cleans-up, Docstringfies

Better to_dict, color parser, and post_inits

Removes duplicate docstring
…MADA-project/climada_python into feature/option-appraisal-dataclasses

@ValentinGebhart ValentinGebhart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This new implementation of the measures looks super cool :) I think it is much cleaner and user-friendly to before! I just commented whenever something was unclear to me. I haven't had time to check the working of the functions with actual data yet , e.g. for some consistency checks.

The only one more conceptual question I have is if you have to work with the (hidden) base_exposure, base_hazard, base_impfunc handling, but maybe I have not understood yet why these can be useful. Happy to discuss!

Note that I first read the tutorial without knowing the code to just check if I understand the usage. Some of the comments there might not be there know after I read the code :)

"\n",
"```python\n",
"new_exp, new_impf, new_haz = retrofitting.apply(exp, impf, haz, kwargs_hazard={'threshold': 3.5})\n",
"impact_with_measure = measure.calc_impact(exp, impf, haz)\n",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This lines is not clear to me. Shouldn't it be
impact_with_measure = retrofitting.calc_impact(exp, impf, haz)?

Also, do I need the first line before running the second one? Or are they two separate things you can to with the measure object? It might make sense to clarify with to brief descriptions?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yup, that's just plain wrong! Will rework this.

"By default, any component not explicitly defined uses the `identity_function`, which simply returns the input object without modification.\n",
"```\n",
"\n",
"The **only** requirement for these python functions is to **take** and **return** an `Exposures` object (or `ImpactFuncSet` or `Hazard`), which enable advanced modeling.\n",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I suggest to make these two sentences a bit clearer. Eg., you do not say what base_exposures, base_impfset, base_hazard are. One suggestion:
"
The only requirement for these python functions is to take and return an Exposures object (or ImpactFuncSet / Hazard objects, respectively), a structure that enables full modeling flexibility.
Notably, the apply() method always passes the entire input triplet of exposure, impact function set, and hazard to every transformation function, so you define measures based on interactions between all three components.
"

"import numpy as np\n",
"\n",
"\n",
"def reduce_exposure_by_depth(exposures, reduction_factor=0.2, threshold=2.0, **kwargs):\n",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

by only reading this tutorial and knowing that in the back you also also hand in base_exposures, base_impfset, and base_hazard to all functions, I am just wondering if this is not a bit confusing. E.g., could the exposures input be different to the base_exposures input? What would that mean? (Note that below you actually note this, but it might be nice to give an example for a case where this is necessary/useful).

Just a naive thought without looking at the code:
Couldn't each of the three functions take all (exp, impfset, haz) as input, and no hidden base objects? e.g. exposures_changes is always a function that has these three inputs. Anyways if it is usefull to have an expsosure input but also a hidden base_exposure, disregard this suggestion.

" kwargs_exposures={'reduction_factor': 0.5, 'threshold': 2.0}\n",
")\n",
"\n",
"# You can also overide this with another \"base\" hazard in the \n",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

see comment above: can you give a simple example about when this is necessary or useful?

" kwargs_exposures={'reduction_factor': 0.5, 'threshold': 2.0}\n",
")\n",
"\n",
"# You can also overide this with another \"base\" hazard in the \n",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

see comment above: can you give a simple example about when this is necessary or useful?


Notes
-----
The kwargs merging follows this priority order:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

maybe not super clear. In my mind prio 1 is overriding prio 2, but below I think 2. is overriding 1.? maybe rephrase

"""Compute impact with the active measure.

This convenience method allow to compute an ``Impact`` object from an
(Exposure, ImpactFuncSet, Hazard) triplet by first applying the measure

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"after having applied" instead of "by first applying"?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
(Exposure, ImpactFuncSet, Hazard) triplet by first applying the measure
(Exposure, ImpactFuncSet, Hazard) triplet after having applied the measure

)
new_exp.assign_centroids(new_haz)
imp = ImpactCalc(new_exp, new_impfs, new_haz).impact(
save_mat=False, assign_centroids=False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we need save_mat False hard-coded? Could this not be an optional parameter?

List of measure names that this measure is a combination of.
cost_income : climada.entity.measures.cost_income.CostIncome
Cost and income object associated with the measure.
implementation_duration : pd.DateOffset, optional

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

as the sub_measures attribute, the implementation_duration attribute is not used anywhere directly? is this here for later additions to the code?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I should just remove it for this PR.

impfset_changes=track("impfset"),
)
m.apply(_make_mock(), _make_mock(), _make_mock(), enforce_copy=False)
assert call_order == ["exposures", "hazard", "impfset"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

very cool tests with all the mocking :)

I'm wondering if it makes sense to eventually also add some tests that actually check numbers, e.g. when you combine two different effects (e.g. using the "base" objects) to see if the effect is what you want? Or should the mock tests above be enough to see that the effects of the change functions (and their interactions?) are the intended ones? Sorry for this vague comment ;)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

My understanding is that would be integration tests (climada/test/test_measures.py).

I'm not sure if they are satisfying yet though.

@spjuhel

spjuhel commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

This new implementation of the measures looks super cool :) I think it is much cleaner and user-friendly to before! I just commented whenever something was unclear to me. I haven't had time to check the working of the functions with actual data yet , e.g. for some consistency checks.

The only one more conceptual question I have is if you have to work with the (hidden) base_exposure, base_hazard, base_impfunc handling, but maybe I have not understood yet why these can be useful. Happy to discuss!

Note that I first read the tutorial without knowing the code to just check if I understand the usage. Some of the comments there might not be there know after I read the code :)

Thanks for these comments.

Answering the conceptual question about the base_X arguments. (see also #1279 (comment)):

The idea comes from the fact that I assume there is a whole range of adaptation options effect on Hazard|Exposures|Impfset where you have to use some information in your base exposures, hazard or impfset.

But it is a bit "advanced" to write for instance (ellipsed/simplified):

def my_effect_on_exposures(exposures, req_hazard):
    indices = req_hazard > 10
    exposures.gdf.loc[indices,"value"] *= 0.98
    
measure_a = Measure(..., exposure_change=my_effect_on_exposures)

exp, impf, haz = measure.apply(exp, impf, haz, kwarg_exposures = {req_hazard: haz}) # That part is slightly scary

By always providing base_Hazard|Exposures|Impfset, you can "simplify":

def my_effect_on_exposures(exposures, base_hazard): # note that the name need to be base_hazard!
    indices = base_hazard > 10
    exposures.gdf.loc[indices,"value"] *= 0.98
    
measure_a = Measure(..., exposure_change=my_effect_on_exposures)

exp, impf, haz = measure.apply(exp, impf, haz) # Scary kwargs are gone!

On this small example it may seem not much, but if the three effects all rely on the three components then it is way more relevant.

I agree the current explanations in the docs might be improved ;D

Comment on lines +128 to +131
A ``Measure`` represents a single adaptation or risk-reduction action. It
holds three (optional) transformation functions, one each for
:class:`Exposures`, :class:`ImpactFuncSet`, and :class:`Hazard`, that are
can be applied to a triplet of ``(Exposures, ImpactFuncSet, Hazard)``, to

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
A ``Measure`` represents a single adaptation or risk-reduction action. It
holds three (optional) transformation functions, one each for
:class:`Exposures`, :class:`ImpactFuncSet`, and :class:`Hazard`, that are
can be applied to a triplet of ``(Exposures, ImpactFuncSet, Hazard)``, to
A ``Measure`` represents an adaptation or risk-reduction action
(or a bundle of actions considered as one). It holds three (optional)
transformation functions, one each for :class:`Exposures`,
:class:`ImpactFuncSet`, and :class:`Hazard`, that are
can be applied to a triplet of ``(Exposures, ImpactFuncSet, Hazard)``, to

Comment on lines +461 to +465
The kwargs merging follows this priority order:

1. Default context: The original triplet (exposures, impfset, hazard)
2. Entity-specific overrides: Values from ``kwargs_exposures``,
``kwargs_impfset``, or ``kwargs_hazard`` respectively

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
The kwargs merging follows this priority order:
1. Default context: The original triplet (exposures, impfset, hazard)
2. Entity-specific overrides: Values from ``kwargs_exposures``,
``kwargs_impfset``, or ``kwargs_hazard`` respectively
Keyword arguments other than "kwargs_exposures", "kwargs_impfset",
"kwargs_hazard" are ignored.
The base exposure, hazard, and impfset provided by default can
be overriden by explicitely providing a different value within
``kwargs_exposures``, ``kwargs_impfset``, or ``kwargs_hazard``
respectively.

Comment on lines +532 to +534
imp = ImpactCalc(new_exp, new_impfs, new_haz).impact(
save_mat=False, assign_centroids=False
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
imp = ImpactCalc(new_exp, new_impfs, new_haz).impact(
save_mat=False, assign_centroids=False
)
imp = ImpactCalc(new_exp, new_impfs, new_haz).impact(
save_mat=kwargs.get("save_mat",False), assign_centroids=kwargs.get("assign_centroids",False)
)

return changed_exposures, changed_impfset, changed_hazard

def calc_impact(
self, exposures: Exposures, impfset: ImpactFuncSet, hazard: Hazard

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
self, exposures: Exposures, impfset: ImpactFuncSet, hazard: Hazard
self, exposures: Exposures, impfset: ImpactFuncSet, hazard: Hazard, **kwargs: dict,

Comment on lines +510 to +513
hazard : Hazard
The initial (no adaptation) hazard set

Returns

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
hazard : Hazard
The initial (no adaptation) hazard set
Returns
hazard : Hazard
The initial (no adaptation) hazard set
kwargs: dict
Keyword arguments passed to the `impact()` method of `ImpactCalc`
(Currently `save_mat` and `assign_centroids`)
Returns

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.

2 participants