Skip to content

Adds conftest.py of common fixtures for integration tests#1273

Open
spjuhel wants to merge 5 commits intodevelopfrom
feature/conftest-for-common-fixtures
Open

Adds conftest.py of common fixtures for integration tests#1273
spjuhel wants to merge 5 commits intodevelopfrom
feature/conftest-for-common-fixtures

Conversation

@spjuhel
Copy link
Copy Markdown
Collaborator

@spjuhel spjuhel commented Mar 26, 2026

This is based on data test used for the integration tests of the static and interpolated trajectories.

The objective is to provide:

  • "as comprehensible as possible and as minimalistic as possible data (exposure, hazard, impfset)"
  • Factories

Changes proposed in this PR:

  • Adds fixtures and factories for minimalistic objects for integration testing
  • Adds an quick example of how to use them

We need to decide if this is something we actually want.
Otherwise, I will move the content to the trajectory integration test directly.

PR Author Checklist

PR Reviewer Checklist

@spjuhel
Copy link
Copy Markdown
Collaborator Author

spjuhel commented Mar 26, 2026

Notes from discussion:

  • shift the examples to a (possibly existing) page in the documentation

@spjuhel
Copy link
Copy Markdown
Collaborator Author

spjuhel commented Mar 31, 2026

Note for this PR (or rather for future improvements):

  • Have also fixtures with the DEMO files
  • Plan (a hackathon?) to move tests to use the fixtures

@spjuhel
Copy link
Copy Markdown
Collaborator Author

spjuhel commented Mar 31, 2026

I've added a documentation page,
I chose to make a new one because direct integration in the "Writing tests" one did not feel right, but I'm happy to reconsider. Note that there are two mentions/references in the "Writing tests" one!

I tried to find the balance between being explicit and concise (had help from Claude to be honest).

I am wondering about putting actual values in the documentation as this implies that we keep them up to date with the file (although the values should technically not change).

Quick access: https://climada-python--1273.org.readthedocs.build/en/1273/development/Guide_test_fixtures.html

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.

This is a good first step towards using common fixtures for all our unit tests! I found a few issues in the code. Other than that, I think we should add an API reference for fixtures to the docs (this might require loading the conftest.py as well. Not sure how to do that atm).

I think most of the functions defined here should not be fixtures because they are not intended for use outside this file. The only "real" fixtures I see:

  • exposures_factory
  • exposures
  • hazard_factory
  • hazard
  • impf_factory
  • linear_impact_function
  • impfset_factory
  • impfset

Comments on the tutorial:

Comment on lines +107 to +116
# Sanity checks
for const in [VALUES, CATEGORIES, EXP_LONS, EXP_LATS]:
assert len(const) == len(
VALUES
), "VALUES, REGIONS, CATEGORIES, EXP_LONS, EXP_LATS should all have the same lengths."

for const in [EVENT_IDS, EVENT_NAMES, DATES, FREQUENCY]:
assert len(const) == len(
EVENT_IDS
), "EVENT_IDS, EVENT_NAMES, DATES, FREQUENCY should all have the same lengths."
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.

If these assertions fail, importing this file will result in an error. I think this is dangerous, as the resulting errors are hard to understand an occur at weird points in time. Are those assertions needed at all? Shouldn't the constructed objects like Exposures complain?

), "EVENT_IDS, EVENT_NAMES, DATES, FREQUENCY should all have the same lengths."


@pytest.fixture(scope="session")
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.

Other scopes than function are primarily intended to avoid reconstructing "expensive" objects. The way I see it, this applies to none of the objects here and can have serious side effects, as modifications will persist throughout the test session. I suggest using the default (smallest) scope everywhere, at least in this file.



@pytest.fixture(scope="session")
def exposure_values():
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.

Do you foresee this being used as a fixture to an actual test? I don't, so I would keep this a "regular" function and only declare fixtures that are intended for use.

Comment on lines +18 to +43
A set of reusable fixtures for testing purpose.

The objective of this file is to provide minimalistic, understandable and consistent
default objects for unit and integration testing.

Values are chosen such that:
- Exposure value of the first points is 0. (First location should always have 0 impacts)
- Category / Group id of all points is 1, except for third point, valued at 2000 (Impacts on that category are always a share of 2000)
- Hazard centroids are the exposure centroids shifted by `HAZARD_JITTER` on both lon and lat.
- There are 4 events, with frequencies == 0.03, 0.01, 0.006, 0.004, 0,
such that impacts for RP250, 100 and 50 and 20 are at_event,
(freq sorted cumulate to 1/250, 1/100, 1/50 and 1/20).
- Hazard intensity is:
* Event 1: zero everywhere (always no impact)
* Event 2: max intensity at first centroid (also always no impact (first centroid is 0))
* Event 3: half max intensity at second centroid (impact == half second centroid)
* Event 4: quarter max intensity everywhere (impact == 1/4 total value)
* Event 5: max intensity everywhere (but zero frequency)
With max intensity set at 100
- Impact function is the "identity function", x intensity is x% damages
- Impact values should be:
* AAI = 18 = 1000*1/2*0.006+(1000+2000+3000+4000+5000)*0.25*0.004
* RP20 = event1 = 0
* RP50 = event2 = 0
* RP100 = event3 = 500 = 1000*1/2
* RP250 = event4 = 3750 = (1000+2000+3000+4000+5000)*0.25
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.

Fixtures are Python functions, so I suggest adding this information to fixture docstrings and creating an API reference. Individual fixtures can be documented with autofunction. See https://docs.pytest.org/en/stable/reference/reference.html#fixtures and https://github.com/pytest-dev/pytest/blob/f93656d611f3c9cd22f346dfeda52cddb778c589/doc/en/reference/reference.rst?plain=1#L325

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.

We should be able to import docstrings from conftest in Sphinx, as the test code is part of the climada package. However, imports may fail (since packages for testing are required).

I asked ChatGPT and it said this:

Screenshot 2026-04-02 at 15 18 56


@pytest.fixture(scope="session")
def exposure_geometry():
return [Point(lon, lat) for lon, lat in zip(EXP_LONS, EXP_LATS)]
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.


# Frequency are choosen so that they cumulate nicely
# to correspond to 250, 100, 50, and 20y return periods (for impacts)
FREQUENCY = np.array([0.03, 0.01, 0.006, 0.004, 0.0])
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.

Do the math for clarity?

Suggested change
FREQUENCY = np.array([0.03, 0.01, 0.006, 0.004, 0.0])
FREQUENCY = np.array([1/20, 1/50, 1/100, 1/200, 0.0, dtype="float"])


# Exposure coordinates
EXP_LONS = np.array([4, 4.25, 4.5, 4, 4.25, 4.5])
EXP_LATS = np.array([45, 45, 45, 45.25, 45.25, 45.25])
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.

Hard to read. As values don't matter, choose digits that are easier to distinguish (also against the LON values)

Suggested change
EXP_LATS = np.array([45, 45, 45, 45.25, 45.25, 45.25])
EXP_LATS = np.array([33, 33, 33, 33.25, 33.25, 33.25])

EXPOSURE_REF_YEAR = 2020
EXPOSURE_VALUE_UNIT = "USD"
VALUES = np.array([0, 1000, 2000, 3000, 4000, 5000])
CATEGORIES = np.array([1, 1, 2, 1, 1, 3])
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.

Good first step, but I think we need to parameterize asap: Use multiple CRS, build exposures of size 1 and >1, etc.

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