From cbd8d5abdb104c908ded146f04c2273f756376ea Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:08:33 +0000 Subject: [PATCH 1/2] Chore: pre-commit.ci automated linting tools update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: pre-commit-ci[bot] Changes in this pull request are as follows: updates: - [github.com/igorshubovych/markdownlint-cli: 76b3d32d3f4b965e1d6425253c59407420ae2c43 → cbcdc463be5464e25e3e955a511599a67b9b987f](https://github.com/igorshubovych/markdownlint-cli/compare/76b3d32d3f4b965e1d6425253c59407420ae2c43...cbcdc463be5464e25e3e955a511599a67b9b987f) - [github.com/kynan/nbstripout: 34071b78c181af3bf5af955c1426466e096fc3db → f5da19ce3b7b40e97c12ee9cd8ce97f48f97ddf7](https://github.com/kynan/nbstripout/compare/34071b78c181af3bf5af955c1426466e096fc3db...f5da19ce3b7b40e97c12ee9cd8ce97f48f97ddf7) - [github.com/shellcheck-py/shellcheck-py: 745eface02aef23e168a8afb6b5737818efbea95 → a6564f5d57444e123433a5c9af733e3975c77229](https://github.com/shellcheck-py/shellcheck-py/compare/745eface02aef23e168a8afb6b5737818efbea95...a6564f5d57444e123433a5c9af733e3975c77229) - [github.com/Mateusz-Grzelinski/actionlint-py: 85c37735ea69e5baf0681530e57e63deee0ce733 → 357f7eec6dec49bf7693dc0dbec30212226d5492](https://github.com/Mateusz-Grzelinski/actionlint-py/compare/85c37735ea69e5baf0681530e57e63deee0ce733...357f7eec6dec49bf7693dc0dbec30212226d5492) - [github.com/astral-sh/ruff-pre-commit: 9b1e9a33f1b33f6ecc2fdbb8d7cc894e951106e4 → 25bcf95026b96e2dd3c0aec3090c3d6ef6ca2e6c](https://github.com/astral-sh/ruff-pre-commit/compare/9b1e9a33f1b33f6ecc2fdbb8d7cc894e951106e4...25bcf95026b96e2dd3c0aec3090c3d6ef6ca2e6c) - [github.com/abravalheri/validate-pyproject: 78f5e0f104cfa803d0597eaa924b4de3becd3e3e → 82d21874c3242e4dc27bb47641c996902ef64eef](https://github.com/abravalheri/validate-pyproject/compare/78f5e0f104cfa803d0597eaa924b4de3becd3e3e...82d21874c3242e4dc27bb47641c996902ef64eef) - [github.com/pre-commit/mirrors-mypy: a66e98df7b4aeeb3724184b332785976d062b92e → ad26ebfa797fcb9b8f83275937c1c1d6e126719c](https://github.com/pre-commit/mirrors-mypy/compare/a66e98df7b4aeeb3724184b332785976d062b92e...ad26ebfa797fcb9b8f83275937c1c1d6e126719c) --- .pre-commit-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 007f4824..e45f5deb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,7 +67,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/igorshubovych/markdownlint-cli - rev: 76b3d32d3f4b965e1d6425253c59407420ae2c43 # frozen: v0.47.0 + rev: cbcdc463be5464e25e3e955a511599a67b9b987f # frozen: v0.50.0 hooks: - id: markdownlint args: ["--fix"] @@ -79,7 +79,7 @@ repos: # "off"); execution_count is not needed for that and is just as volatile # as the timing metadata, so it's left to the default (stripped) behavior. - repo: https://github.com/kynan/nbstripout - rev: 34071b78c181af3bf5af955c1426466e096fc3db # frozen: 0.9.1 + rev: f5da19ce3b7b40e97c12ee9cd8ce97f48f97ddf7 # frozen: 0.9.1 hooks: - id: nbstripout args: ["--keep-output"] @@ -97,13 +97,13 @@ repos: args: ["--ignore=E006,E011"] - repo: https://github.com/shellcheck-py/shellcheck-py - rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1 + rev: a6564f5d57444e123433a5c9af733e3975c77229 # frozen: v0.11.0.1-1 hooks: - id: shellcheck args: ["-x"] # Check external files - repo: https://github.com/Mateusz-Grzelinski/actionlint-py - rev: 85c37735ea69e5baf0681530e57e63deee0ce733 # frozen: v1.7.10.24 + rev: 357f7eec6dec49bf7693dc0dbec30212226d5492 # frozen: v1.7.12.25 hooks: - id: actionlint @@ -119,14 +119,14 @@ repos: ] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 9b1e9a33f1b33f6ecc2fdbb8d7cc894e951106e4 # frozen: v0.14.13 + rev: 25bcf95026b96e2dd3c0aec3090c3d6ef6ca2e6c # frozen: v0.17.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --config=pyproject.toml] - id: ruff-format - repo: https://github.com/abravalheri/validate-pyproject - rev: 78f5e0f104cfa803d0597eaa924b4de3becd3e3e # frozen: v0.24.1 + rev: 82d21874c3242e4dc27bb47641c996902ef64eef # frozen: v0.27 hooks: - id: validate-pyproject # Optional extra validations from SchemaStore: @@ -165,7 +165,7 @@ repos: entry: bash -c 'if [ ! -d .mypy_cache ]; then /bin/mkdir .mypy_cache; fi; exit 0' - repo: https://github.com/pre-commit/mirrors-mypy - rev: "a66e98df7b4aeeb3724184b332785976d062b92e" # frozen: v1.19.1 + rev: "ad26ebfa797fcb9b8f83275937c1c1d6e126719c" # frozen: v2.4.1 hooks: - id: mypy verbose: true From d203d4bbc971c0403f80cba42178b5979ff61aba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:10:55 +0000 Subject: [PATCH 2/2] Chore: pre-commit.ci automated linting tools update Signed-off-by: pre-commit-ci[bot] Changes in this pull request are as follows: --- .../getting_started/asset_level_impacts.ipynb | 4 +- .../cement_plants_example.ipynb | 10 +- .../custom_vulnerability.ipynb | 12 +- docs/getting_started/hazard_indicators.ipynb | 2 +- docs/getting_started/hazard_inventory.ipynb | 1 + .../vulnerability/vulnerability_config.ipynb | 8 +- .../inundation_hazus/onboard.ipynb | 3 +- .../inundation_jrc/onboard.ipynb | 3 +- .../onboard.ipynb | 1 + .../wind_eberenz/onboard.ipynb | 3 +- .../wind_hazus/onboard.ipynb | 3 +- .../vulnerability_uncertainty.ipynb | 1 + methodology/plots/interp_pixel_is_area.ipynb | 5 +- src/physrisk/api/v1/availability_sources.py | 8 +- src/physrisk/api/v1/common.py | 53 +++---- src/physrisk/api/v1/exposure_req_resp.py | 12 +- src/physrisk/api/v1/hazard_data.py | 71 ++++----- src/physrisk/api/v1/hazard_image.py | 26 ++-- src/physrisk/api/v1/impact_req_resp.py | 97 ++++++------ src/physrisk/api/v1/scoring_schemes.py | 4 +- src/physrisk/container.py | 16 +- src/physrisk/data/geocode.py | 6 +- src/physrisk/data/geocode_google.py | 51 +++---- src/physrisk/data/hazard_data_provider.py | 34 ++--- src/physrisk/data/image_creator.py | 47 +++--- src/physrisk/data/inventory.py | 19 ++- src/physrisk/data/inventory_reader.py | 16 +- .../data/pregenerated_hazard_model.py | 20 +-- src/physrisk/data/scenario_year_resolution.py | 2 +- src/physrisk/data/static/oed_occupancy.py | 3 +- src/physrisk/data/utilities.py | 1 + src/physrisk/data/zarr_reader.py | 34 ++--- src/physrisk/hazard_models/core_hazards.py | 3 +- .../hazard_models/credentials_provider.py | 6 +- src/physrisk/hazard_models/hazard_cache.py | 25 ++-- .../hazard_models/hazard_model_factory.py | 30 ++-- .../hazard_models/jba_hazard_model.py | 57 ++++--- .../hazard_models/jba_image_creator.py | 72 ++++----- src/physrisk/kernel/assets.py | 68 ++++----- src/physrisk/kernel/calculation.py | 12 +- src/physrisk/kernel/curve.py | 6 +- src/physrisk/kernel/events.py | 8 +- src/physrisk/kernel/exposure.py | 20 ++- src/physrisk/kernel/financial_model.py | 3 +- src/physrisk/kernel/hazard_event_distrib.py | 14 +- src/physrisk/kernel/hazard_model.py | 48 +++--- src/physrisk/kernel/hazards.py | 19 +-- src/physrisk/kernel/impact.py | 37 ++--- src/physrisk/kernel/impact_aggregator.py | 61 ++++---- src/physrisk/kernel/impact_distrib.py | 14 +- src/physrisk/kernel/risk.py | 73 ++++----- src/physrisk/kernel/vulnerability_distrib.py | 12 +- .../kernel/vulnerability_matrix_provider.py | 4 +- src/physrisk/kernel/vulnerability_model.py | 33 ++-- src/physrisk/requests.py | 141 ++++++++---------- .../risk_models/generic_risk_model.py | 26 ++-- src/physrisk/risk_models/loss_model.py | 16 +- .../risk_models/portfolio_risk_model.py | 10 +- src/physrisk/risk_models/risk_models.py | 8 +- .../score_based_portfolio_risk_model.py | 9 +- src/physrisk/utils/encoder.py | 4 +- src/physrisk/utils/event_loop.py | 5 +- src/physrisk/utils/lazy.py | 3 +- .../chronic_heat_models.py | 21 +-- .../config_based_impact_curves.py | 78 +++++----- .../config_based_vuln_model_acute.py | 15 +- .../config_based_vuln_model_param.py | 6 +- .../config_cdf_based_vuln_function.py | 27 ++-- .../configuration/asset_factory.py | 16 +- .../configuration/build_all_config.py | 18 +-- .../configuration/config_builders.py | 27 ++-- .../configuration/hazus_config_builders.py | 41 ++--- .../configuration/oed_attribute_matcher.py | 17 +-- .../configuration/oed_hazus_mapping.py | 14 +- src/physrisk/vulnerability_models/downtime.py | 14 +- .../vulnerability_models/example_models.py | 3 +- .../impact_function_selector.py | 7 +- .../power_generating_asset_models.py | 5 +- .../real_estate_models.py | 4 +- .../thermal_power_generation_models.py | 33 ++-- .../vulnerability_models/vulnerability.py | 18 +-- tests/api/test_container.py | 2 +- tests/api/test_data_requests.py | 14 +- tests/api/test_impact_requests.py | 2 +- tests/conftest.py | 2 +- tests/data/test_data_retrieval.py | 18 +-- tests/data/test_geocode_google.py | 10 +- tests/data/test_hazard_model_store.py | 28 ++-- tests/data/test_scenario_year_resolution.py | 4 +- tests/hazard_models/test_api_hazard_model.py | 5 +- tests/hazard_models/test_visualisation.py | 8 +- tests/kernel/test_asset_impact.py | 3 +- tests/kernel/test_chronic_asset_impact.py | 8 +- tests/kernel/test_exposure.py | 8 +- tests/kernel/test_financial_model.py | 2 +- tests/kernel/test_hazard_models.py | 13 +- tests/kernel/test_image_creation.py | 11 +- tests/kernel/test_live_services.py | 3 +- tests/risk_models/test_portfolio_models.py | 26 ++-- tests/risk_models/test_risk_models.py | 14 +- tests/test_oed_occupancy_update.py | 1 - .../test_config_based_vuln_model_param.py | 6 +- .../test_config_based_vulnerability.py | 5 +- .../test_config_generate.py | 3 +- .../test_example_models.py | 2 +- .../test_hazard_selection.py | 2 +- .../test_multi_interval_matcher.py | 1 - .../test_oed_based_config.py | 17 +-- .../test_power_generating_asset_models.py | 13 +- .../test_real_estate_models.py | 2 +- tests/vulnerability_models/test_wbgt_model.py | 19 +-- .../vulnerability_models/test_wind_models.py | 2 +- 112 files changed, 981 insertions(+), 1060 deletions(-) diff --git a/docs/getting_started/asset_level_impacts.ipynb b/docs/getting_started/asset_level_impacts.ipynb index 203383b6..211c3ea7 100644 --- a/docs/getting_started/asset_level_impacts.ipynb +++ b/docs/getting_started/asset_level_impacts.ipynb @@ -106,7 +106,7 @@ " key = i[\"key\"]\n", " asset0_impact_dict[Key(key[\"hazard_type\"], key[\"scenario_id\"], key[\"year\"])] = i\n", "\n", - "hazard_types = set(k.hazard_type for k in asset0_impact_dict.keys())\n", + "hazard_types = set(k.hazard_type for k in asset0_impact_dict)\n", "wind_impact_histo = asset0_impact_dict[Key(\"Wind\", \"historical\", \"None\")]\n", "wind_impact_ssp585 = asset0_impact_dict[Key(\"Wind\", \"ssp585\", \"2050\")]" ] @@ -132,8 +132,8 @@ } ], "source": [ - "from plotly.subplots import make_subplots\n", "from IPython.display import HTML\n", + "from plotly.subplots import make_subplots\n", "\n", "exceedance_histo = wind_impact_histo[\"impact_exceedance\"]\n", "exceedance_ssp585 = wind_impact_ssp585[\"impact_exceedance\"]\n", diff --git a/docs/getting_started/cement_plants_example.ipynb b/docs/getting_started/cement_plants_example.ipynb index b7926dad..7359ae7c 100644 --- a/docs/getting_started/cement_plants_example.ipynb +++ b/docs/getting_started/cement_plants_example.ipynb @@ -15,17 +15,19 @@ "metadata": {}, "outputs": [], "source": [ - "from IPython.display import display\n", "import json\n", "from typing import NamedTuple\n", + "\n", + "import plotly.io\n", "from dependency_injector import providers\n", + "from IPython.display import display\n", + "\n", "from physrisk.container import Container\n", - "from physrisk.vulnerability_models.vulnerability import VulnerabilityModelsFactory\n", "from physrisk.vulnerability_models.config_based_impact_curves import (\n", " config_items_from_csv,\n", " config_items_to_df,\n", ")\n", - "import plotly.io\n", + "from physrisk.vulnerability_models.vulnerability import VulnerabilityModelsFactory\n", "\n", "plotly.io.renderers.default = \"notebook\"" ] @@ -374,7 +376,7 @@ " key = i[\"key\"]\n", " asset_impact_dict[Key(key[\"hazard_type\"], key[\"scenario_id\"], key[\"year\"])] = i\n", "\n", - "hazard_types = sorted(set(k.hazard_type for k in asset_impact_dict.keys()))\n", + "hazard_types = sorted(set(k.hazard_type for k in asset_impact_dict))\n", "\n", "for hazard_type in hazard_types:\n", " impact_histo = asset_impact_dict[Key(hazard_type, \"historical\", \"None\")]\n", diff --git a/docs/getting_started/custom_vulnerability.ipynb b/docs/getting_started/custom_vulnerability.ipynb index 5e8c58ed..4cc6125d 100644 --- a/docs/getting_started/custom_vulnerability.ipynb +++ b/docs/getting_started/custom_vulnerability.ipynb @@ -27,19 +27,21 @@ "metadata": {}, "outputs": [], "source": [ - "from io import StringIO\n", "import json\n", + "from io import StringIO\n", "from typing import NamedTuple\n", + "\n", + "import plotly.io\n", "from dependency_injector import providers\n", + "from plotly.subplots import make_subplots\n", + "\n", "from physrisk.container import Container\n", - "from physrisk.vulnerability_models.vulnerability import VulnerabilityModelsFactory\n", "from physrisk.vulnerability_models.config_based_impact_curves import (\n", " VulnerabilityConfigItem,\n", " config_items_from_csv,\n", " config_items_to_df,\n", ")\n", - "import plotly.io\n", - "from plotly.subplots import make_subplots\n", + "from physrisk.vulnerability_models.vulnerability import VulnerabilityModelsFactory\n", "\n", "plotly.io.renderers.default = \"notebook\"" ] @@ -212,7 +214,7 @@ " key = i[\"key\"]\n", " asset0_impact_dict[Key(key[\"hazard_type\"], key[\"scenario_id\"], key[\"year\"])] = i\n", "\n", - "hazard_types = set(k.hazard_type for k in asset0_impact_dict.keys())\n", + "hazard_types = set(k.hazard_type for k in asset0_impact_dict)\n", "wind_impact_histo = asset0_impact_dict[Key(\"Wind\", \"historical\", \"None\")]\n", "wind_impact_ssp585 = asset0_impact_dict[Key(\"Wind\", \"ssp585\", \"2050\")]\n", "\n", diff --git a/docs/getting_started/hazard_indicators.ipynb b/docs/getting_started/hazard_indicators.ipynb index d8f16457..408a9cb1 100644 --- a/docs/getting_started/hazard_indicators.ipynb +++ b/docs/getting_started/hazard_indicators.ipynb @@ -202,8 +202,8 @@ } ], "source": [ - "from plotly.subplots import make_subplots\n", "from IPython.display import HTML\n", + "from plotly.subplots import make_subplots\n", "\n", "fig1 = make_subplots(rows=1, cols=2)\n", "fig1.add_scatter(\n", diff --git a/docs/getting_started/hazard_inventory.ipynb b/docs/getting_started/hazard_inventory.ipynb index 94619c42..862a7b33 100644 --- a/docs/getting_started/hazard_inventory.ipynb +++ b/docs/getting_started/hazard_inventory.ipynb @@ -22,6 +22,7 @@ "outputs": [], "source": [ "import pprint as pp\n", + "\n", "import pandas as pd\n", "import requests\n", "from IPython.display import Markdown, display" diff --git a/docs/user_guide/vulnerability/vulnerability_config.ipynb b/docs/user_guide/vulnerability/vulnerability_config.ipynb index b23fa3c5..a2eb77f1 100644 --- a/docs/user_guide/vulnerability/vulnerability_config.ipynb +++ b/docs/user_guide/vulnerability/vulnerability_config.ipynb @@ -142,13 +142,15 @@ "source": [ "# pip install nbformat pandas plotly requests\n", "from io import StringIO\n", + "\n", + "import plotly.io\n", + "from IPython.display import HTML\n", + "from plotly.subplots import make_subplots\n", + "\n", "from physrisk.vulnerability_models.config_based_impact_curves import (\n", " config_items_from_csv,\n", " config_items_to_df,\n", ")\n", - "import plotly.io\n", - "from plotly.subplots import make_subplots\n", - "from IPython.display import HTML\n", "\n", "plotly.io.renderers.default = \"notebook\"" ] diff --git a/docs/user_guide/vulnerability/vulnerability_functions/inundation_hazus/onboard.ipynb b/docs/user_guide/vulnerability/vulnerability_functions/inundation_hazus/onboard.ipynb index c53480ca..4694b4e8 100644 --- a/docs/user_guide/vulnerability/vulnerability_functions/inundation_hazus/onboard.ipynb +++ b/docs/user_guide/vulnerability/vulnerability_functions/inundation_hazus/onboard.ipynb @@ -50,8 +50,9 @@ "outputs": [], "source": [ "import plotly.io\n", - "from plotly.subplots import make_subplots\n", "from IPython.display import HTML\n", + "from plotly.subplots import make_subplots\n", + "\n", "from physrisk.vulnerability_models.configuration.hazus_config_builders import (\n", " ConfigBuilderHazusFlood,\n", ")\n", diff --git a/docs/user_guide/vulnerability/vulnerability_functions/inundation_jrc/onboard.ipynb b/docs/user_guide/vulnerability/vulnerability_functions/inundation_jrc/onboard.ipynb index f2d1d209..06d528db 100644 --- a/docs/user_guide/vulnerability/vulnerability_functions/inundation_jrc/onboard.ipynb +++ b/docs/user_guide/vulnerability/vulnerability_functions/inundation_jrc/onboard.ipynb @@ -21,8 +21,9 @@ "outputs": [], "source": [ "import plotly.io\n", - "from plotly.subplots import make_subplots\n", "from IPython.display import HTML\n", + "from plotly.subplots import make_subplots\n", + "\n", "from physrisk.vulnerability_models.configuration.config_builders import (\n", " ConfigBuilderJRCFlood,\n", ")\n", diff --git a/docs/user_guide/vulnerability/vulnerability_functions/multi_hazard_wri_power_generating_assets/onboard.ipynb b/docs/user_guide/vulnerability/vulnerability_functions/multi_hazard_wri_power_generating_assets/onboard.ipynb index d055a091..4c8369e0 100644 --- a/docs/user_guide/vulnerability/vulnerability_functions/multi_hazard_wri_power_generating_assets/onboard.ipynb +++ b/docs/user_guide/vulnerability/vulnerability_functions/multi_hazard_wri_power_generating_assets/onboard.ipynb @@ -23,6 +23,7 @@ "outputs": [], "source": [ "import json\n", + "\n", "import pandas\n", "\n", "data = {\"items\": []}\n", diff --git a/docs/user_guide/vulnerability/vulnerability_functions/wind_eberenz/onboard.ipynb b/docs/user_guide/vulnerability/vulnerability_functions/wind_eberenz/onboard.ipynb index 4f20f262..0e95e66f 100644 --- a/docs/user_guide/vulnerability/vulnerability_functions/wind_eberenz/onboard.ipynb +++ b/docs/user_guide/vulnerability/vulnerability_functions/wind_eberenz/onboard.ipynb @@ -30,8 +30,9 @@ "outputs": [], "source": [ "import plotly.io\n", - "from plotly.subplots import make_subplots\n", "from IPython.display import HTML\n", + "from plotly.subplots import make_subplots\n", + "\n", "from physrisk.vulnerability_models.configuration.config_builders import (\n", " ConfigBuilderEberenzWind,\n", ")\n", diff --git a/docs/user_guide/vulnerability/vulnerability_functions/wind_hazus/onboard.ipynb b/docs/user_guide/vulnerability/vulnerability_functions/wind_hazus/onboard.ipynb index c4c856be..66077b8a 100644 --- a/docs/user_guide/vulnerability/vulnerability_functions/wind_hazus/onboard.ipynb +++ b/docs/user_guide/vulnerability/vulnerability_functions/wind_hazus/onboard.ipynb @@ -86,8 +86,9 @@ "source": [ "import numpy as np\n", "import plotly.io\n", - "from plotly.subplots import make_subplots\n", "from IPython.display import HTML\n", + "from plotly.subplots import make_subplots\n", + "\n", "from physrisk.vulnerability_models.configuration.hazus_config_builders import (\n", " ConfigBuilderHazusWind,\n", ")\n", diff --git a/docs/user_guide/vulnerability/vulnerability_uncertainty.ipynb b/docs/user_guide/vulnerability/vulnerability_uncertainty.ipynb index 47207464..13fc663f 100644 --- a/docs/user_guide/vulnerability/vulnerability_uncertainty.ipynb +++ b/docs/user_guide/vulnerability/vulnerability_uncertainty.ipynb @@ -44,6 +44,7 @@ "outputs": [], "source": [ "import numpy as np\n", + "\n", "from physrisk.vulnerability_models.config_cdf_based_vuln_function import (\n", " CDFBasedVulnerabilityFunction,\n", ")\n", diff --git a/methodology/plots/interp_pixel_is_area.ipynb b/methodology/plots/interp_pixel_is_area.ipynb index 42047e2a..743005a5 100644 --- a/methodology/plots/interp_pixel_is_area.ipynb +++ b/methodology/plots/interp_pixel_is_area.ipynb @@ -8,10 +8,11 @@ "source": [ "import os\n", "import sys\n", - "from affine import Affine\n", + "\n", "import s3fs\n", - "import hazard.utilities.zarr_utilities as zarr_utilities\n", + "from affine import Affine\n", "from hazard.sources.osc_zarr import OscZarr\n", + "from hazard.utilities import zarr_utilities\n", "\n", "sys.path.append(\"../src\")\n", "\n", diff --git a/src/physrisk/api/v1/availability_sources.py b/src/physrisk/api/v1/availability_sources.py index dbc8f17a..3e6c8fe0 100644 --- a/src/physrisk/api/v1/availability_sources.py +++ b/src/physrisk/api/v1/availability_sources.py @@ -1,5 +1,5 @@ -from typing import Dict, List from pydantic import BaseModel, Field + from physrisk.api.v1.hazard_data import ScenarioYears @@ -16,13 +16,13 @@ class AvailabilitySourcesRequest(BaseModel): class HazardTypeAvailability(BaseModel): - scenarios: List[ScenarioYears] = [] - available_for_assets_type: List[str] = [] + scenarios: list[ScenarioYears] = [] + available_for_assets_type: list[str] = [] indicator_display_name: str = "" class AvailabilitySourcesResponse(BaseModel): - hazards: Dict[str, Dict[str, HazardTypeAvailability]] = Field( + hazards: dict[str, dict[str, HazardTypeAvailability]] = Field( {}, description="Information of available sources for the hazards in the inventory", ) diff --git a/src/physrisk/api/v1/common.py b/src/physrisk/api/v1/common.py index e8bdd1b9..bddb66b0 100644 --- a/src/physrisk/api/v1/common.py +++ b/src/physrisk/api/v1/common.py @@ -1,4 +1,5 @@ -from typing import Annotated, List, Optional, Sequence, Union +from collections.abc import Sequence +from typing import Annotated import numpy as np import numpy.typing as npt @@ -38,13 +39,13 @@ class FinancialDetails(BaseModel): default="EUR", description="Currency of the financial details, specified in ISO 4217 3 latter code.", ) - revenue_attributable: Optional[float] = Field( + revenue_attributable: float | None = Field( description="Revenue attributable to the asset, in the specified currency." "This is used to calculate business disruption impacts: if the asset operations are disrupted or associated costs" "increase, this value will be used to estimate the financial impact.", alias="revenue_attrib", - ) # noqa: E501 - total_insurable_value: Optional[float] = Field( + ) + total_insurable_value: float | None = Field( description="Total insurable value of the asset, in the specified currency." ) @@ -66,33 +67,33 @@ class Asset(BaseModel): """ id: str = Field(default="", description="Unique identifier for the asset.") - asset_class: Optional[str] = Field( + asset_class: str | None = Field( default=None, description="name of asset class; corresponds to physrisk class names, e.g. PowerGeneratingAsset. If not provided, " "physrisk will infer the class from 'occupancy_code'.", ) - latitude: Optional[float] = Field( + latitude: float | None = Field( default=None, description="Latitude in degrees, specified in WGS84 (EPSG:4326)." ) - longitude: Optional[float] = Field( + longitude: float | None = Field( default=None, description="Longitude in degrees, specified in WGS84 (EPSG:4326).", ) - wkt: Optional[str] = Field( + wkt: str | None = Field( default=None, description="Well Known Text representation of asset geometry, specified in WGS84 (EPSG:4326).", ) - buffer: Optional[float] = Field( + buffer: float | None = Field( default=None, description="Buffer to be applied to the geometry, in metres. " "If 'wkt' is not provided, the buffer is applied.", ) - type: Optional[str] = Field( + type: str | None = Field( None, description="Type of the asset //. This is free-text that matches lines in the vulnerability config, or" " programmatic vulnerability models", ) - location: Optional[str] = Field( + location: str | None = Field( default=None, description="Location (e.g. Africa, Asia, Europe, North America, Oceania, South America); this is used to select location-specific" "vulnerability functions where available.", @@ -102,7 +103,7 @@ class Asset(BaseModel): description="Occupancy code. This is Open Exposure Data occupancy code, unless otherwise specified (and currently no other scheme supported)." "Defaults to 1000, which is 'unknown', in which case physrisk asset class is inferred from 'asset_class'.", ) - number_of_storeys: Optional[int] = Field( + number_of_storeys: int | None = Field( default=-1, description="Number of storeys. Can also take special values: " "-1 = unknown number of storeys - low rise, -2 = unknown number of storeys - mid rise, -3 = Unknown number of storeys = high rise)", @@ -122,16 +123,16 @@ class Asset(BaseModel): "example.", ) # see https://www.fema.gov/sites/default/files/documents/fema_hazus-inventory-technical-manual-6.1.pdf for information about first floor heights in US - capacity: Optional[float] = Field( + capacity: float | None = Field( default=None, description="Power generation capacity in MW for power generating assets.", kw_only=True, ) - financial: Optional[FinancialDetails] = Field( + financial: FinancialDetails | None = Field( default=None, description="Financial details needed for a portfolio-level company assessment.", ) - aggregation_id: Optional[str] = Field( + aggregation_id: str | None = Field( default=None, description="Identifier of the sub-portfolio this asset belongs to. Assets sharing the same " "aggregation_id are aggregated together into independent portfolio-level results, separate " @@ -164,11 +165,11 @@ def validate_occupancy_code(cls, v: int) -> int: class Assets(BaseModel): """Defines a collection of assets.""" - items: List[Asset] + items: list[Asset] class BaseHazardRequest(BaseModel): - group_ids: List[str] = Field( + group_ids: list[str] = Field( ["public"], description="""List of data groups which can be used to service the request, e.g. 'osc': available to OS-Climate members (e.g. pending license decision), @@ -187,7 +188,7 @@ class Country(BaseModel): class Countries(BaseModel): """List of Country.""" - items: List[Country] + items: list[Country] class IntensityCurve(BaseModel): @@ -195,12 +196,12 @@ class IntensityCurve(BaseModel): return periods in years. Chronic hazards are parameterized by a set of index values. Index values are defined per indicator.""" - intensities: List[float] = Field([], description="Hazard indicator intensities.") - return_periods: Optional[Sequence[float]] = Field( + intensities: list[float] = Field([], description="Hazard indicator intensities.") + return_periods: Sequence[float] | None = Field( [], description="[Deprecated] Return period in years in the case of an acute hazard.", ) - index_values: Optional[Union[Sequence[float], Sequence[str]]] = Field( + index_values: Sequence[float] | Sequence[str] | None = Field( [], description="Set of index values. \ This is return period in years in the case of an acute hazard or \ @@ -239,7 +240,7 @@ class HazardEventDistrib(BaseModel): default_factory=lambda: np.zeros(10), description="" ) probabilities: NDArray = Field(default_factory=lambda: np.zeros(10), description="") - path: List[str] = Field([], description="Path to the hazard indicator data source.") + path: list[str] = Field([], description="Path to the hazard indicator data source.") class VulnerabilityCurve(BaseModel): @@ -250,10 +251,10 @@ class VulnerabilityCurve(BaseModel): location: str = Field(...) event_type: str = Field(description="hazard event type, e.g. RiverineInundation") impact_type: str = Field(description="'Damage' or 'Disruption'") - intensity: List[float] = Field(...) + intensity: list[float] = Field(...) intensity_units: str = Field(description="units of the intensity") - impact_mean: List[float] = Field(description="mean impact (damage or disruption)") - impact_std: List[float] = Field( + impact_mean: list[float] = Field(description="mean impact (damage or disruption)") + impact_std: list[float] = Field( description="standard deviation of impact (damage or disruption)" ) @@ -261,7 +262,7 @@ class VulnerabilityCurve(BaseModel): class VulnerabilityCurves(BaseModel): """List of VulnerabilityCurve.""" - items: List[VulnerabilityCurve] + items: list[VulnerabilityCurve] class VulnerabilityDistrib(BaseModel): diff --git a/src/physrisk/api/v1/exposure_req_resp.py b/src/physrisk/api/v1/exposure_req_resp.py index 25567173..bf6e8e5d 100644 --- a/src/physrisk/api/v1/exposure_req_resp.py +++ b/src/physrisk/api/v1/exposure_req_resp.py @@ -1,5 +1,3 @@ -from typing import Dict, List, Optional - from pydantic import BaseModel, Field from physrisk.api.v1.common import Assets @@ -19,7 +17,7 @@ class AssetExposureRequest(BaseModel): 2050, description="Projection year (2030, 2050, 2080). Any year before 2030, e.g. 1980, is treated as historical.", ) - provider_max_requests: Dict[str, int] = Field( + provider_max_requests: dict[str, int] = Field( {}, description="The maximum permitted number of \ requests to external providers. This setting is intended in particular for paid-for data. The key \ @@ -29,7 +27,7 @@ class AssetExposureRequest(BaseModel): class Exposure(BaseModel): category: str - value: Optional[float] + value: float | None path: str = Field( "unknown", description="Path to the hazard indicator data source." ) @@ -38,12 +36,12 @@ class Exposure(BaseModel): class AssetExposure(BaseModel): """Impact at asset level. Each asset can have impacts for multiple hazard types.""" - asset_id: Optional[str] = Field( + asset_id: str | None = Field( None, description="""Asset identifier; will appear if provided in the request otherwise order of assets in response is identical to order of assets in request.""", ) - exposures: Dict[str, Exposure] = Field( + exposures: dict[str, Exposure] = Field( {}, description="Category (value) for each hazard type (key)." ) @@ -51,4 +49,4 @@ class AssetExposure(BaseModel): class AssetExposureResponse(BaseModel): """Response to impact request.""" - items: List[AssetExposure] + items: list[AssetExposure] diff --git a/src/physrisk/api/v1/hazard_data.py b/src/physrisk/api/v1/hazard_data.py index 1c01934d..93a93d2b 100644 --- a/src/physrisk/api/v1/hazard_data.py +++ b/src/physrisk/api/v1/hazard_data.py @@ -1,6 +1,7 @@ +from collections.abc import Iterable, Sequence from enum import Flag, auto from pathlib import PurePosixPath -from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union, Literal +from typing import Literal from pydantic import AliasChoices, BaseModel, Field, computed_field, field_validator @@ -11,14 +12,14 @@ class Colormap(BaseModel): """Provides details of colormap.""" - min_index: Optional[int] = Field( + min_index: int | None = Field( 1, description="Value of colormap minimum. Constant min for a group of maps can facilitate comparison.", ) min_value: float = Field( description="Value of colormap minimum. Constant min for a group of maps can facilitate comparison." ) - max_index: Optional[int] = Field( + max_index: int | None = Field( 255, description="Value of colormap maximum. Constant max for a group of maps can facilitate comparison.", ) @@ -26,7 +27,7 @@ class Colormap(BaseModel): description="Value of colormap maximum. Constant max for a group of maps can facilitate comparison." ) name: str = Field(description="Name of colormap, e.g. 'flare', 'heating'.") - nodata_index: Optional[int] = Field(0, description="Index used for no data.") + nodata_index: int | None = Field(0, description="Index used for no data.") units: str = Field(description="Units, e.g. 'degree days', 'metres'.") scaling: Literal["linear", "log"] = Field( "linear", @@ -38,21 +39,21 @@ class Colormap(BaseModel): class MapInfo(BaseModel): """Provides information about map layer.""" - colormap: Optional[Colormap] = Field(description="Details of colormap.") - bounds: List[Tuple[float, float]] = Field( + colormap: Colormap | None = Field(description="Details of colormap.") + bounds: list[tuple[float, float]] = Field( [(-180.0, 85.0), (180.0, 85.0), (180.0, -85.0), (-180.0, -85.0)], - description="Bounds (top/left, top/right, bottom/right, bottom/left) as degrees. Note applied to map reprojected into Web Mercator CRS.", # noqa + description="Bounds (top/left, top/right, bottom/right, bottom/left) as degrees. Note applied to map reprojected into Web Mercator CRS.", ) - bbox: Optional[List[float]] = Field(default=[-180.0, -85.0, 180.0, 85.0]) - index_values: Optional[Sequence[Union[float, int, str]]] = Field( + bbox: list[float] | None = Field(default=[-180.0, -85.0, 180.0, 85.0]) + index_values: Sequence[float | int | str] | None = Field( default=None, description="Index values to include in maps. If None, the last index value only is included.", ) path: str = Field( - description="Name of array reprojected to Web Mercator for on-the-fly display or to hash to obtain tile ID. If not supplied, convention is to add '_map' to path." # noqa + description="Name of array reprojected to Web Mercator for on-the-fly display or to hash to obtain tile ID. If not supplied, convention is to add '_map' to path." ) # note that the bounds should be consistent with the array attributes - source: Optional[str] = Field( + source: str | None = Field( description="""Source of map image. These are 'map_array': single Mercator projection array at path above 'map_array_pyramid': pyramid of Mercator projection arrays @@ -86,7 +87,7 @@ class HazardResource(BaseModel): """Provides information about a set of hazard indicators, including available scenarios and years.""" hazard_type: str = Field(description="Type of hazard.") - group_id: Optional[str] = Field( + group_id: str | None = Field( "public", description="Identifier of the resource group (used for authentication).", ) @@ -94,7 +95,7 @@ class HazardResource(BaseModel): indicator_id: str = Field( description="Identifier of the hazard indicator (i.e. the modelled quantity), e.g. 'flood_depth'." ) - indicator_model_id: Optional[str] = Field( + indicator_model_id: str | None = Field( None, description="Identifier specifying the type of model used in the derivation of the indicator " "(e.g. whether flood model includes impact of sea-level rise).", @@ -102,21 +103,21 @@ class HazardResource(BaseModel): indicator_model_gcm: str = Field( description="Identifier of general circulation model(s) used in the derivation of the indicator." ) - params: Dict[str, List[str]] = Field( + params: dict[str, list[str]] = Field( {}, description="Parameters used to expand wild-carded fields." ) display_name: str = Field(description="Text used to display indicator.") - display_groups: List[str] = Field( + display_groups: list[str] = Field( [], description="Text used to group the (expanded) indicators for display." ) description: str = Field( description="Brief description in mark down of the indicator and model that generated the indicator." ) - map: Optional[MapInfo] = Field( + map: MapInfo | None = Field( None, description="Optional information used for display of the indicator in a map.", ) - scenarios: List[ScenarioYears] = Field( + scenarios: list[ScenarioYears] = Field( description="Climate change scenarios for which the indicator is available." ) store_netcdf_coords: bool = Field( @@ -204,7 +205,7 @@ def expand(item: str, key: str, param: str): def expand_resource( - resource: HazardResource, keys: List[str], params: Dict[str, List[str]] + resource: HazardResource, keys: list[str], params: dict[str, list[str]] ) -> Iterable[HazardResource]: if len(keys) == 0: yield resource.model_copy(deep=True, update={"params": {}}) @@ -255,33 +256,33 @@ class InventorySource(Flag): class HazardAvailabilityRequest(BaseModel): - types: Optional[List[str]] = [] # e.g. ["RiverineInundation"] - sources: Optional[List[str]] = Field( + types: list[str] | None = [] # e.g. ["RiverineInundation"] + sources: list[str] | None = Field( None, description="Sources of inventory, can be 'embedded', 'hazard' or 'hazard_test'.", ) class HazardAvailabilityResponse(BaseModel): - models: List[HazardResource] + models: list[HazardResource] colormaps: dict class HazardDescriptionRequest(BaseModel): - paths: List[str] = Field(description="List of paths to markdown objects.") + paths: list[str] = Field(description="List of paths to markdown objects.") class HazardDescriptionResponse(BaseModel): - descriptions: Dict[str, str] = Field( + descriptions: dict[str, str] = Field( description="For each path (key), the description markdown (value)." ) class StaticInformationResponse(BaseModel): - scenario_descriptions: Dict[str, str] = Field( + scenario_descriptions: dict[str, str] = Field( description="For each scenario identifier (key, e.g. 'ssp585', 'rcp8p5'), the description markdown (value)." ) - oed_occupancy_codes: Dict[int, str] = Field( + oed_occupancy_codes: dict[int, str] = Field( description="OED occupancy codes: mapping of OED Code (integer) to OED Label." ) @@ -291,8 +292,8 @@ def _supported_hazard_type_names() -> set[str]: class HazardDataRequestItem(BaseModel): - longitudes: List[float] - latitudes: List[float] + longitudes: list[float] + latitudes: list[float] request_item_id: str hazard_type: str | None = Field( default=None, @@ -301,14 +302,14 @@ class HazardDataRequestItem(BaseModel): validation_alias=AliasChoices("hazard_type", "event_type"), ) indicator_id: str - indicator_model_gcm: Optional[str] = "" - path: Optional[str] = None + indicator_model_gcm: str | None = "" + path: str | None = None scenario: str # e.g. rcp8p5 year: int @field_validator("hazard_type") @classmethod - def validate_hazard_type(cls, hazard_type: Optional[str]) -> Optional[str]: + def validate_hazard_type(cls, hazard_type: str | None) -> str | None: if hazard_type is None: return None supported_hazard_types = _supported_hazard_type_names() @@ -321,7 +322,7 @@ def validate_hazard_type(cls, hazard_type: Optional[str]) -> Optional[str]: @computed_field(description="Deprecated: same as hazard_type") # type: ignore[misc] @property - def event_type(self) -> Optional[str]: + def event_type(self) -> str | None: """Deprecated: use hazard_type instead.""" return self.hazard_type @@ -348,13 +349,13 @@ class HazardDataRequest(BaseHazardRequest): interpolation: str = "floor" interpolate_years: bool = True - provider_max_requests: Dict[str, int] = Field( + provider_max_requests: dict[str, int] = Field( {}, description="The maximum permitted number of \ requests to external providers. This setting is intended in particular for paid-for data. The key \ is the provider ID and the value is the maximum permitted requests.", ) - items: List[HazardDataRequestItem] + items: list[HazardDataRequestItem] model_config = { "json_schema_extra": { @@ -393,7 +394,7 @@ class HazardDataRequest(BaseHazardRequest): class HazardDataResponseItem(BaseModel): - intensity_curve_set: List[IntensityCurve] + intensity_curve_set: list[IntensityCurve] request_item_id: str hazard_type: str | None indicator_id: str @@ -414,4 +415,4 @@ def model(self) -> str: class HazardDataResponse(BaseModel): - items: List[HazardDataResponseItem] + items: list[HazardDataResponseItem] diff --git a/src/physrisk/api/v1/hazard_image.py b/src/physrisk/api/v1/hazard_image.py index 0f721c4d..70ba1b74 100644 --- a/src/physrisk/api/v1/hazard_image.py +++ b/src/physrisk/api/v1/hazard_image.py @@ -1,4 +1,4 @@ -from typing import Any, List, Literal, NamedTuple, Optional +from typing import Any, Literal, NamedTuple from pydantic import BaseModel, Field @@ -10,8 +10,6 @@ class TileNotAvailableError(KeyError): the zoom level requested is not present. """ - pass - class Tile(NamedTuple): x: int @@ -23,7 +21,7 @@ class HazardImageInfoRequest(BaseHazardRequest): resource: str = Field(description="Hazard resource path (unique identifier).") scenario_id: str year: int - tile_size: Optional[Literal[256, 512]] = Field( + tile_size: Literal[256, 512] | None = Field( None, description="Tile size in pixels that max_zoom should be reported for. " "Defaults to 512. Only resources backed by JBA currently support 256.", @@ -31,10 +29,10 @@ class HazardImageInfoRequest(BaseHazardRequest): class HazardImageInfoResponse(BaseHazardRequest): - all_index_values: List[Any] = Field( + all_index_values: list[Any] = Field( [], description="The coordinate values of the index dimension." ) - available_index_values: List[Any] = Field( + available_index_values: list[Any] = Field( [], description="The coordinate values of the index dimension for which maps are available.", ) @@ -42,7 +40,7 @@ class HazardImageInfoResponse(BaseHazardRequest): "index", description="The name of the index dimension." ) index_units: str = Field("", description="The units of the index dimension.") - max_zoom: Optional[int] = Field( + max_zoom: int | None = Field( default=None, description="The maximum zoom level for which tiles are available.", ) @@ -55,12 +53,12 @@ class HazardImageRequest(BaseHazardRequest): resource: str = Field(description="Hazard resource path (unique identifier).") scenario_id: str year: int - colormap: Optional[str] = Field("heating") - format: Optional[str] = Field("PNG") - min_value: Optional[float] - max_value: Optional[float] - tile: Optional[Tile] - index_value: Optional[Any] = Field( + colormap: str | None = Field("heating") + format: str | None = Field("PNG") + min_value: float | None + max_value: float | None + tile: Tile | None + index_value: Any | None = Field( None, description="(Non-spatial) index of the array to view." ) scaling: Literal["linear", "log"] | None = Field( @@ -68,7 +66,7 @@ class HazardImageRequest(BaseHazardRequest): description="Value-to-colour scaling: 'linear' or 'log'. " "'log' requires min_value > 0.", ) - tile_size: Optional[Literal[256, 512]] = Field( + tile_size: Literal[256, 512] | None = Field( None, description="Output tile size in pixels. Defaults to 512. Only resources " "backed by JBA currently support 256 (fewer upstream tile fetches, faster).", diff --git a/src/physrisk/api/v1/impact_req_resp.py b/src/physrisk/api/v1/impact_req_resp.py index 88fe1c1f..d5fb1dd9 100644 --- a/src/physrisk/api/v1/impact_req_resp.py +++ b/src/physrisk/api/v1/impact_req_resp.py @@ -1,4 +1,5 @@ -from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Literal +from collections.abc import Sequence +from typing import Any, Literal, NamedTuple from pydantic import BaseModel, ConfigDict, Field @@ -12,11 +13,11 @@ class CalcSettings(BaseModel): - hazard_interp: Optional[str] = Field( + hazard_interp: str | None = Field( default=None, # previously "floor", description="Method used for interpolation of hazards: 'floor' or 'bilinear'.", ) - hazard_scope: Optional[str] = Field( + hazard_scope: str | None = Field( default=None, description="Comma separated list of hazards to include in analysis.", examples=["RiverineInundation,Wind", "Hail,Fire"], @@ -25,7 +26,7 @@ class CalcSettings(BaseModel): default=True, description="If True, the hazard model interpolates the available years to provide data for the requested years. ", ) - hazard_scope_by_indicator: Optional[Dict[str, List[str] | None]] = Field( + hazard_scope_by_indicator: dict[str, list[str] | None] | None = Field( default=None, description="Dictionary of hazards and corresponding indicator ids to include in analysis.", ) @@ -56,14 +57,14 @@ class AssetMeasuresSpecification(BaseModel): * ``"10y"`` … ``"1000y"`` — loss at the given return period """ - measure_ids: List[str] = Field( + measure_ids: list[str] = Field( default_factory=lambda: ["mean"], description=( "Base measure identifiers to compute. Standard values: 'mean', 'semi_std_dev', " "'10y', '20y', '50y', '100y', '200y', '500y', '1000y'." ), ) - quantity_types: List[str] = Field( + quantity_types: list[str] = Field( default_factory=lambda: ["damage"], description=( "Quantity types to include. Valid values: 'damage', 'disruption/revenue', " @@ -89,7 +90,7 @@ class AssetImpactRequest(BaseModel): include_calc_details: bool = Field( True, description="If true, include impact calculation details." ) - measures_specification: Optional[AssetMeasuresSpecification] = Field( + measures_specification: AssetMeasuresSpecification | None = Field( None, description=( "If set, append per-asset financial impact measures to " @@ -102,16 +103,16 @@ class AssetImpactRequest(BaseModel): "", description="Identifier for 'use case' used in the risk measures calculation.", ) - provider_max_requests: Dict[str, int] = Field( + provider_max_requests: dict[str, int] = Field( {}, description="The maximum permitted number of requests \ to external providers. This setting is intended in particular for paid-for data. The key is the provider \ ID and the value is the maximum permitted requests.", ) - scenarios: Optional[Sequence[str]] = Field( + scenarios: Sequence[str] | None = Field( [], description="Name of scenarios ('rcp8p5')" ) - years: Optional[Sequence[int]] = Field( + years: Sequence[int] | None = Field( [], description="""Projection year (2030, 2050, 2080). Any year before 2030, e.g. 1980, is treated as historical.""", @@ -148,24 +149,24 @@ class RiskScoreValue(BaseModel): description="Full description of value, \ e.g. 'very low impact: very unlikely to cause asset devaluation'.", ) - lower_bound: Optional[List[Any]] = Field( + lower_bound: list[Any] | None = Field( default=None, description="Lower bound(s) of the measure(s) from which the score is derived", ) - upper_bound: Optional[List[Any]] = Field( + upper_bound: list[Any] | None = Field( default=None, description="Upper bound(s) of the measure(s) from which the score is derived", ) class ScoreBasedRiskMeasureDefinition(BaseModel): - hazard_types: List[str] = Field( + hazard_types: list[str] = Field( [], description="Defines the hazards that the measure is used for." ) - values: List[RiskScoreValue] = Field( + values: list[RiskScoreValue] = Field( [], description="Defines the set of values that the score can take." ) - underlying_measures: List[RiskMeasureDefinition] = Field( + underlying_measures: list[RiskMeasureDefinition] = Field( [], description="Defines the underlying risk measures from which the scores are inferred.", ) @@ -190,8 +191,8 @@ class RiskMeasureKey(BaseModel): scenario_id: str year: str measure_id: str - hazard_indicator_id: Optional[str] = None - aggregation_id: Optional[str] = Field( + hazard_indicator_id: str | None = None + aggregation_id: str | None = Field( None, description="Identifier of the sub-portfolio (asset 'aggregation_id') this " "portfolio-level measure belongs to; omitted for the pooled portfolio total.", @@ -204,7 +205,7 @@ class ScoreBasedRiskMeasure(BaseModel): key: RiskMeasureKey score: int = Field([0], description="Identifier for the risk measure.") measure_0: float - measure_1: Optional[float] = Field( + measure_1: float | None = Field( None, description="Underlying measures for case where there are multiple underlying measures.", ) @@ -217,11 +218,11 @@ class ScoreBasedRiskMeasuresForAssets(BaseModel): """ key: RiskMeasureKey - scores: Optional[list[int]] = Field( + scores: list[int] | None = Field( [0], description="Identifier for the risk measure." ) - measures_0: Optional[list[float]] - measures_1: Optional[List[float]] = Field( + measures_0: list[float] | None + measures_1: list[float] | None = Field( [], description="Underlying measures for case where there are multiple underlying measures.", ) @@ -238,8 +239,8 @@ class RiskMeasuresForAssets(BaseModel): class ScoreBasedRiskMeasureSetDefinition(BaseModel): measure_set_id: str # for each hazard type, gives the measure ID used to calculate the measure for each asset: - asset_measure_ids_for_hazard: Dict[str, List[str]] - score_definitions: Dict[str, ScoreBasedRiskMeasureDefinition] + asset_measure_ids_for_hazard: dict[str, list[str]] + score_definitions: dict[str, ScoreBasedRiskMeasureDefinition] # where drill-down by hazard indicator ID is relevant, give the measure IDs for each # (hazard type, indicator ID) combination : asset_measure_ids_for_hazard_drilldown: dict[str, dict[str, list[str]]] = Field( @@ -250,7 +251,7 @@ class ScoreBasedRiskMeasureSetDefinition(BaseModel): class RiskMeasures(BaseModel): """Risk measures""" - measures_for_assets: List[ + measures_for_assets: list[ RiskMeasuresForAssets | ScoreBasedRiskMeasuresForAssets ] = Field( default=[], @@ -259,32 +260,32 @@ class RiskMeasures(BaseModel): "score-based risk measures, where a score (often something operationally meaningful) is inferred from " "one or more underlying measures.", ) - measures_for_portfolio: List[ScoreBasedRiskMeasure] = Field( + measures_for_portfolio: list[ScoreBasedRiskMeasure] = Field( default=[], description="Score-based risk measures for the portfolio.", ) score_based_measure_set_defn: ScoreBasedRiskMeasureSetDefinition - measures_definitions: Optional[List[RiskMeasureDefinition]] = Field( + measures_definitions: list[RiskMeasureDefinition] | None = Field( [], description="Definitions of the risk measures." ) - scenarios: List[ScenarioYears] - asset_ids: List[str] + scenarios: list[ScenarioYears] + asset_ids: list[str] class CalculationDetails(BaseModel): """Details of a calculation. hazard_exceedance, hazard_distribution and vulnerability_distribution are only set in the case of an acute hazard calculation.""" - hazard_exceedance: Optional[ExceedanceCurve] = Field( + hazard_exceedance: ExceedanceCurve | None = Field( None, description="Hazard data as exceedance curve." ) - hazard_distribution: Optional[Distribution] = Field( + hazard_distribution: Distribution | None = Field( None, description="Hazard data as probability distribution." ) - vulnerability_distribution: Optional[VulnerabilityDistrib] = Field( + vulnerability_distribution: VulnerabilityDistrib | None = Field( None, description="Vulnerability distribution." ) - hazard_path: List[str] = Field( + hazard_path: list[str] = Field( ["unknown"], description="Path to the hazard indicator data source." ) hazard_units: str = Field(["unknown"], description="Hazard indicator units.") @@ -297,7 +298,7 @@ class ImpactKey(BaseModel): class PortfolioImpactKey(ImpactKey): - aggregation_id: Optional[str] = Field( + aggregation_id: str | None = Field( None, description="Identifier of the sub-portfolio (asset 'aggregation_id') this " "result belongs to; omitted for the pooled portfolio total.", @@ -317,22 +318,22 @@ class AssetSingleImpact(BaseModel): examples=["damage", "disruption", "disruption/revenue", "disruption/costs"], ) hazard_indicator_id: str = Field("", description="The ID of the hazard indicator.") - impact_distribution: Optional[Distribution] = Field( + impact_distribution: Distribution | None = Field( None, description="Impact as probability distribution." ) - impact_exceedance: Optional[ExceedanceCurve] = Field( + impact_exceedance: ExceedanceCurve | None = Field( None, description="Impact as exceedance curve." ) - impact_mean: Optional[float] - impact_std_deviation: Optional[float] = Field( + impact_mean: float | None + impact_std_deviation: float | None = Field( default=None, description="""Impact standard deviation.""", ) - impact_semi_std_deviation: Optional[float] = Field( + impact_semi_std_deviation: float | None = Field( default=None, description="""Impact semi standard deviation: dispersion above the mean.""", ) - calc_details: Optional[CalculationDetails] = Field( + calc_details: CalculationDetails | None = Field( None, description="""Details of impact calculation for acute hazard calculations.""", ) @@ -340,7 +341,7 @@ class AssetSingleImpact(BaseModel): class PortfolioImpact(BaseModel): key: PortfolioImpactKey - impact_exceedance: Optional[ExceedanceCurve] = Field( + impact_exceedance: ExceedanceCurve | None = Field( None, description="Impact as exceedance curve." ) impact_type: str = Field( @@ -350,12 +351,12 @@ class PortfolioImpact(BaseModel): "attributable to the asset.", examples=["damage", "disruption", "disruption/revenue", "disruption/costs"], ) - impact_mean: Optional[float] - impact_std_deviation: Optional[float] = Field( + impact_mean: float | None + impact_std_deviation: float | None = Field( default=None, description="""Impact standard deviation.""", ) - impact_semi_std_deviation: Optional[float] = Field( + impact_semi_std_deviation: float | None = Field( default=None, description="""Impact semi standard deviation: dispersion above the mean.""", ) @@ -364,12 +365,12 @@ class PortfolioImpact(BaseModel): class AssetLevelImpact(BaseModel): """Impact at asset level. Each asset can have impacts for multiple hazard types.""" - asset_id: Optional[str] = Field( + asset_id: str | None = Field( None, description="""Asset identifier; will appear if provided in the request otherwise order of assets in response is identical to order of assets in request.""", ) - impacts: List[AssetSingleImpact] = Field( + impacts: list[AssetSingleImpact] = Field( [], description="Impacts for each hazard type." ) @@ -377,7 +378,7 @@ class AssetLevelImpact(BaseModel): class AssetImpactResponse(BaseModel): """Response to impact request.""" - asset_impacts: Optional[List[AssetLevelImpact]] = Field( + asset_impacts: list[AssetLevelImpact] | None = Field( None, description="Impacts for each asset and hazard type combination. Impacts are damage or disruption. " "Damage is specified as a fraction of asset total insurable value. Disruption, to revenue generation " @@ -385,7 +386,7 @@ class AssetImpactResponse(BaseModel): "provided as probability distributions. Note the effects of damage from downtime and mitigants are " "not included; these are only taken into account in portfolio_impacts.", ) - portfolio_impacts: Optional[List[PortfolioImpact]] = Field( + portfolio_impacts: list[PortfolioImpact] | None = Field( None, description="Impacts for the portfolio, aggregated over assets and provided both per hazard type and " "aggregated over hazard types. Impacts are damage or disruption. Damage is specified as a fraction of " @@ -393,7 +394,7 @@ class AssetImpactResponse(BaseModel): "fraction of revenue attributable to the asset. Impacts are provided as probability distributions. " "Downtime from damage and (optionally) mitigants (insurance and state support) are included.", ) - risk_measures: Optional[RiskMeasures] = Field( + risk_measures: RiskMeasures | None = Field( None, description="Risk measures: both score-based and non-score based.", ) diff --git a/src/physrisk/api/v1/scoring_schemes.py b/src/physrisk/api/v1/scoring_schemes.py index 87e394a3..7fa2b860 100644 --- a/src/physrisk/api/v1/scoring_schemes.py +++ b/src/physrisk/api/v1/scoring_schemes.py @@ -15,9 +15,7 @@ class Category(int, Enum): def map_to_original_category(score: int) -> int: if score in [Category.ERROR, Category.NO_DATA, Category.NO_VULNERABILITY]: return int(OriginalCategory.NODATA) - elif score == Category.VERY_LOW: - return int(OriginalCategory.LOW) - elif score == Category.LOW: + elif score == Category.VERY_LOW or score == Category.LOW: return int(OriginalCategory.LOW) elif score == Category.MEDIUM: return int(OriginalCategory.MEDIUM) diff --git a/src/physrisk/container.py b/src/physrisk/container.py index 63986d65..1e8d3614 100644 --- a/src/physrisk/container.py +++ b/src/physrisk/container.py @@ -1,15 +1,15 @@ -from typing import Dict, MutableMapping, Optional +from collections.abc import MutableMapping from dependency_injector import containers, providers from physrisk.data.hazard_data_provider import HazardResourceSelector +from physrisk.data.image_creator import ImageCreator +from physrisk.data.inventory import EmbeddedInventory, Inventory +from physrisk.data.inventory_reader import InventoryReader from physrisk.data.scenario_year_resolution import ( InterpolatedYearResolver, resolve_exact_year, ) -from physrisk.data.image_creator import ImageCreator -from physrisk.data.inventory import EmbeddedInventory, Inventory -from physrisk.data.inventory_reader import InventoryReader from physrisk.data.zarr_reader import ZarrReader from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.hazard_models.credentials_provider import ( @@ -55,8 +55,8 @@ def __init__( credentials: CredentialsProvider, inventory: Inventory, resource_selector: HazardResourceSelector, - store: Optional[MutableMapping] = None, - reader: Optional[ZarrReader] = None, + store: MutableMapping | None = None, + reader: ZarrReader | None = None, default_interpolation: str = "floor", zarr_max_workers: int = 32, ): @@ -76,8 +76,8 @@ def __init__( def hazard_model( self, - interpolation: Optional[str] = None, - provider_max_requests: Dict[str, int] = {}, + interpolation: str | None = None, + provider_max_requests: dict[str, int] = {}, interpolate_years: bool = True, ): # this is done to allow interpolation etc to be set dynamically, diff --git a/src/physrisk/data/geocode.py b/src/physrisk/data/geocode.py index 5cce3d58..267b6402 100644 --- a/src/physrisk/data/geocode.py +++ b/src/physrisk/data/geocode.py @@ -1,6 +1,6 @@ import threading +from collections.abc import Sequence from importlib.resources import files -from typing import Dict, Optional, Sequence, Union import geopandas as gpd import pandas as pd @@ -36,7 +36,7 @@ def __init__(self) -> None: "Continent" ].to_dict() ) - self.subunit_mapping: Dict[str, Dict[str, str]] = { + self.subunit_mapping: dict[str, dict[str, str]] = { "ES": {"Canary Islands": "IC"}, "-99": {"Crimea": "RU", "Cyprus No Mans Area": "CY", "Somaliland": "SO"}, "PT": {"Madeira": "PT_M"}, @@ -77,7 +77,7 @@ def get_continent(self, country_code_a2: str): @staticmethod def get_continent_and_country_from_code_iso_3166( - country_codes: Optional[Sequence[Union[str, int]]] = None, + country_codes: Sequence[str | int] | None = None, ) -> pd.DataFrame: path = files(physrisk.data.ne_10m_admin_0_map_subunits).joinpath( "country_codes.tsv" diff --git a/src/physrisk/data/geocode_google.py b/src/physrisk/data/geocode_google.py index 57ac5fb9..f2a2598e 100644 --- a/src/physrisk/data/geocode_google.py +++ b/src/physrisk/data/geocode_google.py @@ -16,9 +16,10 @@ from __future__ import annotations import asyncio +from collections.abc import Sequence from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Union from urllib.parse import quote import aiohttp @@ -112,12 +113,12 @@ def is_building_level(self) -> bool: class GeocodeResult: latitude: float longitude: float - building_shape: Optional[BuildingShape] + building_shape: BuildingShape | None formatted_address: str place_id: str granularity: Granularity structure_type: StructureType - types: List[str] = field(default_factory=list) + types: list[str] = field(default_factory=list) candidate_count: int = field(default=1) @@ -152,18 +153,18 @@ def __init__( self, api_key: str, *, - proxy: Optional[str] = None, - session: Optional[aiohttp.ClientSession] = None, + proxy: str | None = None, + session: aiohttp.ClientSession | None = None, language_code: str = "en", - region_code: Optional[str] = None, + region_code: str | None = None, fetch_building_shape: bool = False, max_concurrency: int = 50, - requests_per_second: Optional[float] = 25.0, + requests_per_second: float | None = 25.0, ) -> None: self._api_key = api_key self._proxy = proxy self._external_session = session - self._session: Optional[aiohttp.ClientSession] = session + self._session: aiohttp.ClientSession | None = session self._language_code = language_code self._region_code = region_code self._fetch_building_shape = fetch_building_shape @@ -172,7 +173,7 @@ def __init__( _RateLimiter(requests_per_second) if requests_per_second else None ) - async def __aenter__(self) -> "GoogleGeocoder": + async def __aenter__(self) -> GoogleGeocoder: if self._external_session is None: self._session = aiohttp.ClientSession() return self @@ -183,8 +184,8 @@ async def __aexit__(self, *_: object) -> None: self._session = None async def geocode( - self, address: str, *, region_code: Optional[str] = None - ) -> Optional[GeocodeResult]: + self, address: str, *, region_code: str | None = None + ) -> GeocodeResult | None: """Resolve one address; calls geocode/address and destinations concurrently. Args: @@ -217,7 +218,7 @@ async def geocode( geocode_data = address_results[0] - building_shape: Optional[BuildingShape] = None + building_shape: BuildingShape | None = None structure_type = StructureType.UNSPECIFIED if destinations_data: primary = destinations_data[0].get("primary", {}) @@ -241,8 +242,8 @@ async def geocode_many( self, addresses: Sequence[str], *, - region_codes: Optional[Sequence[Optional[str]]] = None, - ) -> List[Optional[GeocodeResult]]: + region_codes: Sequence[str | None] | None = None, + ) -> list[GeocodeResult | None]: """Geocode a batch of addresses, bounded by ``max_concurrency``. Args: @@ -257,7 +258,7 @@ async def geocode_many( raise ValueError( f"region_codes length ({len(region_codes)}) must match addresses length ({len(addresses)})" ) - codes: Sequence[Optional[str]] = ( + codes: Sequence[str | None] = ( region_codes if region_codes is not None else [None] * len(addresses) ) return list( @@ -270,18 +271,18 @@ async def geocode_many( # Private helpers # ------------------------------------------------------------------ - def _common_headers(self, field_mask: str) -> Dict[str, str]: + def _common_headers(self, field_mask: str) -> dict[str, str]: return { "X-Goog-Api-Key": self._api_key, "X-Goog-FieldMask": field_mask, } async def _fetch_geocode_address( - self, address: str, region_code: Optional[str] - ) -> Optional[List[Dict[str, Any]]]: + self, address: str, region_code: str | None + ) -> list[dict[str, Any]] | None: """Call geocode/address → returns the full results list or None.""" assert self._session is not None - params: Dict[str, str] = {"languageCode": self._language_code} + params: dict[str, str] = {"languageCode": self._language_code} if region_code: params["regionCode"] = region_code @@ -298,11 +299,11 @@ async def _fetch_geocode_address( return data.get("results") or None async def _fetch_destinations( - self, address: str, region_code: Optional[str] - ) -> Optional[List[Dict[str, Any]]]: + self, address: str, region_code: str | None + ) -> list[dict[str, Any]] | None: """Call destinations → returns the destinations list or None.""" assert self._session is not None - body: Dict[str, Any] = { + body: dict[str, Any] = { "addressQuery": {"addressQuery": address}, "languageCode": self._language_code, } @@ -330,21 +331,21 @@ async def _fetch_destinations( # ------------------------------------------------------------------ -def _parse_granularity(value: Optional[str]) -> Granularity: +def _parse_granularity(value: str | None) -> Granularity: try: return Granularity(value) if value else Granularity.UNSPECIFIED except ValueError: return Granularity.UNSPECIFIED -def _parse_structure_type(value: Optional[str]) -> StructureType: +def _parse_structure_type(value: str | None) -> StructureType: try: return StructureType(value) if value else StructureType.UNSPECIFIED except ValueError: return StructureType.UNSPECIFIED -def _parse_geojson(geojson: Optional[Dict[str, Any]]) -> Optional[BuildingShape]: +def _parse_geojson(geojson: dict[str, Any] | None) -> BuildingShape | None: """Convert a GeoJSON dict returned by ``displayPolygon`` to a shapely geometry.""" if not geojson: return None diff --git a/src/physrisk/data/hazard_data_provider.py b/src/physrisk/data/hazard_data_provider.py index bb98af50..c6d5e496 100644 --- a/src/physrisk/data/hazard_data_provider.py +++ b/src/physrisk/data/hazard_data_provider.py @@ -1,13 +1,7 @@ import asyncio import sys +from collections.abc import MutableMapping, Sequence from dataclasses import dataclass -from typing import ( - Dict, - MutableMapping, - Optional, - Sequence, - Type, -) import numpy as np from shapely import Point @@ -30,7 +24,7 @@ class HazardDataHint: A hazard resource path can be specified which uniquely defines the hazard resource; otherwise the resource is inferred from the indicator_id.""" - path: Optional[str] = None + path: str | None = None # consider adding: indicator_model_gcm: str def group_key(self): @@ -115,13 +109,13 @@ class CascadingHazardDataProvider: def __init__( self, - hazard_type: Type[Hazard], + hazard_type: type[Hazard], resource_selector: HazardResourceSelector, scenario_year_resolver: ScenarioYearResolver, *, - store: Optional[MutableMapping] = None, - zarr_reader: Optional[ZarrReader] = None, - interpolation: Optional[str] = "floor", + store: MutableMapping | None = None, + zarr_reader: ZarrReader | None = None, + interpolation: str | None = "floor", ): """Provides hazard data. @@ -154,9 +148,9 @@ async def get_data( indicator_id: str, scenarios: Sequence[str], years: Sequence[int], - hint: Optional[HazardDataHint] = None, - buffer: Optional[int] = None, - ) -> Dict[ScenarioYear, ScenarioYearResult]: + hint: HazardDataHint | None = None, + buffer: int | None = None, + ) -> dict[ScenarioYear, ScenarioYearResult]: """Returns data for set of latitude and longitudes. Args: @@ -317,14 +311,14 @@ async def get_scenarios_and_years( latitudes: np.ndarray, indicator_id: str, resource: HazardResource, - weighted_sums_by_request: Dict[ScenarioYear, WeightedSum], - buffer: Optional[int], + weighted_sums_by_request: dict[ScenarioYear, WeightedSum], + buffer: int | None, ) -> dict[ScenarioYear, ScenarioYearResult]: """Get data for all scenarios and years using just a single HazardResource as the source. The importance of this is that interpolation of years is assumed to be feasible within the same resource as this is a single model (with consistent meaning of the values). """ - results_by_request: Dict[ScenarioYear, ScenarioYearResult] = {} + results_by_request: dict[ScenarioYear, ScenarioYearResult] = {} expected_units = resource.units source_scenario_years = { source_scenario_year @@ -333,7 +327,7 @@ async def get_scenarios_and_years( } if len(source_scenario_years) == 0: return {} - results_by_source: Dict[ScenarioYear, ScenarioYearResult] = {} + results_by_source: dict[ScenarioYear, ScenarioYearResult] = {} try: # Any errors should propagate up. read_results = await asyncio.gather( @@ -423,7 +417,7 @@ async def get_single_item( source_scenario_year: ScenarioYear, latitudes: np.ndarray, longitudes: np.ndarray, - buffer: Optional[int], + buffer: int | None, concrete_path: str, ): in_bounds_mask = None diff --git a/src/physrisk/data/image_creator.py b/src/physrisk/data/image_creator.py index 7896666c..eda60ce8 100644 --- a/src/physrisk/data/image_creator.py +++ b/src/physrisk/data/image_creator.py @@ -1,16 +1,17 @@ import io import logging +from collections.abc import Callable, Sequence from functools import lru_cache from pathlib import PurePosixPath -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any import numpy as np -import PIL.Image as Image +from PIL import Image from physrisk.api.v1.hazard_image import TileNotAvailableError from physrisk.data import colormap_provider -from physrisk.data.scenario_year_resolution import ScenarioYear, ScenarioYearResolver from physrisk.data.inventory import Inventory +from physrisk.data.scenario_year_resolution import ScenarioYear, ScenarioYearResolver from physrisk.data.zarr_reader import ZarrReader from physrisk.kernel.hazard_model import HazardImageCreator, Tile from physrisk.kernel.hazards import HazardKind, hazard_class @@ -40,12 +41,12 @@ def create_image( year: int, format="PNG", colormap: str = "heating", - tile: Optional[Tile] = None, - min_value: Optional[float] = None, - max_value: Optional[float] = None, - index_value: Optional[Union[str, float]] = None, + tile: Tile | None = None, + min_value: float | None = None, + max_value: float | None = None, + index_value: str | float | None = None, scaling: str = "linear", - tile_size: Optional[int] = None, + tile_size: int | None = None, ): if tile_size is not None and tile_size != 512: raise ValueError( @@ -58,7 +59,7 @@ def create_image( ) if weighted_sum is None: raise KeyError(f"Unsupported scenario/year: {scenario}/{year}") - path_weights: Dict[str, float] = {} + path_weights: dict[str, float] = {} for source, weight in weighted_sum.weights: path = resource.map_path_for_scenario_year( source.scenario, @@ -96,8 +97,8 @@ def get_info( resource_id: str, scenario: str, year: int, - tile_size: Optional[int] = None, - ) -> Tuple[Sequence[Any], Sequence[Any], str, str, Optional[int], int]: + tile_size: int | None = None, + ) -> tuple[Sequence[Any], Sequence[Any], str, str, int | None, int]: if tile_size is not None and tile_size != 512: raise ValueError( f"tile_size={tile_size} is not supported; only 512 is available for this resource." @@ -165,8 +166,8 @@ def to_file( path: str, format="PNG", colormap: str = "heating", - min_value: Optional[float] = None, - max_value: Optional[float] = None, + min_value: float | None = None, + max_value: float | None = None, ): """Create image for path specified and save as file. @@ -185,12 +186,12 @@ def to_file( def _to_image( self, - path_weights: Dict[str, float], + path_weights: dict[str, float], colormap: str = "heating", - tile: Optional[Tile] = None, - index_value: Optional[Union[str, float, int]] = None, - min_value: Optional[float] = None, - max_value: Optional[float] = None, + tile: Tile | None = None, + index_value: str | float | None = None, + min_value: float | None = None, + max_value: float | None = None, scaling: str = "linear", ) -> Image.Image: """Get image for path specified as array of bytes.""" @@ -264,11 +265,11 @@ def image_from_packed_rgba(rgba: np.ndarray) -> Image.Image: def to_rgba( data: np.ndarray, - get_colors: Callable[[int], List[int]], - min_value: Optional[float] = None, - max_value: Optional[float] = None, - nodata_lower: Optional[float] = None, - nodata_upper: Optional[float] = None, + get_colors: Callable[[int], list[int]], + min_value: float | None = None, + max_value: float | None = None, + nodata_lower: float | None = None, + nodata_upper: float | None = None, nodata_bin_transparent: bool = False, min_bin_transparent: bool = False, scaling: str = "linear", diff --git a/src/physrisk/data/inventory.py b/src/physrisk/data/inventory.py index 54dc511f..18558ada 100644 --- a/src/physrisk/data/inventory.py +++ b/src/physrisk/data/inventory.py @@ -1,15 +1,14 @@ -# flake8: noqa: E501 import hashlib import importlib.resources import json -from collections import defaultdict import re -from typing import DefaultDict, Dict, Iterable, List, Tuple +from collections import defaultdict +from collections.abc import Iterable from pydantic import TypeAdapter -import physrisk.data.colormap_provider as colormap_provider import physrisk.data.static.hazard +from physrisk.data import colormap_provider from physrisk.data.inventory_reader import HazardModels from physrisk.kernel.hazards import ( CoastalInundation, @@ -37,9 +36,9 @@ def __init__(self, hazard_resources: Iterable[HazardResource]): Args: hazard_resources (Iterable[HazardResource]): list of resources """ - self.resources: Dict[str, HazardResource] = {} - self.resources_by_type_id: DefaultDict[ - Tuple[str, str], List[HazardResource] + self.resources: dict[str, HazardResource] = {} + self.resources_by_type_id: defaultdict[ + tuple[str, str], list[HazardResource] ] = defaultdict(list) for resource in hazard_resources: self.resources[resource.key()] = resource @@ -118,7 +117,7 @@ def api_based_resources(self) -> Iterable[HazardResource]: indicator_id="flood_sop", indicator_model_gcm="", display_name="Standard of protection (JBA Risk Management)", - description="Riverine standard of protection (defended return period), commercial flood model from JBA Risk Management (https://www.jbarisk.com/). An API key is needed to display data.", # noqa: E501 + description="Riverine standard of protection (defended return period), commercial flood model from JBA Risk Management (https://www.jbarisk.com/). An API key is needed to display data.", scenarios=[ ScenarioYears(id="historical", years=[2020]), ], @@ -167,7 +166,7 @@ def base36encode(number, alphabet="0123456789abcdefghijklmnopqrstuvwxyz"): return base36 -def expand(resources: List[HazardResource]) -> List[HazardResource]: +def expand(resources: list[HazardResource]) -> list[HazardResource]: expanded_models = [e for model in resources for e in model.expand()] # we populate map_id hashes programmatically for model in expanded_models: @@ -187,6 +186,6 @@ def expand(resources: List[HazardResource]) -> List[HazardResource]: for period, test_period in zip(scenario.periods, test_periods): if period.map_id != test_period.map_id: raise Exception( - f"validation error: hash {period.map_id} different to specified hash {test_period.map_id}" # noqa: E501 + f"validation error: hash {period.map_id} different to specified hash {test_period.map_id}" ) return expanded_models diff --git a/src/physrisk/data/inventory_reader.py b/src/physrisk/data/inventory_reader.py index d0dbddd6..736dd2a9 100644 --- a/src/physrisk/data/inventory_reader.py +++ b/src/physrisk/data/inventory_reader.py @@ -1,6 +1,6 @@ import json +from collections.abc import Callable, Iterable from pathlib import PurePosixPath -from typing import Callable, Dict, Iterable, List, Optional import s3fs from fsspec import AbstractFileSystem @@ -12,7 +12,7 @@ class HazardModels(BaseModel): - resources: List[HazardResource] + resources: list[HazardResource] class InventoryReader: @@ -24,9 +24,9 @@ class InventoryReader: def __init__( self, *, - get_env: Callable[[str, Optional[str]], str] = get_env, - fs: Optional[AbstractFileSystem] = None, - base_path: Optional[AbstractFileSystem] = None, + get_env: Callable[[str, str | None], str] = get_env, + fs: AbstractFileSystem | None = None, + base_path: AbstractFileSystem | None = None, ): """Class to read and update inventory stored in S3 or supplied AbstractFileSystem. @@ -47,7 +47,7 @@ def __init__( self._base_path = bucket if base_path is None else base_path self._fs = fs - def read(self, path: str) -> List[HazardResource]: + def read(self, path: str) -> list[HazardResource]: """Read inventory at path provided and return HazardModels.""" if not self._fs.exists(self._full_path(path)): return [] @@ -57,9 +57,9 @@ def read(self, path: str) -> List[HazardResource]: ) return models - def read_description_markdown(self, paths: List[str]) -> Dict[str, str]: + def read_description_markdown(self, paths: list[str]) -> dict[str, str]: """Read description markdown at path provided.""" - md: Dict[str, str] = {} + md: dict[str, str] = {} for path in paths: try: with self._fs.open(self._full_path(path), "r", encoding="utf-8") as f: diff --git a/src/physrisk/data/pregenerated_hazard_model.py b/src/physrisk/data/pregenerated_hazard_model.py index adb9a0e6..fd3bad5f 100644 --- a/src/physrisk/data/pregenerated_hazard_model.py +++ b/src/physrisk/data/pregenerated_hazard_model.py @@ -2,7 +2,7 @@ import concurrent.futures import logging from collections import defaultdict -from typing import Dict, List, Mapping, Optional, Sequence, Tuple, Type +from collections.abc import Mapping, Sequence import numpy as np @@ -45,10 +45,10 @@ class PregeneratedHazardModel(HazardModel): def __init__( self, - hazard_data_providers: Dict[Type[Hazard], HazardDataProvider], + hazard_data_providers: dict[type[Hazard], HazardDataProvider], zarr_max_workers: int = 32, - nan_is_zero: Optional[set[tuple[type[Hazard], str]]] = None, - nan_is_no_data: Optional[set[tuple[type[Hazard], str]]] = None, + nan_is_zero: set[tuple[type[Hazard], str]] | None = None, + nan_is_no_data: set[tuple[type[Hazard], str]] | None = None, ): """ Args: @@ -239,12 +239,12 @@ async def _get_batch_data( @staticmethod def _log_response_issues( - responses: Dict[HazardDataRequest, HazardDataResponse], + responses: dict[HazardDataRequest, HazardDataResponse], ) -> None: # in some cases requested data cannot be retrieved, leading to a HazardDataFailedResponse # this may be handled by the vulnerability model or might result in missing results. - grouped_failures: Dict[ - str, List[Tuple[HazardDataRequest, HazardDataFailedResponse]] + grouped_failures: dict[ + str, list[tuple[HazardDataRequest, HazardDataFailedResponse]] ] = defaultdict(list) for req, resp in responses.items(): if isinstance(resp, HazardDataFailedResponse): @@ -274,12 +274,12 @@ def __init__( *, resource_selector: HazardResourceSelector, scenario_year_resolver: ScenarioYearResolver, - reader: Optional[ZarrReader] = None, + reader: ZarrReader | None = None, store=None, interpolation="floor", zarr_max_workers: int = 32, - nan_is_zero: Optional[set[tuple[type[Hazard], str]]] = None, - nan_is_no_data: Optional[set[tuple[type[Hazard], str]]] = None, + nan_is_zero: set[tuple[type[Hazard], str]] | None = None, + nan_is_no_data: set[tuple[type[Hazard], str]] | None = None, ): """Hazard model backed by Zarr arrays. diff --git a/src/physrisk/data/scenario_year_resolution.py b/src/physrisk/data/scenario_year_resolution.py index 4affbbd2..39eec780 100644 --- a/src/physrisk/data/scenario_year_resolution.py +++ b/src/physrisk/data/scenario_year_resolution.py @@ -2,8 +2,8 @@ import re from bisect import bisect_left +from collections.abc import Sequence from dataclasses import dataclass -from typing import Sequence from typing_extensions import Protocol diff --git a/src/physrisk/data/static/oed_occupancy.py b/src/physrisk/data/static/oed_occupancy.py index ea441827..f63367c9 100644 --- a/src/physrisk/data/static/oed_occupancy.py +++ b/src/physrisk/data/static/oed_occupancy.py @@ -1,8 +1,7 @@ # Snapshot of OED occupancy codes and labels from OasisLMF/ODS_OpenExposureData OccupancyValues.csv # Source: https://github.com/OasisLMF/ODS_OpenExposureData/blob/main/OpenExposureData/OccupancyValues.csv -from typing import Dict -OED_OCCUPANCY_CODES: Dict[int, str] = { +OED_OCCUPANCY_CODES: dict[int, str] = { 1000: "Unknown", 1050: "Residential, General residential", 1051: "Residential, Permanent dwelling: single-family", diff --git a/src/physrisk/data/utilities.py b/src/physrisk/data/utilities.py index 6b760c34..a6b41b56 100644 --- a/src/physrisk/data/utilities.py +++ b/src/physrisk/data/utilities.py @@ -1,4 +1,5 @@ from pathlib import Path + from dotenv import load_dotenv diff --git a/src/physrisk/data/zarr_reader.py b/src/physrisk/data/zarr_reader.py index e83ab5a5..c7674752 100644 --- a/src/physrisk/data/zarr_reader.py +++ b/src/physrisk/data/zarr_reader.py @@ -1,21 +1,22 @@ import logging import os +from collections.abc import Callable, MutableMapping, Sequence from pathlib import PurePosixPath -from typing import Any, Callable, List, MutableMapping, Optional, Sequence, Tuple, Union +from typing import Any -from fsspec import FSMap import numpy as np import s3fs import shapely.ops import zarr from affine import Affine +from fsspec import FSMap from pyproj import Transformer -from shapely import MultiPoint, Point, affinity, Polygon +from shapely import MultiPoint, Point, Polygon, affinity logger = logging.getLogger(__name__) -def get_env(key: str, default: Optional[str] = None) -> str: +def get_env(key: str, default: str | None = None) -> str: value = os.environ.get(key) if value is None: if default is not None: @@ -36,9 +37,9 @@ class ZarrReader: def __init__( self, - store: Optional[MutableMapping] = None, - path_provider: Optional[Callable[..., str]] = None, - get_env: Callable[[str, Optional[str]], str] = get_env, + store: MutableMapping | None = None, + path_provider: Callable[..., str] | None = None, + get_env: Callable[[str, str | None], str] = get_env, ): """Create a ZarrReader. @@ -59,7 +60,6 @@ def __init__( self._root = zarr.open(store, mode="r") self._store = store self._path_provider = path_provider - pass def all_data(self, set_id: str): return self._get_array(set_id) @@ -90,9 +90,7 @@ def ls(self, path: str): return self._store.fs.ls(PurePosixPath(self._store.root) / path) @classmethod - def create_s3_zarr_store( - cls, get_env: Callable[[str, Optional[str]], str] = get_env - ): + def create_s3_zarr_store(cls, get_env: Callable[[str, str | None], str] = get_env): access_key = get_env(cls.__access_key, "") secret_key = get_env(cls.__secret_key, "") s3_bucket = get_env(cls.__S3_bucket, "os-climate-physical-risk") @@ -114,8 +112,8 @@ def create_s3_zarr_store( def get_curves( self, set_id: str, - longitudes: Union[np.ndarray, Sequence[float]], - latitudes: Union[np.ndarray, Sequence[float]], + longitudes: np.ndarray | Sequence[float], + latitudes: np.ndarray | Sequence[float], interpolation="floor", ): """Get intensity curve for each latitude and longitude coordinate pair. @@ -186,8 +184,8 @@ def get_curves( def in_bounds( self, set_id: str, - longitudes: Union[np.ndarray, Sequence[float]], - latitudes: Union[np.ndarray, Sequence[float]], + longitudes: np.ndarray | Sequence[float], + latitudes: np.ndarray | Sequence[float], interpolation: str = "floor", ): """Return whether coordinates lie within a hazard array. @@ -232,11 +230,11 @@ def _in_bounds( & (image_coords[1, :] < shape[1]) ) - def get_index_values(self, z: zarr.Array) -> Tuple[List[Any], str]: + def get_index_values(self, z: zarr.Array) -> tuple[list[Any], str]: # if dimensions attribute is present, assume that the first index # is the non-spatial one. index_dim_name = z.attrs.get("dimensions", ["index"])[0] - index_values: List[Any] = z.attrs.get(index_dim_name + "_values", [0]) + index_values: list[Any] = z.attrs.get(index_dim_name + "_values", [0]) index_units = z.attrs.get(index_dim_name + "_units", "default") if index_values is None: index_values = [0] # type: ignore @@ -533,7 +531,7 @@ def _get_coordinates( crs: str, transform: Affine, apply_pixel_center_offset: bool, - shape: Optional[Tuple[int, int, int]] = None, + shape: tuple[int, int, int] | None = None, ): """Transform geographic coordinates into fractional raster coordinates. diff --git a/src/physrisk/hazard_models/core_hazards.py b/src/physrisk/hazard_models/core_hazards.py index 3b981706..1ee0dbb1 100644 --- a/src/physrisk/hazard_models/core_hazards.py +++ b/src/physrisk/hazard_models/core_hazards.py @@ -1,6 +1,7 @@ import logging +from collections.abc import Sequence from enum import Enum -from typing import NamedTuple, Protocol, Sequence +from typing import NamedTuple, Protocol from physrisk.api.v1.hazard_data import HazardResource from physrisk.data.hazard_data_provider import ( diff --git a/src/physrisk/hazard_models/credentials_provider.py b/src/physrisk/hazard_models/credentials_provider.py index 34c4e3f9..f183f559 100644 --- a/src/physrisk/hazard_models/credentials_provider.py +++ b/src/physrisk/hazard_models/credentials_provider.py @@ -1,5 +1,5 @@ from os import getenv -from typing import Dict, Protocol +from typing import Protocol class CredentialsProvider(Protocol): @@ -19,7 +19,7 @@ def jupiter_client_secret(self) -> str: ... def jupiter_refresh_token(self) -> str: ... - def proxies(self) -> Dict[str, str]: ... + def proxies(self) -> dict[str, str]: ... class EnvCredentialsProvider(CredentialsProvider): @@ -52,5 +52,5 @@ def jupiter_client_secret(self) -> str: def jupiter_refresh_token(self) -> str: return getenv("JUPITER_REFRESH_TOKEN", "") - def proxies(self) -> Dict[str, str]: + def proxies(self) -> dict[str, str]: return {"https": getenv("PROXY_HTTPS", ""), "http": getenv("PROXY_HTTP", "")} diff --git a/src/physrisk/hazard_models/hazard_cache.py b/src/physrisk/hazard_models/hazard_cache.py index 0b647dbb..9861ba30 100644 --- a/src/physrisk/hazard_models/hazard_cache.py +++ b/src/physrisk/hazard_models/hazard_cache.py @@ -2,10 +2,11 @@ import hashlib import json import os +from collections.abc import Sequence from dataclasses import dataclass from enum import Enum from pathlib import PurePosixPath -from typing import Any, Dict, List, Optional, Protocol, Sequence +from typing import Any, Protocol import h3 import shapely @@ -27,11 +28,11 @@ class ItemType(str, Enum): class Store(Protocol): def __init__(self, file: str): ... - def setitems(self, items: Dict[str, Any]): ... + def setitems(self, items: dict[str, Any]): ... def getitems(self, keys: Sequence[str]): ... - def getall(self, prefix: str = "") -> Dict[str, bytes]: ... + def getall(self, prefix: str = "") -> dict[str, bytes]: ... def to_json(store: Store, prefix: str = ""): @@ -49,7 +50,7 @@ def __init__(self, file: str): with Lmdb.open(self._file, "c") as _: ... - def setitems(self, items: Dict[str, Any]): + def setitems(self, items: dict[str, Any]): with Lmdb.open(self._file, "c") as db: db.update(items) @@ -65,9 +66,7 @@ def getall(self, prefix: str = ""): class MemoryStore(Store): - def __init__( - self, file: Optional[str] = None, values: Optional[Dict[str, str]] = None - ): + def __init__(self, file: str | None = None, values: dict[str, str] | None = None): self._dict = {} if file is not None and os.path.exists(file): with open(file, "r") as f: @@ -75,7 +74,7 @@ def __init__( if values is not None: self._dict.update(values) - def setitems(self, items: Dict[str, Any]): + def setitems(self, items: dict[str, Any]): self._dict.update(items) def getitems(self, keys: Sequence[str]): @@ -110,10 +109,10 @@ def key(self, provider_id: str, spatial_index: str): def getall(self, prefix: str = ""): return self.store.getall(prefix) - def getitems(self, keys: List[str]): + def getitems(self, keys: list[str]): return self.store.getitems(keys) - def setitems(self, items: Dict[str, Any]): + def setitems(self, items: dict[str, Any]): self.store.setitems(items) @@ -153,7 +152,7 @@ def __init__( self.grid_size = grid_size def spatial_key( - self, latitude: float, longitude: float, geometry: Optional[BaseGeometry] = None + self, latitude: float, longitude: float, geometry: BaseGeometry | None = None ): if geometry is None: # based on the lat/lon @@ -181,8 +180,8 @@ def key(self, provider_id: str, spatial_index: str): def getall(self, prefix: str = ""): return self.store.getall(prefix) - def getitems(self, keys: List[str]): + def getitems(self, keys: list[str]): return self.store.getitems(keys) - def setitems(self, items: Dict[str, Any]): + def setitems(self, items: dict[str, Any]): self.store.setitems(items) diff --git a/src/physrisk/hazard_models/hazard_model_factory.py b/src/physrisk/hazard_models/hazard_model_factory.py index 5264e6f8..edcda6af 100644 --- a/src/physrisk/hazard_models/hazard_model_factory.py +++ b/src/physrisk/hazard_models/hazard_model_factory.py @@ -1,15 +1,15 @@ from collections import defaultdict -from typing import Dict, List, Mapping, MutableMapping, Optional, Sequence +from collections.abc import Mapping, MutableMapping, Sequence from physrisk.data.hazard_data_provider import HazardResourceSelector +from physrisk.data.image_creator import ImageCreator +from physrisk.data.inventory import Inventory +from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.data.scenario_year_resolution import ( InterpolatedYearResolver, ScenarioYearResolver, resolve_exact_year, ) -from physrisk.data.image_creator import ImageCreator -from physrisk.data.inventory import Inventory -from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.data.zarr_reader import ZarrReader from physrisk.hazard_models.credentials_provider import CredentialsProvider from physrisk.hazard_models.hazard_cache import GeometryH3BasedCache @@ -36,8 +36,8 @@ def __init__( credentials: CredentialsProvider, inventory: Inventory, resource_selector: HazardResourceSelector, - store: Optional[MutableMapping] = None, - reader: Optional[ZarrReader] = None, + store: MutableMapping | None = None, + reader: ZarrReader | None = None, default_interpolation: str = "floor", zarr_max_workers: int = 32, ): @@ -52,8 +52,8 @@ def __init__( def hazard_model( self, - interpolation: Optional[str] = None, - provider_max_requests: Dict[str, int] = {}, + interpolation: str | None = None, + provider_max_requests: dict[str, int] = {}, interpolate_years: bool = True, ): return CompositeHazardModel( @@ -94,10 +94,10 @@ def __init__( credentials: CredentialsProvider, resource_selector: HazardResourceSelector, scenario_year_resolver: ScenarioYearResolver, - store: Optional[MutableMapping] = None, - reader: Optional[ZarrReader] = None, + store: MutableMapping | None = None, + reader: ZarrReader | None = None, interpolation: str = "floor", - provider_max_requests: Dict[str, int] = {}, + provider_max_requests: dict[str, int] = {}, restrict_coverage: bool = False, use_jba_coastal: bool = False, zarr_max_workers: int = 32, @@ -135,7 +135,7 @@ def _zarr_hint_path(self, request: HazardDataRequest): def get_hazard_data( self, requests: Sequence[HazardDataRequest] ) -> Mapping[HazardDataRequest, HazardDataResponse]: - requests_by_model: Dict[HazardModel, List[HazardDataRequest]] = defaultdict( + requests_by_model: dict[HazardModel, list[HazardDataRequest]] = defaultdict( list ) for request in requests: @@ -146,9 +146,7 @@ def get_hazard_data( or request.hazard_type == PluvialInundation ) and not self._zarr_hint_path(request) - ): - requests_by_model[self.jba_hazard_model].append(request) - elif ( + ) or ( self.jba_hazard_model and request.hazard_type == CoastalInundation and self.use_jba_coastal @@ -158,7 +156,7 @@ def get_hazard_data( else: requests_by_model[self.zarr_hazard_model].append(request) - responses: Dict[HazardDataRequest, HazardDataResponse] = {} + responses: dict[HazardDataRequest, HazardDataResponse] = {} for model, reqs in requests_by_model.items(): events_reponses = model.get_hazard_data(reqs) diff --git a/src/physrisk/hazard_models/jba_hazard_model.py b/src/physrisk/hazard_models/jba_hazard_model.py index d3d73c62..2da5520d 100644 --- a/src/physrisk/hazard_models/jba_hazard_model.py +++ b/src/physrisk/hazard_models/jba_hazard_model.py @@ -2,19 +2,12 @@ import json import logging from collections import defaultdict +from collections.abc import Iterable, Mapping, MutableMapping, Sequence from dataclasses import dataclass from enum import Enum from threading import Lock from typing import ( - Dict, - Iterable, - List, - Mapping, - MutableMapping, NamedTuple, - Optional, - Sequence, - Tuple, ) import aiohttp @@ -77,10 +70,10 @@ class APIRequest: spatial_keys: Sequence[str] # spatial keys for latitudes and longitudes in order latitudes: Sequence[float] longitudes: Sequence[float] - geometries: Sequence[Optional[BaseGeometry]] + geometries: Sequence[BaseGeometry | None] country_code: str - location_cache_keys: Dict[ - str, List[JBACacheKey] + location_cache_keys: dict[ + str, list[JBACacheKey] ] # for each spatial_keys list of cache keys that is requested # this is requested for all jba_scenarios systematically @@ -91,15 +84,15 @@ def request_count(self): @dataclass class RequestWeights: request: HazardDataRequest - weights: List[Tuple[JBACacheKey, float]] + weights: list[tuple[JBACacheKey, float]] class JBAHazardModel(HazardModel): def __init__( self, cache_store: GeometryH3BasedCache, - credentials: Optional[CredentialsProvider] = None, - geocoder: Optional[Geocoder] = None, + credentials: CredentialsProvider | None = None, + geocoder: Geocoder | None = None, max_requests: int = 5, cmip: int = 6, batch_size: int = 100, @@ -202,8 +195,8 @@ def get_hazard_data( # 2) We are already maxing out the number of requests to JBA using async for a single thread. if not self.use_best_dataset and self.geocoder is None: self.geocoder = Geocoder.instance() - cache_key_country: Dict[JBACacheKey, str] = {} # cache item to requests - request_groups: Dict[RequestKey, List[JBACacheKey]] = defaultdict( + cache_key_country: dict[JBACacheKey, str] = {} # cache item to requests + request_groups: dict[RequestKey, list[JBACacheKey]] = defaultdict( list ) # request to cache items self.check_requests(requests) @@ -222,7 +215,7 @@ def get_hazard_data( } result: MutableMapping[HazardDataRequest, HazardDataResponse] = {} # group requests by common location - requests_by_location: Dict[str, List[HazardDataRequest]] = defaultdict(list) + requests_by_location: dict[str, list[HazardDataRequest]] = defaultdict(list) all_years: set[int] = set() for item in requests: spatial_key = self.cache_store.spatial_key( @@ -266,12 +259,12 @@ def get_hazard_data( cache_keys: set[JBACacheKey] = ( set() ) # the set of cache keys to be requested (spatial key, year and scenario) - req_weights_set: List[ + req_weights_set: list[ RequestWeights ] = [] # for each request the linear combination of cache keys required (interpolating) for reqs, country in zip(requests_by_location.values(), countries): for req in reqs: - req_weights: List[Tuple[JBACacheKey, float]] = [] + req_weights: list[tuple[JBACacheKey, float]] = [] for weight in pillar_years_lookup[ -1 if req.scenario == "historical" else req.year ].weights: @@ -292,10 +285,10 @@ def get_hazard_data( country_code = cache_key_country[cache_key] request_groups[RequestKey(country_code=country_code)].append(cache_key) access_token = self.credentials.jba_access_key() - api_requests: List[APIRequest] = [] + api_requests: list[APIRequest] = [] # contains the raw results for all cache keys, first populated by looking in cache # and then by making API calls if needed. - cached_responses: Dict[JBACacheKey, Dict] = {} + cached_responses: dict[JBACacheKey, dict] = {} for request_key, group_cache_keys in request_groups.items(): # process anything that can be sourced from the cache and identify extra API requests needed group_cached_responses, api_requests_batch = ( @@ -420,7 +413,7 @@ def jba_scenario(self, scenario: str, year: int, country: str = ""): ) return prefix + "_" + range - def _jba_scenarios_for_country(self, country: str) -> List[str]: + def _jba_scenarios_for_country(self, country: str) -> list[str]: """Scenario identifiers for all pillar years. Under cmip=5, GB/NI/ROI use narrower year ranges (see jba_scenario's gb_code check, which keys off the "rcp" naming), so this must use rcp-style names and be recomputed per @@ -464,7 +457,7 @@ async def flood_depth( ) ) ) - req_id_to_keys: Dict[str, List[JBACacheKey]] = defaultdict(list) + req_id_to_keys: dict[str, list[JBACacheKey]] = defaultdict(list) for k in api_request.spatial_keys: req_id_to_keys[self.jba_request_id(k)] = [ JBACacheKey(s, k) for s in (["historical"] + scenarios) @@ -477,7 +470,7 @@ async def flood_depth( # response: the cache key is a hash/H3 cell derived from this, and is not reversible, # so without this we could never recover the original geometry from the cache alone # (e.g. if migrating to a different caching scheme in future). - id_to_wkt: Dict[str, str] = { + id_to_wkt: dict[str, str] = { id: (f"POINT({lon} {lat})" if geom is None else geom.wkt) for id, lat, lon, geom in zip( req_ids, @@ -566,14 +559,14 @@ def _identify_api_requests( self, request_key: RequestKey, cache_keys: Iterable[JBACacheKey], - requests_by_location: Dict[str, List[HazardDataRequest]], + requests_by_location: dict[str, list[HazardDataRequest]], ): """Process any results that can be sourced from the cache and identify the requests to the API that are needed.""" - batches: List[APIRequest] = [] + batches: list[APIRequest] = [] cache_ids = [self.jba_cache_id(k) for k in cache_keys] # first checks cache - cached_responses: Dict[JBACacheKey, Dict] = {} + cached_responses: dict[JBACacheKey, dict] = {} for cache_key, item in zip(cache_keys, self.cache_store.getitems(cache_ids)): if item is not None: value = json.loads(item) @@ -581,7 +574,7 @@ def _identify_api_requests( cached_responses[cache_key] = value # we need to create requests for anything not in cache # req_keys_all = [k for k in cache_keys if k not in cached_responses] - location_cache_keys: Dict[str, List[JBACacheKey]] = defaultdict(list) + location_cache_keys: dict[str, list[JBACacheKey]] = defaultdict(list) missing_cache_keys = [k for k in cache_keys if k not in cached_responses] for k in missing_cache_keys: location_cache_keys[k.spatial_key].append(k) @@ -630,7 +623,7 @@ def _process_api_requests( with aiohttp.TCPConnector( limit_per_host=concurrent_requests, loop=loop ) as conn: - reruns: List[APIRequest] = [] + reruns: list[APIRequest] = [] async def gather_requests(api_requests: Sequence[APIRequest]): semaphore = asyncio.Semaphore(concurrent_requests) @@ -696,7 +689,7 @@ async def request_single(request: APIRequest): logger.info(f"Check: {len(single_api_requests)} reruns") return cached_responses - def _process_response(self, request: HazardDataRequest, response: Dict): + def _process_response(self, request: HazardDataRequest, response: dict): if request.hazard_type == RiverineInundation: tag = "FLRF_U" path = "jba_riverine" @@ -716,8 +709,8 @@ def _process_response(self, request: HazardDataRequest, response: Dict): [sop, sop], units="years", path=path ) # min and max: in this case just a single value elif request.indicator_id == "flood_depth": - return_periods: List[float] = [] - intens: List[float] = [] + return_periods: list[float] = [] + intens: list[float] = [] for key, value in response["stats"].get(tag, {}).items(): assert isinstance(key, str) if key.startswith("rp_"): diff --git a/src/physrisk/hazard_models/jba_image_creator.py b/src/physrisk/hazard_models/jba_image_creator.py index 459889c9..2bd422e2 100644 --- a/src/physrisk/hazard_models/jba_image_creator.py +++ b/src/physrisk/hazard_models/jba_image_creator.py @@ -1,24 +1,24 @@ import asyncio -from dataclasses import dataclass import io import logging -from typing import Any, List, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any import aiohttp import numpy as np -import PIL.Image as Image from lxml import etree +from PIL import Image from physrisk.api.v1.hazard_image import TileNotAvailableError from physrisk.data import colormap_provider from physrisk.data.image_creator import ImageCreator, image_from_packed_rgba, to_rgba -from physrisk.kernel.hazard_model import HazardImageCreator, Tile - -from physrisk.utils.event_loop import get_loop, run from physrisk.hazard_models.credentials_provider import ( CredentialsProvider, EnvCredentialsProvider, ) +from physrisk.kernel.hazard_model import HazardImageCreator, Tile +from physrisk.utils.event_loop import get_loop, run logger = logging.getLogger(__name__) @@ -198,7 +198,7 @@ def identifier(self): return f"{self.name}_{self.release_date}_{self.resolution}_{self.projection}" -TileSpec = Tuple[int, int, int] # (z, x, y) +TileSpec = tuple[int, int, int] # (z, x, y) _SUPPORTED_TILE_SIZES = (256, 512) _DEFAULT_TILE_SIZE = 512 @@ -209,7 +209,7 @@ def identifier(self): _JBA_NATIVE_MAX_ZOOM = 16 -def _resolve_tile_size(tile_size: Optional[int]) -> int: +def _resolve_tile_size(tile_size: int | None) -> int: size = tile_size if tile_size is not None else _DEFAULT_TILE_SIZE if size not in _SUPPORTED_TILE_SIZES: raise ValueError( @@ -228,7 +228,7 @@ class JBAImageCreator(HazardImageCreator): def __init__( self, - credentials: Optional[CredentialsProvider] = None, + credentials: CredentialsProvider | None = None, tileset: TileSet = TileSet("WR", "202603", "5m", "4326"), ): self.credentials = ( @@ -249,12 +249,12 @@ def create_image( year: int, format="PNG", colormap: str = "heating", - tile: Optional[Tile] = None, - min_value: Optional[float] = None, - max_value: Optional[float] = None, - index_value: Optional[Union[str, float]] = None, + tile: Tile | None = None, + min_value: float | None = None, + max_value: float | None = None, + index_value: str | float | None = None, scaling: str = "linear", - tile_size: Optional[int] = None, + tile_size: int | None = None, ): assert tile is not None size = _resolve_tile_size(tile_size) @@ -314,16 +314,18 @@ def get_legend(self, resource_id: str, return_period: int = 1500) -> bytes: loop = get_loop() async def _fetch() -> bytes: - async with aiohttp.ClientSession( - proxy=self.credentials.proxies()["https"], - auth=aiohttp.BasicAuth( - self.credentials.jba_vision_username(), - self.credentials.jba_vision_password(), - ), - ) as session: - async with session.get(url) as resp: - resp.raise_for_status() - return await resp.read() + async with ( + aiohttp.ClientSession( + proxy=self.credentials.proxies()["https"], + auth=aiohttp.BasicAuth( + self.credentials.jba_vision_username(), + self.credentials.jba_vision_password(), + ), + ) as session, + session.get(url) as resp, + ): + resp.raise_for_status() + return await resp.read() return run(_fetch(), loop=loop) @@ -332,8 +334,8 @@ def get_info( resource_id: str, scenario: str, year: int, - tile_size: Optional[int] = None, - ) -> Tuple[Sequence[Any], Sequence[Any], str, str, Optional[int], int]: + tile_size: int | None = None, + ) -> tuple[Sequence[Any], Sequence[Any], str, str, int | None, int]: size = _resolve_tile_size(tile_size) index_values = [20, 50, 100, 200, 500, 1500] return ( @@ -408,7 +410,7 @@ async def _fetch_tile( return Image.open(io.BytesIO(data)).convert("RGBA") # ensure RGBA async def _fetch_all_tiles( - self, resource_id: str, return_period: int, tile_specs: List[TileSpec] + self, resource_id: str, return_period: int, tile_specs: list[TileSpec] ): """Download all tiles concurrently and return them in the same order.""" async with aiohttp.ClientSession( @@ -476,8 +478,8 @@ def _identifier(self, tile_set: TileSet, resource_id: str, return_period: int): class CombinedImageCreator(HazardImageCreator): def __init__( self, - image_creator: Optional[ImageCreator], - jba_image_creator: Optional[JBAImageCreator], + image_creator: ImageCreator | None, + jba_image_creator: JBAImageCreator | None, ): self._image_creator = image_creator self._jba_image_creator = jba_image_creator @@ -499,12 +501,12 @@ def create_image( year: int, format="PNG", colormap: str = "heating", - tile: Optional[Tile] = None, - min_value: Optional[float] = None, - max_value: Optional[float] = None, - index_value: Optional[Union[str, float]] = None, + tile: Tile | None = None, + min_value: float | None = None, + max_value: float | None = None, + index_value: str | float | None = None, scaling: str = "linear", - tile_size: Optional[int] = None, + tile_size: int | None = None, ): return self._creator(resource_id).create_image( resource_id, @@ -525,7 +527,7 @@ def get_info( resource_id: str, scenario: str, year: int, - tile_size: Optional[int] = None, + tile_size: int | None = None, ): return self._creator(resource_id).get_info( resource_id, scenario, year, tile_size=tile_size diff --git a/src/physrisk/kernel/assets.py b/src/physrisk/kernel/assets.py index 0f0e73a4..744e195d 100644 --- a/src/physrisk/kernel/assets.py +++ b/src/physrisk/kernel/assets.py @@ -1,7 +1,13 @@ +import math from dataclasses import dataclass from enum import Enum -import math -from typing import Optional, Protocol, runtime_checkable +from typing import Protocol, runtime_checkable + +import shapely.wkt +from pyproj import Transformer +from shapely import Point +from shapely.geometry.base import BaseGeometry +from shapely.ops import transform from physrisk.kernel.hazards import ( CoastalInundation, @@ -9,12 +15,6 @@ RiverineInundation, ) -from pyproj import Transformer -from shapely.ops import transform -from shapely import Point -from shapely.geometry.base import BaseGeometry -import shapely.wkt - project_4326_to_3857 = Transformer.from_crs( "EPSG:4326", "EPSG:3857", always_xy=True ).transform @@ -108,12 +108,12 @@ class Asset: def __init__( self, - latitude: Optional[float] = None, - longitude: Optional[float] = None, - wkt_geometry: Optional[str] = None, + latitude: float | None = None, + longitude: float | None = None, + wkt_geometry: str | None = None, buffer: float = 0.0, - id: Optional[str] = None, - aggregation_id: Optional[str] = None, + id: str | None = None, + aggregation_id: str | None = None, **kwargs, ): """ @@ -198,10 +198,10 @@ def buffered_geometry(geometry: BaseGeometry, buffer: float) -> BaseGeometry: class OEDAsset(Asset): def __init__( self, - latitude: Optional[float] = None, - longitude: Optional[float] = None, + latitude: float | None = None, + longitude: float | None = None, occupancy_code: int = 1000, - wkt_geometry: Optional[str] = None, + wkt_geometry: str | None = None, buffer: float = 0.0, number_of_storeys: int = -1, # -1 = unknown No. storeys - low rise, -2 = unknown No. storeys - mid rise, -3 = Unknown no. storeys = high rise). basement: int = 0, # 0 = unknown / default, 1 = unfinished, 2 = 100% finished @@ -225,7 +225,7 @@ def __init__( class SimpleTypeLocationAsset(Asset): def __init__( - self, *, location: Optional[str] = None, type: Optional[str] = None, **kwargs + self, *, location: str | None = None, type: str | None = None, **kwargs ): super().__init__(**kwargs) self.location = location @@ -274,19 +274,19 @@ def __init__( class PowerGeneratingAsset(OEDAsset, SimpleTypeLocationAsset): def __init__( self, - capacity: Optional[float] = None, + capacity: float | None = None, **kwargs, ): super().__init__(**kwargs) - self.capacity: Optional[float] = capacity - self.primary_fuel: Optional[FuelKind] = None + self.capacity: float | None = capacity + self.primary_fuel: FuelKind | None = None if self.type is not None: archetypes = self.type.split("/") if 0 < len(archetypes): self.primary_fuel = FuelKind[archetypes[0]] - def get_protection_return_period(self, hazard_type: type) -> Optional[float]: + def get_protection_return_period(self, hazard_type: type) -> float | None: if issubclass( hazard_type, (CoastalInundation, PluvialInundation, RiverineInundation) ): @@ -302,16 +302,16 @@ def __init__(self, **kwargs): @runtime_checkable class HasStandardOfProtection(Protocol): - def get_protection_return_period(self, hazard_type: type) -> Optional[float]: ... + def get_protection_return_period(self, hazard_type: type) -> float | None: ... class ThermalPowerGeneratingAsset(PowerGeneratingAsset): def __init__( self, *, - type: Optional[str] = None, - location: Optional[str] = None, - capacity: Optional[float] = None, + type: str | None = None, + location: str | None = None, + capacity: float | None = None, **kwargs, ): super().__init__( @@ -321,8 +321,8 @@ def __init__( **kwargs, ) - self.turbine: Optional[TurbineKind] = None - self.cooling: Optional[CoolingKind] = None + self.turbine: TurbineKind | None = None + self.cooling: CoolingKind | None = None if self.type is not None: archetypes = self.type.split("/") @@ -340,7 +340,7 @@ def get_inundation_protection_return_period(self) -> float: return 10000.0 return 250.0 - def get_protection_return_period(self, hazard_type: type) -> Optional[float]: + def get_protection_return_period(self, hazard_type: type) -> float | None: if issubclass( hazard_type, (CoastalInundation, PluvialInundation, RiverineInundation) ): @@ -364,12 +364,12 @@ def __init__(self, **kwargs): @dataclass class WindTurbine(Asset): - capacity: Optional[float] = None - hub_height: Optional[float] = None - cut_in_speed: Optional[float] = None - cut_out_speed: Optional[float] = None - fixed_base: Optional[bool] = True - rotor_diameter: Optional[float] = None + capacity: float | None = None + hub_height: float | None = None + cut_in_speed: float | None = None + cut_out_speed: float | None = None + fixed_base: bool | None = True + rotor_diameter: float | None = None _EXCLUDED_ASSET_TYPES = (OEDAsset, SimpleTypeLocationAsset, TestAsset) diff --git a/src/physrisk/kernel/calculation.py b/src/physrisk/kernel/calculation.py index 59d31a99..54add4fb 100644 --- a/src/physrisk/kernel/calculation.py +++ b/src/physrisk/kernel/calculation.py @@ -1,7 +1,7 @@ -from typing import Dict, Sequence, Type +from collections.abc import Sequence -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.hazards import ChronicHeat, Drought, Fire, Hail, Precipitation from physrisk.kernel.impact_distrib import ImpactType @@ -65,7 +65,7 @@ def placeholder_models() -> Sequence[VulnerabilityModelBase]: ] -def default_vulnerability_models() -> Dict[type, list[VulnerabilityModelBase]]: +def default_vulnerability_models() -> dict[type, list[VulnerabilityModelBase]]: """Base set of programmatic models; other models are added on top of these There is a specific treatment for power generating assets and real estate assets. """ @@ -81,7 +81,7 @@ def default_vulnerability_models() -> Dict[type, list[VulnerabilityModelBase]]: } -def alternate_default_vulnerability_models_scores() -> Dict[ +def alternate_default_vulnerability_models_scores() -> dict[ type, Sequence[VulnerabilityModelBase] ]: """A vulnerability models set that combines loss-based and exposure-based scores.""" @@ -125,7 +125,7 @@ def alternate_default_vulnerability_models_scores() -> Dict[ } -def get_default_risk_measure_calculators() -> Dict[Type[Asset], RiskMeasureCalculator]: +def get_default_risk_measure_calculators() -> dict[type[Asset], RiskMeasureCalculator]: """For asset-level risk measure, define the measure calculators to use.""" return {Asset: GenericScoreBasedRiskMeasures()} # return {RealEstateAsset: RealEstateToyRiskMeasures()} @@ -136,7 +136,7 @@ class DefaultMeasuresFactory(RiskMeasuresFactory): def asset_calculators( self, use_case_id: str - ) -> Dict[Type[Asset], RiskMeasureCalculator]: + ) -> dict[type[Asset], RiskMeasureCalculator]: """Get the appropriate risk measure calculators based on the use case identifier.""" if use_case_id == "generic": diff --git a/src/physrisk/kernel/curve.py b/src/physrisk/kernel/curve.py index 52d49893..0ceb4eff 100644 --- a/src/physrisk/kernel/curve.py +++ b/src/physrisk/kernel/curve.py @@ -1,5 +1,3 @@ -from typing import List, Union - import numpy as np @@ -125,8 +123,8 @@ class ExceedanceCurve: def __init__( self, - probs: Union[List[float], np.ndarray], - values: Union[List[float], np.ndarray], + probs: list[float] | np.ndarray, + values: list[float] | np.ndarray, ): """Create a new asset event distribution. Args: diff --git a/src/physrisk/kernel/events.py b/src/physrisk/kernel/events.py index 8bf76c97..ff5ebe28 100644 --- a/src/physrisk/kernel/events.py +++ b/src/physrisk/kernel/events.py @@ -1,4 +1,4 @@ -from typing import List, Protocol +from typing import Protocol import numpy as np from numba import float64, njit @@ -112,7 +112,7 @@ def inv_cumulative_marginal_probs(self, cum_probs: np.ndarray): def event_samples( - impacts_bins: np.ndarray, probs: List[np.ndarray], nb_events: int, nb_samples: int + impacts_bins: np.ndarray, probs: list[np.ndarray], nb_events: int, nb_samples: int ): if any([p.size != 1 and p.size != nb_events for p in probs]): raise ValueError( @@ -138,7 +138,7 @@ def find(elements: np.ndarray, value): @njit(cache=True) def event_samples_numba( - impacts_bins: np.ndarray, probs: List[np.ndarray], nb_events: int, nb_samples: int + impacts_bins: np.ndarray, probs: list[np.ndarray], nb_events: int, nb_samples: int ): samples = np.zeros(shape=(nb_samples, nb_events)) np.random.seed(111) @@ -162,7 +162,7 @@ def event_samples_numba( @jitclass(spec) -class CumulativeProb(object): +class CumulativeProb: def __init__(self, values: np.ndarray, cum_probs: np.ndarray): self.values = values self.cum_probs = cum_probs diff --git a/src/physrisk/kernel/exposure.py b/src/physrisk/kernel/exposure.py index eefb4e77..d3304084 100644 --- a/src/physrisk/kernel/exposure.py +++ b/src/physrisk/kernel/exposure.py @@ -1,9 +1,9 @@ import logging import math from abc import abstractmethod +from collections.abc import Sequence from dataclasses import dataclass from enum import Enum -from typing import Dict, List, Sequence, Tuple import numpy as np @@ -50,14 +50,14 @@ class Bounds: @dataclass class AssetExposureResult: - hazard_categories: Dict[type, Tuple[Category, float, str]] + hazard_categories: dict[type, tuple[Category, float, str]] class ExposureMeasure(DataRequester): @abstractmethod def get_exposures( self, asset: Asset, data_responses: Sequence[HazardDataResponse] - ) -> Dict[type, Tuple[Category, float, str]]: ... + ) -> dict[type, tuple[Category, float, str]]: ... class JupterExposureMeasure(ExposureMeasure): @@ -84,7 +84,7 @@ def get_data_requests( ] def get_exposures(self, asset: Asset, data_responses: Sequence[HazardDataResponse]): - result: Dict[type, Tuple[Category, float, str]] = {} + result: dict[type, tuple[Category, float, str]] = {} for (k, v), resp in zip(self.exposure_bins.items(), data_responses): if isinstance(resp, HazardParameterDataResponse): param = resp.parameter @@ -169,23 +169,21 @@ def bounds_to_lookup(self, bounds: Sequence[Bounds]): def calculate_exposures( - assets: List[Asset], + assets: list[Asset], hazard_model: HazardModel, exposure_measure: ExposureMeasure, scenario: str, year: int, -) -> Dict[Asset, AssetExposureResult]: - requester_assets: Dict[DataRequester, List[Asset]] = {exposure_measure: assets} +) -> dict[Asset, AssetExposureResult]: + requester_assets: dict[DataRequester, list[Asset]] = {exposure_measure: assets} scen_year_asset_requests, responses = _download_data_consolidated( hazard_model, requester_assets, [scenario], [year] ) asset_requests = scen_year_asset_requests[ScenarioYear(scenario, year)] logging.info( - "Applying exposure measure {0} to {1} assets of type {2}".format( - type(exposure_measure).__name__, len(assets), type(assets[0]).__name__ - ) + f"Applying exposure measure {type(exposure_measure).__name__} to {len(assets)} assets of type {type(assets[0]).__name__}" ) - result: Dict[Asset, AssetExposureResult] = {} + result: dict[Asset, AssetExposureResult] = {} for asset in assets: requests = asset_requests[ diff --git a/src/physrisk/kernel/financial_model.py b/src/physrisk/kernel/financial_model.py index 7456d569..9a862b53 100644 --- a/src/physrisk/kernel/financial_model.py +++ b/src/physrisk/kernel/financial_model.py @@ -1,4 +1,5 @@ -from typing import Protocol, Sequence +from collections.abc import Sequence +from typing import Protocol import numpy as np diff --git a/src/physrisk/kernel/hazard_event_distrib.py b/src/physrisk/kernel/hazard_event_distrib.py index 45cd6786..a6b6b1a9 100644 --- a/src/physrisk/kernel/hazard_event_distrib.py +++ b/src/physrisk/kernel/hazard_event_distrib.py @@ -1,5 +1,3 @@ -from typing import List, Union - import numpy as np from physrisk.kernel.hazards import Hazard @@ -13,19 +11,19 @@ class HazardEventDistrib: __slots__ = [ "__event_type", + "__exceedance", "__intensity_bins", - "__prob", "__path", - "__exceedance", + "__prob", "__units", ] def __init__( self, event_type: type[Hazard], - intensity_bins: Union[List[float], np.ndarray], - prob: Union[List[float], np.ndarray], - path: List[str], + intensity_bins: list[float] | np.ndarray, + prob: list[float] | np.ndarray, + path: list[str], units: str, ): """Create a new asset event distribution. @@ -60,7 +58,7 @@ def prob(self) -> np.ndarray: return self.__prob @property - def path(self) -> List[str]: + def path(self) -> list[str]: return self.__path @property diff --git a/src/physrisk/kernel/hazard_model.py b/src/physrisk/kernel/hazard_model.py index 9e246b7b..f22560a3 100644 --- a/src/physrisk/kernel/hazard_model.py +++ b/src/physrisk/kernel/hazard_model.py @@ -1,16 +1,10 @@ import sys from abc import ABC, abstractmethod +from collections.abc import Mapping, Sequence from typing import ( Any, - Dict, - Mapping, NamedTuple, - Optional, Protocol, - Sequence, - Tuple, - Type, - Union, ) import numpy as np @@ -27,29 +21,29 @@ class HazardDataRequest: and the parameter value is returned.""" __slots__ = ( + "buffer", + "geometry", "hazard_type", - "longitude", - "latitude", + "hint", "indicator_id", + "latitude", + "longitude", "scenario", "year", - "hint", - "geometry", - "buffer", ) def __init__( self, - hazard_type: Type[Hazard], + hazard_type: type[Hazard], longitude: float, latitude: float, *, indicator_id: str, scenario: str, year: int, - hint: Optional[HazardDataHint] = None, - geometry: Optional[BaseGeometry] = None, - buffer: Optional[int] = None, + hint: HazardDataHint | None = None, + geometry: BaseGeometry | None = None, + buffer: int | None = None, ): """Create HazardDataRequest. @@ -112,7 +106,7 @@ class HazardDataResponse(Protocol): class HazardDataFailedResponse(HazardDataResponse): - def __init__(self, err: Optional[Exception] = None, reason: Optional[str] = None): + def __init__(self, err: Exception | None = None, reason: str | None = None): self.error = err self.reason = reason self.path = "" @@ -246,12 +240,12 @@ def create_image( year: int, format="PNG", colormap: str = "heating", - tile: Optional[Tile] = None, - min_value: Optional[float] = None, - max_value: Optional[float] = None, - index_value: Optional[Union[str, float]] = None, + tile: Tile | None = None, + min_value: float | None = None, + max_value: float | None = None, + index_value: str | float | None = None, scaling: str = "linear", - tile_size: Optional[int] = None, + tile_size: int | None = None, ): """Creates an image Tile for display on maps. @@ -278,8 +272,8 @@ def get_info( resource_id: str, scenario: str, year: int, - tile_size: Optional[int] = None, - ) -> Tuple[Sequence[Any], Sequence[Any], str, str, Optional[int], int]: + tile_size: int | None = None, + ) -> tuple[Sequence[Any], Sequence[Any], str, str, int | None, int]: """Provides additional image information required to create an image. Args: @@ -303,8 +297,8 @@ def get_info( class HazardModelFactory(Protocol): def hazard_model( self, - interpolation: Optional[str] = None, - provider_max_requests: Dict[str, int] = {}, + interpolation: str | None = None, + provider_max_requests: dict[str, int] = {}, interpolate_years: bool = False, ) -> HazardModel: """Create a HazardModel instance based on a number of options. @@ -331,4 +325,4 @@ def image_creator(self, interpolate_years: bool = True) -> HazardImageCreator: class DataSource(Protocol): def __call__( self, longitudes, latitudes, *, model: str, scenario: str, year: int - ) -> Tuple[np.ndarray, np.ndarray]: ... + ) -> tuple[np.ndarray, np.ndarray]: ... diff --git a/src/physrisk/kernel/hazards.py b/src/physrisk/kernel/hazards.py index 24b43583..e332539c 100644 --- a/src/physrisk/kernel/hazards.py +++ b/src/physrisk/kernel/hazards.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Dict, Type class IndicatorData(Enum): @@ -20,14 +19,14 @@ class HazardKind(Enum): class Hazard: kind = HazardKind.UNKNOWN - indicator_data: Dict[str, IndicatorData] = {} + indicator_data: dict[str, IndicatorData] = {} -def hazard_kind(hazard_type: Type[Hazard]): +def hazard_kind(hazard_type: type[Hazard]): return hazard_type.kind -def indicator_data(hazard_type: Type[Hazard], indicator_id: str): +def indicator_data(hazard_type: type[Hazard], indicator_id: str): default = ( IndicatorData.EVENT if hazard_type.kind == HazardKind.ACUTE @@ -38,7 +37,6 @@ def indicator_data(hazard_type: Type[Hazard], indicator_id: str): class ChronicHeat(Hazard): kind = HazardKind.CHRONIC - pass class Inundation(Hazard): @@ -47,7 +45,6 @@ class Inundation(Hazard): "flood_depth": IndicatorData.EVENT, "flood_sop": IndicatorData.PARAMETERS, } - pass class AirTemperature(ChronicHeat): @@ -60,17 +57,14 @@ class CoastalInundation(Inundation): class ChronicWind(Hazard): kind = HazardKind.CHRONIC - pass class CombinedInundation(Hazard): kind = HazardKind.CHRONIC - pass class Drought(Hazard): kind = HazardKind.CHRONIC - pass class Fire(Hazard): @@ -78,7 +72,6 @@ class Fire(Hazard): indicator_data = { "fire_probability": IndicatorData.PARAMETERS, } - pass class Hail(Hazard): @@ -86,7 +79,6 @@ class Hail(Hazard): indicator_data = { "days/above/5cm": IndicatorData.PARAMETERS, } - pass class PluvialInundation(Inundation): @@ -95,7 +87,6 @@ class PluvialInundation(Inundation): class Precipitation(Hazard): kind = HazardKind.CHRONIC - pass class RiverineInundation(Inundation): @@ -104,7 +95,6 @@ class RiverineInundation(Inundation): class WaterRisk(Hazard): kind = HazardKind.CHRONIC - pass class WaterTemperature(ChronicHeat): @@ -113,7 +103,6 @@ class WaterTemperature(ChronicHeat): class Wind(Hazard): kind = HazardKind.ACUTE - pass class Subsidence(Hazard): @@ -121,7 +110,6 @@ class Subsidence(Hazard): indicator_data = { "subsidence_probability": IndicatorData.PARAMETERS, } - pass def all_hazards() -> list[type[Hazard]]: @@ -155,7 +143,6 @@ class Landslide(Hazard): indicator_data = { "landslide_probability": IndicatorData.PARAMETERS, } - pass class Earthquake(Hazard): diff --git a/src/physrisk/kernel/impact.py b/src/physrisk/kernel/impact.py index cabfc31d..7439e1f1 100644 --- a/src/physrisk/kernel/impact.py +++ b/src/physrisk/kernel/impact.py @@ -1,7 +1,8 @@ import logging from collections import defaultdict +from collections.abc import Iterable, Sequence from dataclasses import dataclass -from typing import Dict, Iterable, List, NamedTuple, Optional, Sequence, Tuple, Union +from typing import NamedTuple from physrisk.data.scenario_year_resolution import ScenarioYear from physrisk.kernel.assets import Asset @@ -37,7 +38,7 @@ class ImpactKey(NamedTuple): # if the calculation is for a given scenario and year # impact_type: Optional[str] = None # consider adding: whether damage or disruption scenario: str - key_year: Optional[int] = None # this is None for 'historical' scenario + key_year: int | None = None # this is None for 'historical' scenario def __repr__(self) -> str: asset_id = self.asset.id if self.asset.id is not None else "no_id" @@ -50,24 +51,24 @@ def __repr__(self) -> str: @dataclass class AssetImpactResult: impact: ImpactDistrib - vulnerability: Optional[VulnerabilityDistrib] = None - event: Optional[HazardEventDistrib] = None - hazard_data: Optional[Sequence[HazardDataResponse]] = ( + vulnerability: VulnerabilityDistrib | None = None + event: HazardEventDistrib | None = None + hazard_data: Sequence[HazardDataResponse] | None = ( None # optional detailed results for drill-down ) -def calculate_impacts( # noqa: C901 +def calculate_impacts( assets: Iterable[Asset], hazard_model: HazardModel, vulnerability_models: VulnerabilityModels, *, scenarios: Sequence[str], years: Sequence[int], -) -> Dict[ImpactKey, List[AssetImpactResult]]: +) -> dict[ImpactKey, list[AssetImpactResult]]: """Calculate asset level impacts.""" - model_assets: Dict[DataRequester, List[Asset]] = defaultdict( + model_assets: dict[DataRequester, list[Asset]] = defaultdict( list ) # list of assets to be modelled using vulnerability model @@ -76,7 +77,7 @@ def calculate_impacts( # noqa: C901 mappings = vulnerability_models.vuln_model_for_asset_of_type(asset_type) for mapping in mappings: model_assets[mapping].append(asset) - results: Dict[ImpactKey, List[AssetImpactResult]] = {} + results: dict[ImpactKey, list[AssetImpactResult]] = {} scen_year_asset_requests, responses = _download_data_consolidated( hazard_model, model_assets, scenarios, years @@ -109,7 +110,7 @@ def calculate_impacts( # noqa: C901 # print("%r generated an exception: %s" % (tag, exc)) logging.info("Calculating impacts") - summary: Dict[str, List[Tuple[str, int, str]]] = defaultdict(list) + summary: dict[str, list[tuple[str, int, str]]] = defaultdict(list) for model, assets in model_assets.items(): assert isinstance(model, VulnerabilityModelBase) summary[model.hazard_type.__name__].append( @@ -191,7 +192,7 @@ def calculate_impacts( # noqa: C901 def _download_data_consolidated( hazard_model: HazardModel, - requester_assets: Dict[DataRequester, List[Asset]], + requester_assets: dict[DataRequester, list[Asset]], scenarios: Sequence[str], years: Sequence[int], ): @@ -200,18 +201,18 @@ def _download_data_consolidated( note that key for a single request is (requester, asset). """ # the list of requests for each requester and asset - scen_year_asset_requests: Dict[ + scen_year_asset_requests: dict[ ScenarioYear, - Dict[ - Tuple[DataRequester, Asset], - Union[HazardDataRequest, Sequence[HazardDataRequest]], + dict[ + tuple[DataRequester, Asset], + HazardDataRequest | Sequence[HazardDataRequest], ], ] = {} for scenario in scenarios: for year in [-1] if scenario == "historical" else years: - asset_requests: Dict[ - Tuple[DataRequester, Asset], - Union[HazardDataRequest, Sequence[HazardDataRequest]], + asset_requests: dict[ + tuple[DataRequester, Asset], + HazardDataRequest | Sequence[HazardDataRequest], ] = {} for requester, assets in requester_assets.items(): for asset in assets: diff --git a/src/physrisk/kernel/impact_aggregator.py b/src/physrisk/kernel/impact_aggregator.py index 5ec2d53e..694f6d3d 100644 --- a/src/physrisk/kernel/impact_aggregator.py +++ b/src/physrisk/kernel/impact_aggregator.py @@ -1,28 +1,27 @@ +import logging from collections import defaultdict +from collections.abc import Generator from dataclasses import dataclass -import logging -from typing import Generator, NamedTuple, Optional -from typing_extensions import Protocol +from typing import NamedTuple import numpy as np +from typing_extensions import Protocol -from physrisk.kernel.hazards import Hazard -from physrisk.kernel.impact_distrib import EmptyImpactDistrib, ImpactDistrib, ImpactType -from physrisk.kernel.insurance_model import InsuranceDataProvider -from physrisk.kernel.risk import Quantity, QuantityType, RiskQuantityKey from physrisk.kernel.assets import Asset from physrisk.kernel.curve import ExceedanceCurve from physrisk.kernel.financial_model import FinancialDataProvider, FinancialModel -from physrisk.kernel.hazards import HazardKind +from physrisk.kernel.hazards import Hazard, HazardKind from physrisk.kernel.impact import AssetImpactResult, ImpactKey - +from physrisk.kernel.impact_distrib import EmptyImpactDistrib, ImpactDistrib, ImpactType +from physrisk.kernel.insurance_model import InsuranceDataProvider +from physrisk.kernel.risk import Quantity, QuantityType, RiskQuantityKey logger = logging.getLogger(__name__) class AggregationKeys(Protocol): def get_aggregation_keys( - self, asset: Asset, hazard_type: Optional[type[Hazard]], quantity: QuantityType + self, asset: Asset, hazard_type: type[Hazard] | None, quantity: QuantityType ) -> list[RiskQuantityKey]: ... """Returns a list of keys for aggregation. @@ -33,14 +32,14 @@ def get_aggregation_keys( class ContributionKey(NamedTuple): # key describing the contribution to the aggregated result - asset_id: Optional[str] = None - quantity: Optional[QuantityType] = None - hazard_type: Optional[type[Hazard]] = None + asset_id: str | None = None + quantity: QuantityType | None = None + hazard_type: type[Hazard] | None = None class Aggregator: def __init__( - self, key_provider: AggregationKeys, size: Optional[tuple[int, ...]] = None + self, key_provider: AggregationKeys, size: tuple[int, ...] | None = None ): self.key_provider = key_provider self.aggregation_pools: dict[RiskQuantityKey, np.ndarray] = {} @@ -50,7 +49,7 @@ def __init__( ) self.size = size - def zero(self, shape: Optional[tuple[int, ...]] = None) -> None: + def zero(self, shape: tuple[int, ...] | None = None) -> None: if shape is not None: for k in self.aggregation_pools.keys(): self.aggregation_pools[k] = np.zeros(shape) @@ -61,10 +60,10 @@ def zero(self, shape: Optional[tuple[int, ...]] = None) -> None: def aggregate( self, asset: Asset, - hazard_type: Optional[type[Hazard]], + hazard_type: type[Hazard] | None, quantity: QuantityType, values: np.ndarray, - slice: Optional[tuple[slice, ...]] = None, + slice: tuple[slice, ...] | None = None, ): for key in self.key_provider.get_aggregation_keys(asset, hazard_type, quantity): if key not in self.aggregation_pools: @@ -89,7 +88,7 @@ class ByAssetAggregationKeys(AggregationKeys): """Aggregator that provides aggregated results by asset and quantity type.""" def get_aggregation_keys( - self, asset: Asset, hazard_type: Optional[type[Hazard]], quantity: QuantityType + self, asset: Asset, hazard_type: type[Hazard] | None, quantity: QuantityType ) -> list[RiskQuantityKey]: return [ RiskQuantityKey(asset=asset, quantity=quantity), @@ -101,7 +100,7 @@ class PortfolioAggregationKeys(AggregationKeys): hazard type but split by portfolio via the asset's 'aggregation_id' attribute.""" def get_aggregation_keys( - self, asset: Asset, hazard_type: Optional[type[Hazard]], quantity: QuantityType + self, asset: Asset, hazard_type: type[Hazard] | None, quantity: QuantityType ) -> list[RiskQuantityKey]: aggregation_id = asset.aggregation_id return [ @@ -114,7 +113,7 @@ class HazardQuantityAggregationKeys(AggregationKeys): portfolio via the asset's 'aggregation_id' attribute.""" def get_aggregation_keys( - self, asset: Asset, hazard_type: Optional[type[Hazard]], quantity: QuantityType + self, asset: Asset, hazard_type: type[Hazard] | None, quantity: QuantityType ) -> list[RiskQuantityKey]: aggregation_id = asset.aggregation_id return [ @@ -131,8 +130,8 @@ class AcuteImpactCurves(NamedTuple): impact (fraction of TIV) and/or a disruption impact (fraction of revenue), both driven by the same underlying event severity.""" - damage: Optional[tuple[ImpactDistrib, ExceedanceCurve]] = None - disruption: Optional[tuple[ImpactDistrib, ExceedanceCurve]] = None + damage: tuple[ImpactDistrib, ExceedanceCurve] | None = None + disruption: tuple[ImpactDistrib, ExceedanceCurve] | None = None @dataclass @@ -152,7 +151,7 @@ class _SimulationInputs: def _classify_impacts( impacts: dict[ImpactKey, list[AssetImpactResult]], scenario: str, - key_year: Optional[int], + key_year: int | None, ) -> tuple[ set[Asset], dict[type[Hazard], set[Asset]], @@ -269,7 +268,7 @@ def _build_chronic_arrays( all_assets_list: list[Asset], impacts: dict[ImpactKey, list[AssetImpactResult]], scenario: str, - key_year: Optional[int], + key_year: int | None, financial_model: FinancialModel, ) -> dict[type[Hazard], np.ndarray]: """Pre-compute per-asset chronic impact deltas (future minus historical) for each chronic hazard.""" @@ -333,7 +332,7 @@ def _run_simulation( asset_revenue: dict[Asset, float], n_events: int = 50000, event_batch_sz: int = 1000, - insurance_provider: Optional[InsuranceDataProvider] = None, + insurance_provider: InsuranceDataProvider | None = None, ) -> dict[RiskQuantityKey, np.ndarray]: """Run Monte Carlo simulation; return per-event impact arrays keyed by RiskQuantityKey.""" severity_provider = UncorrelatedEventSeverityProvider( @@ -690,8 +689,8 @@ def _summarise_results( asset_revenue: dict[Asset, float], ) -> dict[RiskQuantityKey, Quantity]: """Normalise per-event arrays by portfolio totals and build exceedance-curve summaries.""" - sum_tiv_by_aggregation_id: dict[Optional[str], float] = defaultdict(float) - sum_revenue_by_aggregation_id: dict[Optional[str], float] = defaultdict(float) + sum_tiv_by_aggregation_id: dict[str | None, float] = defaultdict(float) + sum_revenue_by_aggregation_id: dict[str | None, float] = defaultdict(float) for asset, tiv in asset_tiv.items(): sum_tiv_by_aggregation_id[asset.aggregation_id] += tiv for asset, revenue in asset_revenue.items(): @@ -725,10 +724,10 @@ def aggregate_impacts( impacts: dict[ImpactKey, list[AssetImpactResult]], financial_model: FinancialModel, scenario: str, - key_year: Optional[int], + key_year: int | None, n_events: int = 50000, event_batch_sz: int = 1000, - insurance_provider: Optional[InsuranceDataProvider] = None, + insurance_provider: InsuranceDataProvider | None = None, ) -> dict[RiskQuantityKey, Quantity]: """Aggregate impacts over assets and hazards for a given scenario and year. For acute hazards, i.e. hazards associated with an event, a Monte Carlo approach is used whereby a large number of @@ -890,7 +889,7 @@ def next_claim_payments_in_batch( class SimpleEventInsuranceProvider(EventInsuranceProvider): def __init__( self, - insurance: Optional[InsuranceDataProvider], + insurance: InsuranceDataProvider | None, financials: FinancialDataProvider, all_acute_impacted_assets: list[Asset], ): @@ -970,7 +969,7 @@ def severity_zone_to_asset_indices( return self.severity_zone_to_asset_indices_by_hazard[hazard_type] -class Events(object): +class Events: def __init__( self, hazard_type: type[Hazard], diff --git a/src/physrisk/kernel/impact_distrib.py b/src/physrisk/kernel/impact_distrib.py index 67f86215..9c7a9ae5 100644 --- a/src/physrisk/kernel/impact_distrib.py +++ b/src/physrisk/kernel/impact_distrib.py @@ -1,6 +1,6 @@ -from enum import Enum import sys -from typing import Sequence, Type, Union +from collections.abc import Sequence +from enum import Enum import numpy as np @@ -17,19 +17,19 @@ class ImpactDistrib: """Impact distributions specific to an asset.""" __slots__ = [ + "_hazard_indicator_id", "_hazard_type", "_impact_bin_edges", - "_probabilities", - "_hazard_indicator_id", "_impact_type", "_path", + "_probabilities", ] def __init__( self, - hazard_type: Type[Hazard], - impact_bin_edges: Union[Sequence[float], np.ndarray], - probabilities: Union[Sequence[float], np.ndarray], + hazard_type: type[Hazard], + impact_bin_edges: Sequence[float] | np.ndarray, + probabilities: Sequence[float] | np.ndarray, hazard_indicator_id: str, impact_type: ImpactType = ImpactType.damage, path: Sequence[str] = [], diff --git a/src/physrisk/kernel/risk.py b/src/physrisk/kernel/risk.py index 40506e7a..4fe0d86e 100644 --- a/src/physrisk/kernel/risk.py +++ b/src/physrisk/kernel/risk.py @@ -1,18 +1,11 @@ from collections import defaultdict +from collections.abc import Sequence from dataclasses import dataclass from enum import Enum from typing import ( Any, - Dict, - List, NamedTuple, - Optional, Protocol, - Sequence, - Set, - Tuple, - Type, - Union, ) import numpy as np @@ -34,17 +27,15 @@ # import concurrent.futures -Impact = Dict[Tuple[Asset, type], AssetImpactResult] # the key is (Asset, Hazard type) +Impact = dict[tuple[Asset, type], AssetImpactResult] # the key is (Asset, Hazard type) # portfolio_quantities keyed by (scenario, year) then by RiskQuantityKey -PortfolioQuantities = Dict[ - Tuple[str, Optional[int]], Dict["RiskQuantityKey", "Quantity"] -] +PortfolioQuantities = dict[tuple[str, int | None], dict["RiskQuantityKey", "Quantity"]] class BatchId(NamedTuple): scenario: str - key_year: Optional[int] + key_year: int | None class QuantityType(str, Enum): @@ -56,15 +47,15 @@ class QuantityType(str, Enum): class RiskQuantityKey(NamedTuple): - quantity: Optional[QuantityType] = None - asset: Optional[Asset] = None - aggregation_id: Optional[str] = None - hazard_type: Optional[type[Hazard]] = None + quantity: QuantityType | None = None + asset: Asset | None = None + aggregation_id: str | None = None + hazard_type: type[Hazard] | None = None @dataclass(frozen=True) class Quantity: - values: Optional[npt.NDArray[np.floating[Any]]] + values: npt.NDArray[np.floating[Any]] | None exceedance_curve: ExceedanceCurve mean: float semi_standard_deviation: float @@ -83,12 +74,12 @@ class MeasureKey(NamedTuple): None for asset-level measures and for the pooled portfolio total. """ - asset: Optional[Asset] + asset: Asset | None scenario: str - year: Optional[int] - hazard_type: Optional[Type[Hazard]] - hazard_indicator_id: Optional[str] = None - aggregation_id: Optional[str] = None + year: int | None + hazard_type: type[Hazard] | None + hazard_indicator_id: str | None = None + aggregation_id: str | None = None @dataclass @@ -96,7 +87,7 @@ class Measure: score: int measure_0: float definition: ScoreBasedRiskMeasureDefinition # reference to single instance of ScoreBasedRiskMeasureDefinition - measure_1: Optional[float] = None + measure_1: float | None = None class PortfolioRiskMeasureCalculator(Protocol): @@ -106,7 +97,7 @@ class PortfolioRiskMeasureCalculator(Protocol): """ def get_definition( - self, hazard_type: Optional[type[Hazard]] = None + self, hazard_type: type[Hazard] | None = None ) -> ScoreBasedRiskMeasureDefinition: ... def calculate_risk_measures( @@ -124,7 +115,7 @@ def portfolio_quantities_required(self) -> bool: ... class NullAssetBasedPortfolioRiskMeasureCalculator(PortfolioRiskMeasureCalculator): """Calculates portfolio score-based risk measures from asset-level score-based risk measures only.""" - def get_definition(self, hazard_type: Optional[Type[Hazard]] = None): + def get_definition(self, hazard_type: type[Hazard] | None = None): return ScoreBasedRiskMeasureDefinition( hazard_types=[], values=[], underlying_measures=[] ) @@ -171,7 +162,6 @@ def calculate_risk_measures( financial_data_provider: FinancialDataProvider, ): """Calculate risk measures for a set of assets, scenarios, and years.""" - ... def _calculate_all_impacts( self, @@ -197,7 +187,7 @@ def _calculate_all_impacts( class RiskMeasureCalculator(Protocol): def calc_measure( self, - hazard_type: Type[Hazard], + hazard_type: type[Hazard], base_impacts: Sequence[AssetImpactResult], impacts: Sequence[AssetImpactResult], ) -> Measure | dict[str | None, Measure]: @@ -221,18 +211,18 @@ def calc_measure( ... def get_definition( - self, hazard_type: Type[Hazard], hazard_indicator_id: Optional[str] = None + self, hazard_type: type[Hazard], hazard_indicator_id: str | None = None ) -> ScoreBasedRiskMeasureDefinition: ... - def supported_hazards(self) -> Set[type]: ... + def supported_hazards(self) -> set[type]: ... def aggregate_risk_measures( self, - measures: Dict[MeasureKey, Measure], + measures: dict[MeasureKey, Measure], assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int], - ) -> Dict[MeasureKey, Measure]: + ) -> dict[MeasureKey, Measure]: """The RiskMeasureCalculator can aggregate child hazards into parent hazards or proxy one hazard to another. If no aggregation or proxying is needed, the measures input is returned unchanged. @@ -253,14 +243,13 @@ class RiskMeasuresFactory(Protocol): def asset_calculators( self, use_case_id: str - ) -> Dict[Type[Asset], RiskMeasureCalculator]: + ) -> dict[type[Asset], RiskMeasureCalculator]: """Get risk measure calculators for asset types. Args: use_case_id (Optional[str]): Optional use case ID to filter calculators. """ - pass def portfolio_calculator(self, use_case_id: str) -> PortfolioRiskMeasureCalculator: pass @@ -273,7 +262,7 @@ def __init__( self, hazard_model: HazardModel, vulnerability_models: VulnerabilityModels, - measure_calculators: Dict[type[Asset], RiskMeasureCalculator], + measure_calculators: dict[type[Asset], RiskMeasureCalculator], portfolio_measure_calculator: PortfolioRiskMeasureCalculator = NullAssetBasedPortfolioRiskMeasureCalculator(), ): """Risk model that calculates risk measures at asset level and portfolio level. @@ -341,7 +330,7 @@ def populate_measure_definitions( ) if item is not None ), - key=lambda c: list(sorted(c.hazard_types))[0], + key=lambda c: sorted(c.hazard_types)[0], ) ) } @@ -355,9 +344,9 @@ def populate_measure_definitions( ) def get_measure_id( - measure_calc: Union[RiskMeasureCalculator, None], + measure_calc: RiskMeasureCalculator | None, hazard_type: type[Hazard], - hazard_indicator_id: Optional[str] = None, + hazard_indicator_id: str | None = None, ): if measure_calc is None: return "na" @@ -425,9 +414,9 @@ def calculate_risk_measures( impacts = self._calculate_all_impacts( assets, scenarios, years, include_histo=True ) - measures: Dict[MeasureKey, Measure] = {} - aggregated_measures: Dict[MeasureKey, Measure] = {} - measure_calc_assets: Dict[RiskMeasureCalculator, List[Asset]] = defaultdict( + measures: dict[MeasureKey, Measure] = {} + aggregated_measures: dict[MeasureKey, Measure] = {} + measure_calc_assets: dict[RiskMeasureCalculator, list[Asset]] = defaultdict( list ) for asset in assets: @@ -495,7 +484,7 @@ def calculate_risk_measures( aggregated_measures.update(portfolio_measures) return impacts, aggregated_measures, portfolio_quantities - def _calculator_for_asset(self, asset: Asset) -> Optional[RiskMeasureCalculator]: + def _calculator_for_asset(self, asset: Asset) -> RiskMeasureCalculator | None: return self._asset_level_measure_calculators.get( type(asset), self._asset_level_measure_calculators.get(Asset, None) ) diff --git a/src/physrisk/kernel/vulnerability_distrib.py b/src/physrisk/kernel/vulnerability_distrib.py index a0762953..46fb90b4 100644 --- a/src/physrisk/kernel/vulnerability_distrib.py +++ b/src/physrisk/kernel/vulnerability_distrib.py @@ -1,5 +1,3 @@ -from typing import List, Union - import numpy as np from physrisk.kernel.hazards import Hazard @@ -13,19 +11,19 @@ class VulnerabilityDistrib: """Vulnerability distribution as a discrete matrix.""" __slots__ = [ + "_hazard_indicator_id", "_hazard_type", - "_intensity_bins", "_impact_bins", + "_intensity_bins", "_prob_matrix", - "_hazard_indicator_id", ] def __init__( self, hazard_type: type[Hazard], - intensity_bins: Union[List[float], np.ndarray], - impact_bins: Union[List[float], np.ndarray], - prob_matrix: Union[List[List[float]], np.ndarray], + intensity_bins: list[float] | np.ndarray, + impact_bins: list[float] | np.ndarray, + prob_matrix: list[list[float]] | np.ndarray, hazard_indicator_id: str, ): """Create a new vulnerability distribution. diff --git a/src/physrisk/kernel/vulnerability_matrix_provider.py b/src/physrisk/kernel/vulnerability_matrix_provider.py index 60317b97..df9d9624 100644 --- a/src/physrisk/kernel/vulnerability_matrix_provider.py +++ b/src/physrisk/kernel/vulnerability_matrix_provider.py @@ -1,4 +1,4 @@ -from typing import Callable, Sequence +from collections.abc import Callable, Sequence import numpy as np @@ -10,7 +10,7 @@ def __init__(self, mean, std_dev): class VulnMatrixProvider: - __slots__ = ["intensity_bin_centres", "impact_cdfs"] + __slots__ = ["impact_cdfs", "intensity_bin_centres"] def __init__( self, diff --git a/src/physrisk/kernel/vulnerability_model.py b/src/physrisk/kernel/vulnerability_model.py index 0993599a..d08731c4 100644 --- a/src/physrisk/kernel/vulnerability_model.py +++ b/src/physrisk/kernel/vulnerability_model.py @@ -1,14 +1,9 @@ import importlib.resources import json from abc import ABC, abstractmethod +from collections.abc import Sequence from typing import ( - Dict, - Optional, Protocol, - Sequence, - Tuple, - Type, - Union, ) import numpy as np @@ -84,7 +79,7 @@ def beta_distrib(mean, std, scaling_factor): class DataRequester(Protocol): def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: ... + ) -> HazardDataRequest | Sequence[HazardDataRequest]: ... class EventBased(Protocol): @@ -107,7 +102,7 @@ def __init__( @abstractmethod def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Provide the hazard event data requests. It will return one or more HazardDataRequest as required to calculate the @@ -123,7 +118,7 @@ def get_impact( class VulnerabilityModels(Protocol): def vuln_model_for_asset_of_type( - self, type: Type[Asset] + self, type: type[Asset] ) -> Sequence[VulnerabilityModelBase]: """Return for a given asset type the vulnerability models for each hazard required. @@ -156,10 +151,10 @@ def vulnerability_models( class DictBasedVulnerabilityModels(VulnerabilityModels): - def __init__(self, models: Dict[Type[Asset], Sequence[VulnerabilityModelBase]]): + def __init__(self, models: dict[type[Asset], Sequence[VulnerabilityModelBase]]): self.models = models - def vuln_model_for_asset_of_type(self, type: Type[Asset]): + def vuln_model_for_asset_of_type(self, type: type[Asset]): return self.models[type] @@ -174,7 +169,7 @@ def __init__(self, indicator_id: str, hazard_type: type, impact_type: ImpactType @abstractmethod def get_distributions( self, asset: Asset, event_data_responses: Sequence[HazardDataResponse] - ) -> Tuple[VulnerabilityDistrib, HazardEventDistrib]: + ) -> tuple[VulnerabilityDistrib, HazardEventDistrib]: """Return distributions for asset: VulnerabilityDistrib and HazardEventDistrib. The hazard event data is used to do this. @@ -192,7 +187,7 @@ def get_impact(self, asset: Asset, data_responses: Sequence[HazardDataResponse]) def get_impact_details( self, asset: Asset, data_responses: Sequence[HazardDataResponse] - ) -> Tuple[ImpactDistrib, VulnerabilityDistrib, HazardEventDistrib]: + ) -> tuple[ImpactDistrib, VulnerabilityDistrib, HazardEventDistrib]: """Return impact distribution along with vulnerability and hazard event distributions used to infer this. Args: @@ -228,7 +223,7 @@ def __init__( hazard_type: type, impact_type: ImpactType, impact_bin_edges, - buffer: Optional[int] = None, + buffer: int | None = None, ): super().__init__(indicator_id, hazard_type, impact_type) self.impact_bin_edges = impact_bin_edges @@ -236,7 +231,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: return HazardDataRequest( self.hazard_type, asset.longitude, @@ -250,7 +245,7 @@ def get_data_requests( def get_distributions( self, asset: Asset, event_data_responses: Sequence[HazardDataResponse] - ) -> Tuple[VulnerabilityDistrib, HazardEventDistrib]: + ) -> tuple[VulnerabilityDistrib, HazardEventDistrib]: (event_data,) = event_data_responses assert isinstance(event_data, HazardEventDataResponse) @@ -321,7 +316,7 @@ def __init__( damage_curve_impacts: Sequence[float], indicator_id: str, impact_type: ImpactType, - buffer: Optional[int] = None, + buffer: int | None = None, ): """A vulnerability model that requires only specification of a damage/disruption curve. This simple model contains no uncertainty around damage/disruption. The damage curve is passed via the @@ -348,7 +343,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: return HazardDataRequest( self.hazard_type, asset.longitude, @@ -362,7 +357,7 @@ def get_data_requests( def get_distributions( self, asset: Asset, event_data_responses: Sequence[HazardDataResponse] - ) -> Tuple[VulnerabilityDistrib, HazardEventDistrib]: + ) -> tuple[VulnerabilityDistrib, HazardEventDistrib]: (event_data,) = event_data_responses assert isinstance(event_data, HazardEventDataResponse) diff --git a/src/physrisk/requests.py b/src/physrisk/requests.py index 7364e281..507cf406 100644 --- a/src/physrisk/requests.py +++ b/src/physrisk/requests.py @@ -1,28 +1,27 @@ -from collections import defaultdict import importlib.resources import json import math +from collections import defaultdict +from collections.abc import Callable, Sequence from typing import ( Any, - Callable, - Dict, - List, - Optional, - Sequence, - Type, - Union, ) import numpy as np -from physrisk.api.v1.example_portfolios import ExamplePortfoliosResponse import physrisk.data.static.example_portfolios import physrisk.kernel.hazard_model +from physrisk.api.v1.availability_sources import ( + AvailabilitySourcesRequest, + AvailabilitySourcesResponse, + HazardTypeAvailability, +) from physrisk.api.v1.common import ( Distribution, ExceedanceCurve, VulnerabilityDistrib, ) +from physrisk.api.v1.example_portfolios import ExamplePortfoliosResponse from physrisk.api.v1.exposure_req_resp import ( AssetExposure, AssetExposureRequest, @@ -40,25 +39,24 @@ from physrisk.data.static.oed_occupancy import OED_OCCUPANCY_CODES from physrisk.data.static.scenarios import scenario_description from physrisk.data.zarr_reader import ZarrReader +from physrisk.kernel.assets import Asset, all_asset_types from physrisk.kernel.exposure import JupterExposureMeasure, calculate_exposures from physrisk.kernel.hazards import Hazard, all_hazards, hazard_class from physrisk.kernel.impact import AssetImpactResult, ImpactKey # , ImpactKey from physrisk.kernel.impact_distrib import EmptyImpactDistrib, PlaceholderImpactDistrib from physrisk.kernel.risk import ( - PortfolioRiskModel, Measure, MeasureKey, + NullAssetBasedPortfolioRiskMeasureCalculator, PortfolioQuantities, + PortfolioRiskMeasureCalculator, + PortfolioRiskModel, Quantity, QuantityType, RiskMeasureCalculator, RiskMeasuresFactory, RiskQuantityKey, ) -from physrisk.kernel.risk import PortfolioRiskMeasureCalculator -from physrisk.kernel.risk import ( - NullAssetBasedPortfolioRiskMeasureCalculator, -) from physrisk.kernel.vulnerability_model import ( VulnerabilityModels, VulnerabilityModelsFactory, @@ -84,48 +82,39 @@ StaticInformationResponse, ) from .api.v1.impact_req_resp import ( - AssetMeasuresSpecification, - CalculationDetails, AssetImpactRequest, AssetImpactResponse, AssetLevelImpact, + AssetMeasuresSpecification, Assets, AssetSingleImpact, + CalculationDetails, PortfolioImpact, - ScoreBasedRiskMeasure, - RiskMeasuresForAssets, -) -from .api.v1.impact_req_resp import ImpactKey as APIImpactKey -from .api.v1.impact_req_resp import PortfolioImpactKey -from .api.v1.impact_req_resp import ( + PortfolioImpactKey, RiskMeasureDefinition, RiskMeasureKey, RiskMeasures, - ScoreBasedRiskMeasuresForAssets, + RiskMeasuresForAssets, + ScoreBasedRiskMeasure, ScoreBasedRiskMeasureDefinition, ScoreBasedRiskMeasureSetDefinition, + ScoreBasedRiskMeasuresForAssets, ) -from physrisk.api.v1.availability_sources import ( - AvailabilitySourcesRequest, - AvailabilitySourcesResponse, - HazardTypeAvailability, -) +from .api.v1.impact_req_resp import ImpactKey as APIImpactKey from .data.inventory import EmbeddedInventory, Inventory -from physrisk.kernel.assets import Asset, all_asset_types from .kernel import calculation as calc from .kernel.hazard_model import ( HazardDataRequest as hmHazardDataRequest, - HazardImageCreator, ) from .kernel.hazard_model import HazardEventDataResponse as hmHazardEventDataResponse from .kernel.hazard_model import ( + HazardImageCreator, HazardModel, HazardModelFactory, HazardParameterDataResponse, ) - -Colormaps = Dict[str, Any] +Colormaps = dict[str, Any] class Requester: @@ -140,7 +129,7 @@ def __init__( reader: ZarrReader, colormaps: Colormaps, measures_factory: RiskMeasuresFactory, - json_encoder_cls: Type[json.JSONEncoder] = PhysriskDefaultEncoder, + json_encoder_cls: type[json.JSONEncoder] = PhysriskDefaultEncoder, sig_figures: int = -1, enforce_permissions: bool = True, ): @@ -327,8 +316,8 @@ def get_asset_impacts(self, request: AssetImpactRequest) -> AssetImpactResponse: sig_figures=self.round_sig_figures, ) - def get_image(self, request_or_dict: Union[HazardImageRequest, Dict]): - if isinstance(request_or_dict, Dict): + def get_image(self, request_or_dict: HazardImageRequest | dict): + if isinstance(request_or_dict, dict): request = HazardImageRequest(**request_or_dict) else: request = request_or_dict @@ -398,17 +387,17 @@ def get_image_info(self, request: HazardImageInfoRequest): def dumps(self, dict): return json.dumps(dict, cls=self.json_encoder_cls) - def round_sig_figures(self, x: Union[np.ndarray, float]): + def round_sig_figures(self, x: np.ndarray | float): if self.sig_figures == -1: return x return encoder.sig_figures(x, self.sig_figures) def _create_inventory( - reader: Optional[InventoryReader] = None, sources: Optional[List[str]] = None + reader: InventoryReader | None = None, sources: list[str] | None = None ): - resources: List[HazardResource] = [] - colormaps: Dict[str, Dict[str, Any]] = {} + resources: list[HazardResource] = [] + colormaps: dict[str, dict[str, Any]] = {} request_sources = ["embedded"] if sources is None else [s.lower() for s in sources] for source in request_sources: if source == "embedded": @@ -423,7 +412,7 @@ def _create_inventory( return Inventory(resources) -def _read_permitted(group_ids: List[str], resource: HazardResource): +def _read_permitted(group_ids: list[str], resource: HazardResource): """Check whether HazardResource is available to the user. Args: @@ -459,9 +448,7 @@ def _get_hazard_data_description( def _get_hazard_data( request: HazardDataRequest, hazard_model: HazardModel, - sig_figures: Callable[ - [Union[np.ndarray, float]], Union[float, np.ndarray] - ] = lambda x: x, + sig_figures: Callable[[np.ndarray | float], float | np.ndarray] = lambda x: x, ): # if any( # not _read_permitted(request.group_ids, inventory.resources_by_type_id[(i.event_type, i.model)][0]) @@ -547,7 +534,7 @@ def _get_hazard_data( def create_assets( api_assets: Assets, - assets: Optional[List[Asset]] = None, + assets: list[Asset] | None = None, asset_factory: AssetFactory = DefaultAssetFactory(), ): """Create list of Asset objects from the Assets API object:""" @@ -567,7 +554,7 @@ def create_assets( def _get_asset_exposures( request: AssetExposureRequest, hazard_model: HazardModel, - assets: Optional[List[Asset]] = None, + assets: list[Asset] | None = None, asset_factory: AssetFactory = DefaultAssetFactory(), ): _assets, _ = create_assets(request.assets, assets, asset_factory) @@ -594,12 +581,10 @@ def _get_asset_impacts( hazard_model: HazardModel, vulnerability_models: VulnerabilityModels, asset_factory: AssetFactory = DefaultAssetFactory(), - measure_calculators: Optional[Dict[Type[Asset], RiskMeasureCalculator]] = None, - portfolio_measure_calculator: Optional[PortfolioRiskMeasureCalculator] = None, - assets: Optional[List[Asset]] = None, - sig_figures: Callable[ - [Union[np.ndarray, float]], Union[np.ndarray, float] - ] = lambda x: x, + measure_calculators: dict[type[Asset], RiskMeasureCalculator] | None = None, + portfolio_measure_calculator: PortfolioRiskMeasureCalculator | None = None, + assets: list[Asset] | None = None, + sig_figures: Callable[[np.ndarray | float], np.ndarray | float] = lambda x: x, ): # we keep API definition of asset separate from internal Asset class; convert by reflection # based on asset_class: @@ -668,9 +653,9 @@ def _get_asset_impacts( # populated by calculate_risk_measures above. drilldown_req = request.measures_specification if drilldown_req is not None and portfolio_quantities: - drilldown_entries: List[RiskMeasuresForAssets] = [] + drilldown_entries: list[RiskMeasuresForAssets] = [] for scenario in scenarios: - key_years: List[Optional[int]] = ( + key_years: list[int | None] = ( [None] if scenario == "historical" else list(years) ) for key_year in key_years: @@ -727,9 +712,9 @@ def _get_asset_impacts( def _compile_portfolio_impacts( portfolio_quantities: PortfolioQuantities, - sig_figures: Callable[[Union[np.ndarray, float]], Union[np.ndarray, float]], -) -> Optional[List[PortfolioImpact]]: - results: List[PortfolioImpact] = [] + sig_figures: Callable[[np.ndarray | float], np.ndarray | float], +) -> list[PortfolioImpact] | None: + results: list[PortfolioImpact] = [] for (scenario, year), quantities in portfolio_quantities.items(): for rk, qty in quantities.items(): if rk.asset is not None: @@ -786,10 +771,10 @@ def _compile_portfolio_impacts( def _build_financial_measure_definitions( - measure_ids: List[str], quantity_types: List[str] -) -> List[RiskMeasureDefinition]: + measure_ids: list[str], quantity_types: list[str] +) -> list[RiskMeasureDefinition]: """Return RiskMeasureDefinition objects for each valid (base_id, quantity_type) combination.""" - defns: List[RiskMeasureDefinition] = [] + defns: list[RiskMeasureDefinition] = [] for base_id in measure_ids: if base_id not in _ALL_FINANCIAL_MEASURE_IDS: continue @@ -819,7 +804,7 @@ def _build_financial_measure_definitions( return defns -def _extract_financial_value(qty: Optional[Quantity], measure_id: str) -> float: +def _extract_financial_value(qty: Quantity | None, measure_id: str) -> float: """Extract a single scalar from a Quantity for the given measure_id; returns 0.0 if absent.""" if qty is None: return 0.0 @@ -836,12 +821,12 @@ def _extract_financial_value(qty: Optional[Quantity], measure_id: str) -> float: def _compile_asset_financial_impacts( drilldown_results: dict[RiskQuantityKey, Quantity], - assets: List[Asset], + assets: list[Asset], scenario: str, - year: Optional[int], + year: int | None, drilldown_req: AssetMeasuresSpecification, - sig_figures: Callable[[Union[np.ndarray, float]], Union[np.ndarray, float]], -) -> List[RiskMeasuresForAssets]: + sig_figures: Callable[[np.ndarray | float], np.ndarray | float], +) -> list[RiskMeasuresForAssets]: """Produce one RiskMeasuresForAssets entry per (measure_id, hazard_type, quantity_type). Per-asset arrays are aligned with risk_measures.asset_ids; assets with no impact for a @@ -855,7 +840,7 @@ def _compile_asset_financial_impacts( by_hazard_qty[(rk.hazard_type, rk.quantity)][rk.asset] = qty year_str = str(year) if year is not None else "" - entries: List[RiskMeasuresForAssets] = [] + entries: list[RiskMeasuresForAssets] = [] for measure_id in drilldown_req.measure_ids: if measure_id not in _ALL_FINANCIAL_MEASURE_IDS: @@ -897,12 +882,10 @@ def _compile_asset_financial_impacts( def _compile_asset_impacts( - impacts: Dict[ImpactKey, List[AssetImpactResult]], - assets: List[Asset], + impacts: dict[ImpactKey, list[AssetImpactResult]], + assets: list[Asset], include_calc_details: bool, - sig_figures: Callable[ - [Union[np.ndarray, float]], Union[np.ndarray, float] - ] = lambda x: x, + sig_figures: Callable[[np.ndarray | float], np.ndarray | float] = lambda x: x, ): """Convert (internal) list of AssetImpactResult objects to a list of AssetLevelImpact objects ready for serialization. @@ -916,7 +899,7 @@ def _compile_asset_impacts( Returns: List[AssetLevelImpact]: AssetImpactResult objects for serialization. """ - ordered_impacts: Dict[Asset, List[AssetSingleImpact]] = {} + ordered_impacts: dict[Asset, list[AssetSingleImpact]] = {} for asset in assets: ordered_impacts[asset] = [] for k, value in impacts.items(): @@ -1027,9 +1010,7 @@ def _create_risk_measures( assets: list[Asset], scenarios: Sequence[str], years: Sequence[int], - sig_figures: Callable[ - [Union[np.ndarray, float]], Union[np.ndarray, float] - ] = lambda x: x, + sig_figures: Callable[[np.ndarray | float], np.ndarray | float] = lambda x: x, hazard_type_indicators: dict[type[Hazard], set[str]] = {}, measure_ids_for_asset_drilldown: dict[tuple[type[Hazard], str], list[str]] = {}, ) -> RiskMeasures: @@ -1053,15 +1034,15 @@ def _create_risk_measures( """ nan_value = -9999.0 # Nan not part of JSON spec - hazard_types = set(k.hazard_type for k in measures.keys()) + hazard_types = set(k.hazard_type for k in measures) # hazard_types = all_hazards() portfolio_aggregation_ids = sorted( - {k.aggregation_id for k in measures.keys() if k.asset is None}, + {k.aggregation_id for k in measures if k.asset is None}, key=lambda a: a or "", ) measure_set_id = "measure_set_0" - measures_for_assets: List[ScoreBasedRiskMeasuresForAssets] = [] - measures_for_portfolio: List[ScoreBasedRiskMeasure] = [] + measures_for_assets: list[ScoreBasedRiskMeasuresForAssets] = [] + measures_for_portfolio: list[ScoreBasedRiskMeasure] = [] for hazard_type in sorted( hazard_types, key=lambda x: x.__name__ if x is not None else "" ): @@ -1248,8 +1229,8 @@ def _get_hazards_from_vulnerability_models( def _hazard_type_indicators(measures: dict[MeasureKey, Measure]): # the measures keys might contain hazard_indicator_id for drill-down: we check for this # and create a look-up of the hazard_indicator_ids used for each hazard type. - hazard_type_indicators: Dict[type[Hazard], set[str]] = defaultdict(set) - for k in measures.keys(): + hazard_type_indicators: dict[type[Hazard], set[str]] = defaultdict(set) + for k in measures: if k.hazard_type is not None and k.hazard_indicator_id is not None: hazard_type_indicators[k.hazard_type].add(k.hazard_indicator_id) return hazard_type_indicators diff --git a/src/physrisk/risk_models/generic_risk_model.py b/src/physrisk/risk_models/generic_risk_model.py index f60dfe9d..1eb72322 100644 --- a/src/physrisk/risk_models/generic_risk_model.py +++ b/src/physrisk/risk_models/generic_risk_model.py @@ -1,8 +1,11 @@ import math +from collections.abc import Sequence from dataclasses import dataclass -from typing import Any, Dict, Optional, Protocol, Sequence, Set, Type, Union +from typing import Any, Protocol import numpy as np +from pint import UnitRegistry + from physrisk.api.v1.impact_req_resp import ( RiskMeasureDefinition, RiskScoreValue, @@ -33,7 +36,6 @@ ImpactDistrib, ) from physrisk.kernel.risk import Measure, MeasureKey, RiskMeasureCalculator -from pint import UnitRegistry from physrisk.utils.units import needs_conversion ureg = UnitRegistry() @@ -53,7 +55,7 @@ def __call__( @dataclass class HazardIndicatorBounds: - hazard_type: Type[Hazard] + hazard_type: type[Hazard] indicator_id: str units: str indicator_return: float @@ -91,8 +93,8 @@ class GenericScoreBasedRiskMeasures(RiskMeasureCalculator): the point of view of loan origination or project financing. """ - _bounds: Dict[ - Type[Hazard], Union[HazardIndicatorBounds, ImpactBounds, ImpactBoundsJoint] + _bounds: dict[ + type[Hazard], HazardIndicatorBounds | ImpactBounds | ImpactBoundsJoint ] def __init__(self): @@ -103,13 +105,13 @@ def __init__(self): measure=self._aal_future, lower=[float("-inf"), 0.01/100., 0.2/100., 1./100., 5./100.], # applies to impact upper=[0.01/100., 0.2/100., 1./100., 5./100., float("inf")], - ) # noqa + ) chronic_bounds = ImpactBounds( categories=[Category.VERY_LOW, Category.LOW, Category.MEDIUM, Category.HIGH, Category.VERY_HIGH], measure=self._delta_aal, lower=[float("-inf"), 0.01/100., 1./100., 5./100., 10./100.], # applies to impact upper=[0.01/100, 1./100., 5./100., 10./100., float("inf")], - ) # noqa + ) # fmt: on # The one remaining example of an exposure-based measure is Precipitation, an exemplar special case. @@ -128,7 +130,7 @@ def __init__(self): units="mm/day", lower=[float("-inf"), 10, 100, 130, 160], upper=[10, 100, 130, 160, float("inf")], - ), # noqa + ), CoastalInundation: acute_bounds, PluvialInundation: acute_bounds, RiverineInundation: acute_bounds, @@ -558,11 +560,11 @@ def _delta_impact( return future_loss - histo_loss def get_definition( - self, hazard_type: type[Hazard], hazard_indicator_id: Optional[str] = None + self, hazard_type: type[Hazard], hazard_indicator_id: str | None = None ) -> ScoreBasedRiskMeasureDefinition: return self._definition_lookup.get(hazard_type, None) - def supported_hazards(self) -> Set[type]: + def supported_hazards(self) -> set[type]: return set( [ CoastalInundation, @@ -579,9 +581,9 @@ def supported_hazards(self) -> Set[type]: def aggregate_risk_measures( self, - measures: Dict[MeasureKey, Measure], + measures: dict[MeasureKey, Measure], assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int], - ) -> Dict[MeasureKey, Measure]: + ) -> dict[MeasureKey, Measure]: return measures diff --git a/src/physrisk/risk_models/loss_model.py b/src/physrisk/risk_models/loss_model.py index 1aff3101..e04503a6 100644 --- a/src/physrisk/risk_models/loss_model.py +++ b/src/physrisk/risk_models/loss_model.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Sequence +from collections.abc import Sequence import numpy as np @@ -7,8 +7,8 @@ from ..kernel.assets import Asset from ..kernel.calculation import ( - get_default_hazard_model, alternate_default_vulnerability_models_scores, + get_default_hazard_model, ) from ..kernel.financial_model import DefaultFinancialModel from ..kernel.hazard_model import HazardModel @@ -21,19 +21,19 @@ class Aggregator(ABC): @abstractmethod - def get_aggregation_keys(self, asset: Asset, impact: ImpactDistrib) -> List: ... + def get_aggregation_keys(self, asset: Asset, impact: ImpactDistrib) -> list: ... class DefaultAggregator(Aggregator): - def get_aggregation_keys(self, asset: Asset, impact: ImpactDistrib) -> List: + def get_aggregation_keys(self, asset: Asset, impact: ImpactDistrib) -> list: return [(impact.hazard_type.__name__), ("root")] class LossModel: def __init__( self, - hazard_model: Optional[HazardModel] = None, - vulnerability_models: Optional[VulnerabilityModels] = None, + hazard_model: HazardModel | None = None, + vulnerability_models: VulnerabilityModels | None = None, ): self.hazard_model = ( get_default_hazard_model() if hazard_model is None else hazard_model @@ -55,14 +55,14 @@ def get_financial_impacts( financial_model: DefaultFinancialModel, scenario: str, year: int, - aggregator: Optional[Aggregator] = None, + aggregator: Aggregator | None = None, currency: str = "EUR", sims: int = 100000, ): if aggregator is None: aggregator = DefaultAggregator() - aggregation_pools: Dict[str, np.ndarray] = {} + aggregation_pools: dict[str, np.ndarray] = {} results = calculate_impacts( assets, diff --git a/src/physrisk/risk_models/portfolio_risk_model.py b/src/physrisk/risk_models/portfolio_risk_model.py index 95e57c55..0e7df218 100644 --- a/src/physrisk/risk_models/portfolio_risk_model.py +++ b/src/physrisk/risk_models/portfolio_risk_model.py @@ -1,6 +1,6 @@ from collections import defaultdict +from collections.abc import Sequence from enum import Enum -from typing import Optional, Sequence import numpy as np import scipy.interpolate @@ -120,7 +120,7 @@ def __init__( self, n_events: int = 50000, event_batch_sz: int = 1000, - insurance_provider: Optional[InsuranceDataProvider] = None, + insurance_provider: InsuranceDataProvider | None = None, downtime_config: Sequence[DowntimeConfigItem] = [], ): self._n_events = n_events @@ -142,7 +142,7 @@ def __init__( ], ) - def get_definition(self, hazard_type: Optional[type[Hazard]] = None): + def get_definition(self, hazard_type: type[Hazard] | None = None): return self._definition def calculate_risk_measures( @@ -160,13 +160,13 @@ def calculate_risk_measures( impacts_by_year_scen: dict[tuple[str, int | None], list[MeasureKey]] = ( defaultdict(list) ) - for mk in asset_level_measures.keys(): + for mk in asset_level_measures: impacts_by_year_scen[(mk.scenario, mk.year)].append(mk) measures: dict[MeasureKey, Measure] = {} all_portfolio_quantities: dict[ tuple[str, int | None], dict[RiskQuantityKey, Quantity] ] = {} - for scenario, year in impacts_by_year_scen.keys(): + for scenario, year in impacts_by_year_scen: portfolio_quantities = aggregate_impacts( impacts, financial_model, diff --git a/src/physrisk/risk_models/risk_models.py b/src/physrisk/risk_models/risk_models.py index 83967636..12892fcf 100644 --- a/src/physrisk/risk_models/risk_models.py +++ b/src/physrisk/risk_models/risk_models.py @@ -1,5 +1,5 @@ +from collections.abc import Callable, Sequence from enum import Enum -from typing import Callable, Optional, Sequence, Set, Type from physrisk.api.v1.impact_req_resp import ( RiskMeasureDefinition, @@ -176,7 +176,7 @@ def _cooling_description(self, category: Category): def calc_measure( self, - hazard_type: Type[Hazard], + hazard_type: type[Hazard], base_impacts: Sequence[AssetImpactResult], impacts: Sequence[AssetImpactResult], ) -> Measure: @@ -260,9 +260,9 @@ def calc_measure_cooling( ) def get_definition( - self, hazard_type: type[Hazard], hazard_indicator_id: Optional[str] = None + self, hazard_type: type[Hazard], hazard_indicator_id: str | None = None ) -> ScoreBasedRiskMeasureDefinition: return self._definition_lookup.get(hazard_type, None) - def supported_hazards(self) -> Set[type]: + def supported_hazards(self) -> set[type]: return set([RiverineInundation, CoastalInundation, Wind, ChronicHeat]) diff --git a/src/physrisk/risk_models/score_based_portfolio_risk_model.py b/src/physrisk/risk_models/score_based_portfolio_risk_model.py index 544c9010..a7d7fbfb 100644 --- a/src/physrisk/risk_models/score_based_portfolio_risk_model.py +++ b/src/physrisk/risk_models/score_based_portfolio_risk_model.py @@ -1,6 +1,5 @@ -from collections import defaultdict import statistics -from typing import Optional +from collections import defaultdict from physrisk.api.v1.impact_req_resp import ( RiskMeasureDefinition, @@ -38,7 +37,7 @@ def __init__(self): ], ) - def get_definition(self, hazard_type: Optional[type[Hazard]] = None): + def get_definition(self, hazard_type: type[Hazard] | None = None): return self._definition def calculate_risk_measures( @@ -52,9 +51,9 @@ def calculate_risk_measures( ]: portfolio_measures: dict[MeasureKey, Measure] = {} measure_by_year_scen_agg: dict[ - tuple[str, int | None, Optional[str]], list[MeasureKey] + tuple[str, int | None, str | None], list[MeasureKey] ] = defaultdict(list) - for mk in asset_level_measures.keys(): + for mk in asset_level_measures: aggregation_id = mk.asset.aggregation_id if mk.asset is not None else None measure_by_year_scen_agg[(mk.scenario, mk.year, aggregation_id)].append(mk) for (scenario, year, aggregation_id), v in measure_by_year_scen_agg.items(): diff --git a/src/physrisk/utils/encoder.py b/src/physrisk/utils/encoder.py index 61ba2454..86174bb3 100644 --- a/src/physrisk/utils/encoder.py +++ b/src/physrisk/utils/encoder.py @@ -1,10 +1,10 @@ import json import math -from typing import Union + import numpy as np -def sig_figures(x: Union[np.ndarray, float], sf: int): +def sig_figures(x: np.ndarray | float, sf: int): """Round the array or float to sf significant figures. Why do we support this: is it not better to keep all numbers as floats until the end and then use formatting like {0:.6g} or similar only when displaying? diff --git a/src/physrisk/utils/event_loop.py b/src/physrisk/utils/event_loop.py index e82d6435..9cb69850 100644 --- a/src/physrisk/utils/event_loop.py +++ b/src/physrisk/utils/event_loop.py @@ -2,7 +2,6 @@ import os import threading from contextlib import contextmanager -from typing import Optional iothread = [None] # dedicated fsspec IO thread loop = [None] # global event loop for any non-async instance @@ -80,7 +79,7 @@ async def _runner(event: threading.Event, coro, result, timeout=None): event.set() -def run(coro, loop, timeout: Optional[float] = None): +def run(coro, loop, timeout: float | None = None): """Run a coroutine in the given event loop, waiting until this is complete or has timed out. This is blocking, typically used within a thread pool to execute a batch of IO requests executed in parallel. @@ -106,6 +105,6 @@ def run(coro, loop, timeout: Optional[float] = None): if timeout is not None: timeout -= 1 if timeout < 0: - raise asyncio.TimeoutError() + raise TimeoutError() return result[0] diff --git a/src/physrisk/utils/lazy.py b/src/physrisk/utils/lazy.py index 0c47aabe..00f3dab5 100644 --- a/src/physrisk/utils/lazy.py +++ b/src/physrisk/utils/lazy.py @@ -1,8 +1,9 @@ import importlib.util import sys +from collections.abc import Callable from threading import Lock from types import ModuleType -from typing import Callable, Generic +from typing import Generic from typing_extensions import TypeVar diff --git a/src/physrisk/vulnerability_models/chronic_heat_models.py b/src/physrisk/vulnerability_models/chronic_heat_models.py index ca550f37..f7b2ac3d 100644 --- a/src/physrisk/vulnerability_models/chronic_heat_models.py +++ b/src/physrisk/vulnerability_models/chronic_heat_models.py @@ -1,4 +1,5 @@ -from typing import List, Sequence, Union, cast +from collections.abc import Sequence +from typing import cast import numpy as np from scipy.stats import norm @@ -37,7 +38,7 @@ def __init__(self, indicator_id: str = "mean_degree_days/above/32c", delta=True) def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Request the hazard data needed by the vulnerability model for a specific asset (this is a Google-style doc string) @@ -131,7 +132,7 @@ def work_type_mapping(self): def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Request the hazard data needed by the vulnerability model for a specific asset (this is a Google-style doc string) @@ -245,19 +246,19 @@ def get_impact( if asset.type in ["low", "high"]: a_historical = ( wbgt_responses[0].parameter - - abs((wbgt_responses[2].parameter - wbgt_responses[0].parameter)) / 2 + - abs(wbgt_responses[2].parameter - wbgt_responses[0].parameter) / 2 ) b_historical = ( wbgt_responses[0].parameter - + abs((wbgt_responses[2].parameter - wbgt_responses[0].parameter)) / 2 + + abs(wbgt_responses[2].parameter - wbgt_responses[0].parameter) / 2 ) a_scenario = ( wbgt_responses[1].parameter - - abs((wbgt_responses[3].parameter - wbgt_responses[1].parameter)) / 2 + - abs(wbgt_responses[3].parameter - wbgt_responses[1].parameter) / 2 ) b_scenario = ( wbgt_responses[1].parameter - + abs((wbgt_responses[3].parameter - wbgt_responses[1].parameter)) / 2 + + abs(wbgt_responses[3].parameter - wbgt_responses[1].parameter) / 2 ) elif asset.type == "medium": a_historical = ( @@ -270,11 +271,11 @@ def get_impact( ) a_scenario = ( wbgt_responses[1].parameter - - abs((wbgt_responses[3].parameter - wbgt_responses[1].parameter)) / 2 + - abs(wbgt_responses[3].parameter - wbgt_responses[1].parameter) / 2 ) b_scenario = ( wbgt_responses[1].parameter - + abs((wbgt_responses[5].parameter - wbgt_responses[1].parameter)) / 2 + + abs(wbgt_responses[5].parameter - wbgt_responses[1].parameter) / 2 ) # Estimation of the variance @@ -319,7 +320,7 @@ def get_impact_distrib( fraction_loss_mean: float, fraction_loss_std: float, hazard_type: type, - hazard_paths: List[str], + hazard_paths: list[str], impact_type: ImpactType, hazard_indicator_id: str, ) -> ImpactDistrib: diff --git a/src/physrisk/vulnerability_models/config_based_impact_curves.py b/src/physrisk/vulnerability_models/config_based_impact_curves.py index dc7718c5..7b055bff 100644 --- a/src/physrisk/vulnerability_models/config_based_impact_curves.py +++ b/src/physrisk/vulnerability_models/config_based_impact_curves.py @@ -1,22 +1,16 @@ import re from abc import abstractmethod from collections import defaultdict +from collections.abc import Callable, Iterable, Sequence from dataclasses import dataclass from typing import ( Any, - Callable, - Dict, - Iterable, - Optional, - Sequence, - Set, - Union, ) import numpy as np - import pandas as pd from pydantic import BaseModel, ConfigDict, Field + from physrisk.kernel.assets import Asset from physrisk.kernel.curve import add_x_value_to_curve from physrisk.vulnerability_models.config_cdf_based_vuln_function import ( @@ -52,7 +46,7 @@ class VulnerabilityConfigItem(BaseModel): indicator_id: str = Field( "", description="Hazard indicator identifier.", examples=["flood_depth"] ) - indicator_units: Optional[str] = Field( + indicator_units: str | None = Field( "", description="Hazard indicator units.", examples=["metres"] ) impact_id: str = Field( @@ -60,41 +54,41 @@ class VulnerabilityConfigItem(BaseModel): description="Identifier for the impact type.", examples=["damage", "disruption"], ) - impact_units: Optional[str] = Field("", description="Units of the impact type.") + impact_units: str | None = Field("", description="Units of the impact type.") curve_type: str = Field( "indicator/piecewise_linear", description="Type of the curve expressing the vulnerability function.", examples=["indicator/piecewise_linear", "threshold/piecewise_linear"], ) - points_x: Union[Sequence[float], np.ndarray] = Field( + points_x: Sequence[float] | np.ndarray = Field( None, description="Array of x (indicator/threshold) components of points defining vulnerability " "curve/surface.", ) - points_y: Union[Sequence[float], np.ndarray] = Field( + points_y: Sequence[float] | np.ndarray = Field( [], description="Array of y (impact) components of points defining vulnerability curve/surface.", ) - points_z: Optional[Union[Sequence[float], np.ndarray]] = Field( + points_z: Sequence[float] | np.ndarray | None = Field( None, description="Describes z (probability) components of points defining vulnerability " "curve/surface. 1 or 2 dimensional array.", ) - points_kind: Optional[str] = Field( + points_kind: str | None = Field( default=None, description="'deterministic', 'beta', 'trunc_gaussian' or 'cdf'. If omitted, 'deterministic'" " will be assumed.", ) - cap_of_points_x: Optional[float] = Field( + cap_of_points_x: float | None = Field( None, description="Cap of x (indicator/threshold).", ) - cap_of_points_y: Optional[float] = Field(None, description="Cap of y (impact).") - activation_of_points_x: Optional[float] = Field( + cap_of_points_y: float | None = Field(None, description="Cap of y (impact).") + activation_of_points_x: float | None = Field( None, description="Activation threshold of x (indicator/threshold).", ) - baseline_quantile_of_points_x: Optional[float] = Field( + baseline_quantile_of_points_x: float | None = Field( default=None, description="Baseline quantile of x (indicator/threshold) for vulnerability curve/surface." "If present, threshold values are specified relative to this quantile of the historical distribution." @@ -103,18 +97,18 @@ class VulnerabilityConfigItem(BaseModel): "then the threshold applied will be 10 °C above the 90% quantile, i.e. 43 °C.", ) - reference: Optional[str] = Field( + reference: str | None = Field( None, description="Source of the vulnerability config model", ) - license: Optional[str] = Field( + license: str | None = Field( None, description="License in compact format, e.g., 'CC-BY-4.0'", ) -def to_year_fraction_divisor(indicator_units: Optional[str]) -> float: +def to_year_fraction_divisor(indicator_units: str | None) -> float: if indicator_units is not None: if indicator_units == "days/year": return 365.0 @@ -145,26 +139,28 @@ def convert_curve(key: str, value: Any): return value -def convert_row(row: Dict[Any, Any]): +def convert_row(row: dict[Any, Any]): return {k: convert_curve(k, v) for k, v in row.items()} def config_items_to_df( - config_items: Sequence[Union[VulnerabilityConfigItem, DowntimeConfigItem]], + config_items: Sequence[VulnerabilityConfigItem | DowntimeConfigItem], ): return pd.DataFrame(vars(c) for c in config_items) def config_items_to_csv( - config_items: Sequence[Union[VulnerabilityConfigItem, DowntimeConfigItem]], + config_items: Sequence[VulnerabilityConfigItem | DowntimeConfigItem], path: str, ): df = config_items_to_df(config_items) for column in df.columns: df[column] = df[column].apply( - lambda x: "[" + ",".join([str(xx) for xx in x]) + "]" - if isinstance(x, (list, np.ndarray)) - else x + lambda x: ( + "[" + ",".join([str(xx) for xx in x]) + "]" + if isinstance(x, (list, np.ndarray)) + else x + ) ) df.to_csv(path, index=False) @@ -200,7 +196,7 @@ class ImpactCurveKey: """ def __init__(self, asset_identifier: str): - self.identifier: Dict[str, str] = ImpactCurveKey.to_dict(asset_identifier) + self.identifier: dict[str, str] = ImpactCurveKey.to_dict(asset_identifier) def __hash__(self): return hash(ImpactCurveKey.from_dict(self.identifier)) @@ -220,7 +216,7 @@ def __str__(self): return ImpactCurveKey.from_dict(self.identifier) @staticmethod - def to_dict(identifier: str) -> Dict[str, str]: + def to_dict(identifier: str) -> dict[str, str]: if not identifier: return defaultdict() return dict( @@ -232,13 +228,13 @@ def to_dict(identifier: str) -> Dict[str, str]: ) @staticmethod - def from_dict(identifier: Dict[str, str]) -> str: + def from_dict(identifier: dict[str, str]) -> str: return ",".join( ["=".join([key, str(value)]) for key, value in identifier.items()] ) @staticmethod - def get(asset: Asset, attributes: Set[str], keys: Optional[Iterable[Any]] = None): + def get(asset: Asset, attributes: set[str], keys: Iterable[Any] | None = None): """Return the matching key from keys using a subset of the asset attributes specified by attributes. @@ -285,10 +281,10 @@ class ImpactCurve: def __init__( self, curve_type: str, - indicator_units: Optional[str], - cap_of_points_x: Optional[float], - cap_of_points_y: Optional[float], - activation_of_points_x: Optional[float], + indicator_units: str | None, + cap_of_points_x: float | None, + cap_of_points_y: float | None, + activation_of_points_x: float | None, ): assert curve_type in ["indicator", "threshold"] self.curve_type = curve_type @@ -380,13 +376,13 @@ class PiecewiseLinearImpactCurve(ImpactCurve): def __init__( self, curve_type: str, - indicator_units: Optional[str], + indicator_units: str | None, points_x: np.ndarray, points_y: np.ndarray, - cap_of_points_x: Optional[float], - cap_of_points_y: Optional[float], - activation_of_points_x: Optional[float], - baseline_quantile_of_points_x: Optional[float] = None, + cap_of_points_x: float | None, + cap_of_points_y: float | None, + activation_of_points_x: float | None, + baseline_quantile_of_points_x: float | None = None, ): super().__init__( curve_type, @@ -413,7 +409,7 @@ def get_impact(self, points_x: np.ndarray) -> np.ndarray: class OccurrenceProbabilityImpactCurve: def __init__( self, - indicator_units: Optional[str], + indicator_units: str | None, points_x: np.ndarray, points_y: np.ndarray, ): diff --git a/src/physrisk/vulnerability_models/config_based_vuln_model_acute.py b/src/physrisk/vulnerability_models/config_based_vuln_model_acute.py index a1fd7d62..8f7245bf 100644 --- a/src/physrisk/vulnerability_models/config_based_vuln_model_acute.py +++ b/src/physrisk/vulnerability_models/config_based_vuln_model_acute.py @@ -1,13 +1,10 @@ -from enum import Enum import logging -from typing import ( - Sequence, - Tuple, - Union, -) +from collections.abc import Sequence +from enum import Enum import numpy as np from pint import UnitRegistry + from physrisk.kernel.assets import Asset, HasStandardOfProtection from physrisk.kernel.curve import ExceedanceCurve from physrisk.kernel.hazard_event_distrib import ( @@ -32,6 +29,7 @@ VulnerabilityDistrib, ) from physrisk.kernel.vulnerability_model import VulnerabilityModelAcuteBase +from physrisk.utils.units import needs_conversion from physrisk.vulnerability_models.config_based_impact_curves import ImpactCurve from physrisk.vulnerability_models.config_cdf_based_vuln_function import ( CDFBasedVulnerabilityFunction, @@ -39,7 +37,6 @@ from physrisk.vulnerability_models.impact_function_selector import ( ImpactFunctionSelector, ) -from physrisk.utils.units import needs_conversion ureg = UnitRegistry() logger = logging.getLogger(__name__) @@ -73,7 +70,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: future = HazardDataRequest( self.hazard_type, asset.longitude, @@ -124,7 +121,7 @@ def _sop_id_for_hazard(self, hazard_type: type): def get_distributions( self, asset: Asset, data_responses: Sequence[HazardDataResponse] - ) -> Tuple[VulnerabilityDistrib, HazardEventDistrib]: + ) -> tuple[VulnerabilityDistrib, HazardEventDistrib]: standard_of_protection = 0.0 if len(data_responses) == 3: (future, histo, sop) = data_responses diff --git a/src/physrisk/vulnerability_models/config_based_vuln_model_param.py b/src/physrisk/vulnerability_models/config_based_vuln_model_param.py index 3cdb0bd7..54f04d10 100644 --- a/src/physrisk/vulnerability_models/config_based_vuln_model_param.py +++ b/src/physrisk/vulnerability_models/config_based_vuln_model_param.py @@ -1,10 +1,10 @@ +from collections.abc import Sequence from typing import ( - Sequence, - Union, cast, ) import numpy as np + from physrisk.kernel.assets import Asset from physrisk.kernel.hazard_model import ( HazardDataRequest, @@ -50,7 +50,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: # find the curve to check if historical data is needed # note that the curve is looked up twice, once to identify requests # and again to apply (consider caching if necessary). diff --git a/src/physrisk/vulnerability_models/config_cdf_based_vuln_function.py b/src/physrisk/vulnerability_models/config_cdf_based_vuln_function.py index f909e53b..87245a1b 100644 --- a/src/physrisk/vulnerability_models/config_cdf_based_vuln_function.py +++ b/src/physrisk/vulnerability_models/config_cdf_based_vuln_function.py @@ -1,6 +1,7 @@ -from typing import Optional, Protocol -from numba import njit +from typing import Protocol + import numpy as np +from numba import njit from physrisk.kernel.vulnerability_model import checked_beta_distrib @@ -13,15 +14,15 @@ class CDFBasedVulnerabilityFunction(UncertainVulnerabilityFunction): """In general the vulnerability curve specifies for each hazard intensity level a cumulative probability density function. More precisely: - The hazard intensity values, :math:`x_i`, are given, :math:`i \in [1 \dots n]`. - .. math:: F_i(y) = \mathbb{P}(Y \leq y|x_i) + The hazard intensity values, :math:`x_i`, are given, :math:`i \\in [1 \\dots n]`. + .. math:: F_i(y) = \\mathbb{P}(Y \\leq y|x_i) - The CDF, :math:`F_i(y)`, is given for points :math:`y_j`, :math:`j \in [1 \dots m]`. + The CDF, :math:`F_i(y)`, is given for points :math:`y_j`, :math:`j \\in [1 \\dots m]`. .. math:: - F_{ij} = \mathbb{P}(Y \leq y_j|x_i) - x = [x_1, x_2, \dots, x_n ] - y = [y_1, y_2, \dots, y_m ] - z = [[F_{11}, F_{12}, \dots, F_{1m}], [F_{21}, F_{12}, \dots, F_{2m}], \dots, [F_{n1}, F_{n2}, \dots, F_{nm}]] + F_{ij} = \\mathbb{P}(Y \\leq y_j|x_i) + x = [x_1, x_2, \\dots, x_n ] + y = [y_1, y_2, \\dots, y_m ] + z = [[F_{11}, F_{12}, \\dots, F_{1m}], [F_{21}, F_{12}, \\dots, F_{2m}], \\dots, [F_{n1}, F_{n2}, \\dots, F_{nm}]] Two cases: @@ -38,16 +39,16 @@ class CDFBasedVulnerabilityFunction(UncertainVulnerabilityFunction): [0, 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] ) - __slots__ = "cdf", "impact", "haz_indicator", "kind" + __slots__ = "cdf", "haz_indicator", "impact", "kind" def __init__( self, - indicator_units: Optional[str], + indicator_units: str | None, x: np.ndarray, y: np.ndarray, - z: Optional[np.ndarray] = None, + z: np.ndarray | None = None, kind: str = "deterministic", - impact_grid: Optional[np.ndarray] = _default_impact_grid, + impact_grid: np.ndarray | None = _default_impact_grid, ): """Initialise a CDF diff --git a/src/physrisk/vulnerability_models/configuration/asset_factory.py b/src/physrisk/vulnerability_models/configuration/asset_factory.py index 4f93e79d..f9a5b944 100644 --- a/src/physrisk/vulnerability_models/configuration/asset_factory.py +++ b/src/physrisk/vulnerability_models/configuration/asset_factory.py @@ -1,7 +1,9 @@ import logging -from typing import Any, Optional, Protocol, Sequence +from collections.abc import Sequence +from typing import Any, Protocol import pandas as pd + from physrisk.api.v1.common import Asset as APIAsset from physrisk.kernel.assets import ( AgricultureAsset, @@ -29,7 +31,7 @@ def assets_and_financial_details( ) -> tuple[list[Asset], FinancialDataProvider]: ... -_oed_occ_codes_to_asset_types: dict[tuple[int, int], tuple[type, Optional[str]]] = { +_oed_occ_codes_to_asset_types: dict[tuple[int, int], tuple[type, str | None]] = { (1000, 1000): (Asset, None), (1050, 1099): (RealEstateAsset, "Buildings/Residential"), (1100, 1125): (RealEstateAsset, "Buildings/Commercial"), @@ -94,10 +96,8 @@ class DefaultAssetFactory(AssetFactory): def __init__( self, - occupancy_mapping: Optional[ - dict[tuple[int, int], tuple[str, Optional[str]]] - ] = None, - buffer_mapping: Optional[dict[tuple[int, int], float]] = None, + occupancy_mapping: dict[tuple[int, int], tuple[str, str | None]] | None = None, + buffer_mapping: dict[tuple[int, int], float] | None = None, use_default_buffer: bool = False, ): """Initialise the factory with occupancy-code-to-asset-type and optional buffer mappings. @@ -123,7 +123,7 @@ def interval_df(mapping: dict, col: str) -> pd.DataFrame: if buffer_mapping is not None else (_oed_occ_codes_to_default_buffer if use_default_buffer else {}) ) - self._buffer_df: Optional[pd.DataFrame] = ( + self._buffer_df: pd.DataFrame | None = ( interval_df(resolved_buffer_mapping, "buffer") if resolved_buffer_mapping else None @@ -145,7 +145,7 @@ def create_asset(self, api_asset: APIAsset) -> Asset: kwargs = {k: v for k, v in kwargs.items() if v is not None} # if occupancy code is provided, use this to determine asset class and type if api_asset.occupancy_code != 1000: - match: tuple[str, Optional[str]] = self.sectorial_df.loc[ + match: tuple[str, str | None] = self.sectorial_df.loc[ api_asset.occupancy_code ]["mapping"] asset_class, asset_type = match diff --git a/src/physrisk/vulnerability_models/configuration/build_all_config.py b/src/physrisk/vulnerability_models/configuration/build_all_config.py index 8c603195..575e9b50 100644 --- a/src/physrisk/vulnerability_models/configuration/build_all_config.py +++ b/src/physrisk/vulnerability_models/configuration/build_all_config.py @@ -1,29 +1,25 @@ from pathlib import Path -from typing import List, Optional -from physrisk.vulnerability_models.configuration.config_builders import ( - ConfigBuilderEberenzWind, - ConfigBuilderOffshoreWind, - ConfigBuilderSectoralModels, -) + from physrisk.vulnerability_models.config_based_impact_curves import ( VulnerabilityConfigItem, + config_items_to_csv, ) from physrisk.vulnerability_models.configuration.config_builders import ( ConfigBuilder, + ConfigBuilderEberenzWind, ConfigBuilderJRCFlood, + ConfigBuilderOffshoreWind, + ConfigBuilderSectoralModels, vulnerability_config_dir, vulnerability_onboarding_dir, ) -from physrisk.vulnerability_models.config_based_impact_curves import ( - config_items_to_csv, -) from physrisk.vulnerability_models.configuration.hazus_config_builders import ( ConfigBuilderHazusFlood, ConfigBuilderHazusWind, ) -def build_all(source_dir: Optional[Path] = None): +def build_all(source_dir: Path | None = None): """Build all of the configuration. ConfigBuilderHazusFlood and ConfigBuilderHazusWind will be slow to run first time as these require downloading and processing large Hazus data files. @@ -31,7 +27,7 @@ def build_all(source_dir: Optional[Path] = None): """ config_items: list[VulnerabilityConfigItem] = [] source_dir = vulnerability_onboarding_dir() if source_dir is None else source_dir - config_builders: List[ConfigBuilder] = [ + config_builders: list[ConfigBuilder] = [ ConfigBuilderJRCFlood(source_dir=source_dir), ConfigBuilderEberenzWind(source_dir=source_dir), ConfigBuilderSectoralModels(), diff --git a/src/physrisk/vulnerability_models/configuration/config_builders.py b/src/physrisk/vulnerability_models/configuration/config_builders.py index 3c08700a..2f182c11 100644 --- a/src/physrisk/vulnerability_models/configuration/config_builders.py +++ b/src/physrisk/vulnerability_models/configuration/config_builders.py @@ -1,9 +1,10 @@ from pathlib import Path -from typing import List, Optional, Protocol -from fsspec import AbstractFileSystem -from fsspec.implementations.local import LocalFileSystem +from typing import Protocol + import numpy as np import pandas as pd +from fsspec import AbstractFileSystem +from fsspec.implementations.local import LocalFileSystem from pint import UnitRegistry from physrisk.kernel.assets import Asset, PowerGeneratingAsset, RealEstateAsset @@ -40,14 +41,14 @@ def vulnerability_config_dir(): class ConfigBuilder(Protocol): - def build_config(self) -> List[VulnerabilityConfigItem]: ... + def build_config(self) -> list[VulnerabilityConfigItem]: ... def download_inputs(self) -> None: ... class ConfigBuilderBase: def __init__( - self, source_dir: Optional[Path] = None, fs: Optional[AbstractFileSystem] = None + self, source_dir: Path | None = None, fs: AbstractFileSystem | None = None ): """Instantiate base class for configuration builder. @@ -66,7 +67,7 @@ def __init__( class ConfigBuilderJRCFlood(ConfigBuilderBase, ConfigBuilder): - def build_config(self) -> List[VulnerabilityConfigItem]: + def build_config(self) -> list[VulnerabilityConfigItem]: """Create vulnerability config for flood based on the EU JRC global depth-damage functions. This replaces the notebook implementation and sets the pattern for code-based generation of the config. """ @@ -98,7 +99,7 @@ def build_config(self) -> List[VulnerabilityConfigItem]: "Industrial buildings": "Buildings/Industrial", } - config_items: List[VulnerabilityConfigItem] = [] + config_items: list[VulnerabilityConfigItem] = [] for mapping in type_mappings: type_df = df[df["Type"] == mapping] flood_depth = type_df["Flood depth [m]"].to_numpy() @@ -170,7 +171,7 @@ def download_inputs(self) -> None: class ConfigBuilderEberenzWind(ConfigBuilderBase, ConfigBuilder): - def build_config(self) -> List[VulnerabilityConfigItem]: + def build_config(self) -> list[VulnerabilityConfigItem]: """Create vulnerability config for generic wind assets based on Eberenz S, Lüthi S, Bresch DN. Regional tropical cyclone impact functions for globally consistent risk assessments. Natural Hazards and Earth System Sciences. @@ -210,7 +211,7 @@ def build_config(self) -> List[VulnerabilityConfigItem]: / 2, } x = np.concat((np.arange(20.0, 55.0, 5.0), np.arange(60.0, 120.0, 10.0))) - config_items: List[VulnerabilityConfigItem] = [] + config_items: list[VulnerabilityConfigItem] = [] for location, v_half in mapping.items(): config_items.append( VulnerabilityConfigItem( @@ -239,7 +240,7 @@ def download_inputs(self) -> None: class ConfigBuilderOffshoreWind(ConfigBuilderBase, ConfigBuilder): - def build_config(self) -> List[VulnerabilityConfigItem]: + def build_config(self) -> list[VulnerabilityConfigItem]: """Generate configuration items for offshore wind vulnerability models. Simulating impacts of extreme events on grids with high penetrations of wind power resources https://docs.nrel.gov/docs/fy22osti/80639.pdf @@ -255,7 +256,7 @@ def build_config(self) -> List[VulnerabilityConfigItem]: # turbine pointed perpendicular to the wind (no yawing) alpha, beta = 140, 18.6 - config_items: List[VulnerabilityConfigItem] = [] + config_items: list[VulnerabilityConfigItem] = [] x = np.concat((np.arange(20.0, 55.0, 5.0), np.arange(60.0, 120.0, 10.0))) # Placeholder example curve @@ -303,7 +304,7 @@ def download_inputs(self) -> None: class ConfigBuilderSectoralModels(ConfigBuilder): - def build_config(self) -> List[VulnerabilityConfigItem]: + def build_config(self) -> list[VulnerabilityConfigItem]: """Generate configuration items for sectorial vulnerability models. Returns: @@ -351,7 +352,7 @@ def build_config(self) -> List[VulnerabilityConfigItem]: cap_of_points_y=1.0, ) - config_items: List[VulnerabilityConfigItem] = [ + config_items: list[VulnerabilityConfigItem] = [ generic_drought_item, generic_productivity_loss_item, generic_fire_item, diff --git a/src/physrisk/vulnerability_models/configuration/hazus_config_builders.py b/src/physrisk/vulnerability_models/configuration/hazus_config_builders.py index 71b2f7f9..dfaf4fe2 100644 --- a/src/physrisk/vulnerability_models/configuration/hazus_config_builders.py +++ b/src/physrisk/vulnerability_models/configuration/hazus_config_builders.py @@ -1,10 +1,11 @@ +from collections.abc import Sequence from pathlib import Path, PurePosixPath -from typing import List, Optional, Sequence -from fsspec import AbstractFileSystem + import numpy as np import pandas as pd -from pint import UnitRegistry import s3fs +from fsspec import AbstractFileSystem +from pint import UnitRegistry from physrisk.kernel.curve import add_x_value_to_curve from physrisk.vulnerability_models.config_based_impact_curves import ( @@ -38,8 +39,8 @@ def _download_inputs(filename: str, source_dir: Path, check_exists: bool = True) class ConfigBuilderHazusWind(ConfigBuilderBase, ConfigBuilder): def __init__( self, - source_dir: Optional[Path] = None, - fs: Optional[AbstractFileSystem] = None, + source_dir: Path | None = None, + fs: AbstractFileSystem | None = None, attributes: Sequence[str] = ["hazus_wind_spec_build_type", "damage_type"], ): """Create config builder for extracting vulnerability config items from an extract of Hazus wind damage functions. @@ -87,7 +88,7 @@ def cached_dataframe(self): def _config_items( self, df: pd.DataFrame, - attributes: List[str] = ["hazus_wind_spec_build_type", "hazus_terrain_id"], + attributes: list[str] = ["hazus_wind_spec_build_type", "hazus_terrain_id"], ): units = "mph" df["points_y"] = df.apply( @@ -158,7 +159,7 @@ def asset_identifier(row): def _grouped_data_frame( self, df: pd.DataFrame, - groupby: List[str] = [ + groupby: list[str] = [ "hazus_wind_spec_build_type", "damage_type", "hazus_terrain_id", @@ -251,8 +252,8 @@ def _load_data_frame(self): class ConfigBuilderHazusFlood(ConfigBuilderBase, ConfigBuilder): def __init__( self, - source_dir: Optional[Path] = None, - fs: Optional[AbstractFileSystem] = None, + source_dir: Path | None = None, + fs: AbstractFileSystem | None = None, adjust_to_absolute_depth: bool = False, flood_depth_threshold: float = 0.05, first_floor_height: float = 0.3, @@ -443,10 +444,8 @@ def _load_data_frame(self): key = "inventory" assert key in ["structure", "contents", "inventory"] df["hazus_fl_dmg_id"] = df[column].apply( - lambda id, key=key: key - + "_" - + str( - id + lambda id, key=key: ( + key + "_" + str(id) ) # note that the key prefix is to emphasize that the hazus_fl_dmg_id # is different for the different damage types. ) @@ -474,13 +473,15 @@ def _load_data_frame_util(self): df = pd.read_csv(hazus_path) df = df.assign( - Occupancy_short=lambda d: d["Occupancy"] - .str[:3] - .replace({"PST": "PSTGC", "PDF": "PDFLT"}), - DmgFnID=lambda d: "util_" - + d.groupby("Occupancy_short")["UtilFltyDmgFnID"] - .transform("min") - .astype(str), + Occupancy_short=lambda d: ( + d["Occupancy"].str[:3].replace({"PST": "PSTGC", "PDF": "PDFLT"}) + ), + DmgFnID=lambda d: ( + "util_" + + d.groupby("Occupancy_short")["UtilFltyDmgFnID"] + .transform("min") + .astype(str) + ), ) # Average ft-columns ft_cols = df.filter(like="ft").columns diff --git a/src/physrisk/vulnerability_models/configuration/oed_attribute_matcher.py b/src/physrisk/vulnerability_models/configuration/oed_attribute_matcher.py index 7c91379c..0e588205 100644 --- a/src/physrisk/vulnerability_models/configuration/oed_attribute_matcher.py +++ b/src/physrisk/vulnerability_models/configuration/oed_attribute_matcher.py @@ -1,12 +1,11 @@ import re +from collections.abc import Sequence from dataclasses import dataclass from functools import lru_cache -from typing import Sequence, Union import numpy as np import pandas as pd - _INTERVAL_RE = re.compile(r"^\[\s*([^,]+?)\s*,\s*([^,\]]+?)\s*\]$") _PAIR_RE = re.compile(r"(\w+)=((?:\[[^\]]+\])+|[^\[,]+)") _BRACKET_RE = re.compile(r"\[[^\]]+\]") @@ -22,14 +21,14 @@ def parse_interval(s: str) -> pd.Interval: raise ValueError(f"Expected '[a, b]' format, got: {s!r}") left_str, right_str = m.groups() - def _coerce(v: str) -> Union[int, float]: + def _coerce(v: str) -> int | float: f = float(v) return int(f) if f == int(f) else f return pd.Interval(_coerce(left_str), _coerce(right_str), closed="both") -def parse_asset_identifier(s: str) -> dict[str, Union[str, list[str]]]: +def parse_asset_identifier(s: str) -> dict[str, str | list[str]]: """Parse an asset_identifier string into a dict of attribute → value(s). Values are either a plain scalar string, a single interval string, or a list of @@ -46,7 +45,7 @@ def parse_asset_identifier(s: str) -> dict[str, Union[str, list[str]]]: parse_asset_identifier("occupancy_code=[1050,1099][2000,2352]") # -> {"occupancy_code": ["[1050,1099]", "[2000,2352]"]} """ - result: dict[str, Union[str, list[str]]] = {} + result: dict[str, str | list[str]] = {} for key, raw in _PAIR_RE.findall(s): intervals = _BRACKET_RE.findall(raw) if not intervals: @@ -79,7 +78,7 @@ def _parse_numeric_value(v: str) -> pd.Interval: if v.startswith("["): return parse_interval(v) f = float(v) - pv: Union[int, float] = int(f) if f == int(f) else f + pv: int | float = int(f) if f == int(f) else f return pd.Interval(pv, pv, closed="both") @@ -127,7 +126,7 @@ class MultiIntervalMatcher: def __init__( self, - keys: Sequence[dict[str, Union[str, list[str]]]], + keys: Sequence[dict[str, str | list[str]]], attribute_weights: dict[str, float] | None = None, ): self._n = len(keys) @@ -198,7 +197,7 @@ def match( self, required: frozenset[str] = frozenset(), /, - **values: Union[int, float, str, None], + **values: float | str | None, ) -> list[int]: """Return indices of matching keys ordered by specificity (most specific first). @@ -264,7 +263,7 @@ def _match_impl( ) def _match_attr( - self, attr: str, value: Union[int, float, str, None] + self, attr: str, value: float | str | None ) -> tuple[set[int], set[int]]: """Return (direct_matches, wildcards) for this attribute and value. diff --git a/src/physrisk/vulnerability_models/configuration/oed_hazus_mapping.py b/src/physrisk/vulnerability_models/configuration/oed_hazus_mapping.py index 845cf5e6..1bafccce 100644 --- a/src/physrisk/vulnerability_models/configuration/oed_hazus_mapping.py +++ b/src/physrisk/vulnerability_models/configuration/oed_hazus_mapping.py @@ -1,13 +1,10 @@ +import sys from collections import defaultdict +from collections.abc import Sequence from dataclasses import dataclass from enum import Enum from functools import lru_cache -import sys - -from physrisk.vulnerability_models.configuration.oed_attribute_matcher import ( - parse_asset_identifier, -) -from typing import Any, NamedTuple, Optional, Protocol, Sequence +from typing import Any, NamedTuple, Protocol import numpy as np import pandas as pd @@ -25,6 +22,9 @@ ImpactCurve, VulnerabilityConfigItem, ) +from physrisk.vulnerability_models.configuration.oed_attribute_matcher import ( + parse_asset_identifier, +) class VulnerabilityMapper(Protocol): @@ -118,7 +118,7 @@ class HazusOccToDamageFn(BaseModel): number_of_storeys_max: int basement: int damage_type: list[DamageType] # structure, contents or inventory - damage_fn_id: list[Optional[str]] + damage_fn_id: list[str | None] @field_validator("damage_type", mode="before") @classmethod diff --git a/src/physrisk/vulnerability_models/downtime.py b/src/physrisk/vulnerability_models/downtime.py index 837b248a..778f07a3 100644 --- a/src/physrisk/vulnerability_models/downtime.py +++ b/src/physrisk/vulnerability_models/downtime.py @@ -1,11 +1,11 @@ from abc import abstractmethod from collections import defaultdict +from collections.abc import Sequence from operator import add -from typing import Dict, List, Optional, Sequence, Type import numpy as np -from physrisk.kernel.assets import Asset, asset_class +from physrisk.kernel.assets import Asset, asset_class from physrisk.vulnerability_models.config_based_impact_curves import ( DowntimeConfigItem, ImpactCurveKey, @@ -42,7 +42,7 @@ def get_impact(self, asset: Asset, frac_damage: np.ndarray) -> np.ndarray: class DowntimeModels: def __init__(self, config: Sequence[DowntimeConfigItem] = []): - self.models: Dict[Type[Asset], Sequence[DowntimeModelBase]] = ( + self.models: dict[type[Asset], Sequence[DowntimeModelBase]] = ( self.programmatic_models() ) if any(config): @@ -52,14 +52,14 @@ def __init__(self, config: Sequence[DowntimeConfigItem] = []): def downtime_model_for_asset_of_type( self, type: type - ) -> Optional[Sequence[DowntimeModelBase]]: + ) -> Sequence[DowntimeModelBase] | None: for asset_class_type in type.mro(): if asset_class_type is not object and asset_class_type in self.models: return self.models[asset_class_type] return self.models.get(Asset) def config_based_models(self, config: Sequence[DowntimeConfigItem]): - items_by_asset_class: Dict[Type[Asset], List[DowntimeConfigItem]] = defaultdict( + items_by_asset_class: dict[type[Asset], list[DowntimeConfigItem]] = defaultdict( list ) for item in config: @@ -85,7 +85,7 @@ def config_based_models(self, config: Sequence[DowntimeConfigItem]): ) keys = [item.asset_identifier for item in items_by_asset_class[k]] - models: Dict[Type[Asset], List[DowntimeModelBase]] = defaultdict(list) + models: dict[type[Asset], list[DowntimeModelBase]] = defaultdict(list) for asset_type, items in items_by_asset_class.items(): try: models[asset_type].append( @@ -100,5 +100,5 @@ def config_based_models(self, config: Sequence[DowntimeConfigItem]): ) return models - def programmatic_models(self) -> Dict[Type[Asset], Sequence[DowntimeModelBase]]: + def programmatic_models(self) -> dict[type[Asset], Sequence[DowntimeModelBase]]: return {} diff --git a/src/physrisk/vulnerability_models/example_models.py b/src/physrisk/vulnerability_models/example_models.py index fe82f296..4acd6c3f 100644 --- a/src/physrisk/vulnerability_models/example_models.py +++ b/src/physrisk/vulnerability_models/example_models.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence import numpy as np @@ -90,7 +90,6 @@ def __init__(self): the wildfire propagates into the pixel containing the asset. For large pixel sizes, clearly this is not the same as the probability that the asset is within the wildfire zone. """ - ... super().__init__( indicator_id="fire_probability", diff --git a/src/physrisk/vulnerability_models/impact_function_selector.py b/src/physrisk/vulnerability_models/impact_function_selector.py index 737ef3e7..e177425a 100644 --- a/src/physrisk/vulnerability_models/impact_function_selector.py +++ b/src/physrisk/vulnerability_models/impact_function_selector.py @@ -1,13 +1,14 @@ # selects impact curve based on asset properties import collections -from dataclasses import dataclass import copy - +from collections.abc import Sequence +from dataclasses import dataclass from importlib.resources import files -from typing import NamedTuple, Protocol, Sequence +from typing import NamedTuple, Protocol import pandas as pd + import physrisk.data.static.vulnerability.oed_hazus from physrisk.kernel.assets import Asset, OEDAsset, all_asset_types, asset_class from physrisk.kernel.hazards import ( diff --git a/src/physrisk/vulnerability_models/power_generating_asset_models.py b/src/physrisk/vulnerability_models/power_generating_asset_models.py index 6130b0ee..1ccb88b2 100644 --- a/src/physrisk/vulnerability_models/power_generating_asset_models.py +++ b/src/physrisk/vulnerability_models/power_generating_asset_models.py @@ -1,4 +1,4 @@ -from typing import Sequence, Union +from collections.abc import Sequence import numpy as np @@ -32,11 +32,10 @@ def __init__(self, indicator_id="flood_depth"): hazard_type=RiverineInundation, impact_type=ImpactType.disruption, ) - pass def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" diff --git a/src/physrisk/vulnerability_models/real_estate_models.py b/src/physrisk/vulnerability_models/real_estate_models.py index 3660845e..a5948839 100644 --- a/src/physrisk/vulnerability_models/real_estate_models.py +++ b/src/physrisk/vulnerability_models/real_estate_models.py @@ -1,7 +1,7 @@ """Define vulnerability models for assessing the impact of various hazards on real estate assets.""" from collections import defaultdict -from typing import Dict, Sequence, Tuple +from collections.abc import Sequence import numpy as np @@ -68,7 +68,7 @@ def __init__( ((c.location, c.asset_type), c) for c in curve_set.items ) self.vuln_curves_by_type = defaultdict(list) - self.proxy_curves: Dict[Tuple[str, str], VulnerabilityCurve] = {} + self.proxy_curves: dict[tuple[str, str], VulnerabilityCurve] = {} for item in curve_set.items: self.vuln_curves_by_type[item.asset_type].append(item) diff --git a/src/physrisk/vulnerability_models/thermal_power_generation_models.py b/src/physrisk/vulnerability_models/thermal_power_generation_models.py index aec75ca4..67755296 100644 --- a/src/physrisk/vulnerability_models/thermal_power_generation_models.py +++ b/src/physrisk/vulnerability_models/thermal_power_generation_models.py @@ -1,7 +1,8 @@ """Define vulnerability models for assessing the impact of various hazards on thermal power generation assets.""" from collections import defaultdict -from typing import List, Sequence, Tuple, Union, cast +from collections.abc import Sequence +from typing import cast import numpy as np from scipy.stats import multivariate_normal, norm @@ -109,7 +110,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Provide the hazard event data requests. It will return one or more HazardDataRequest as required to calculate the @@ -139,7 +140,7 @@ def get_data_requests( def get_distributions( self, asset: Asset, event_data_responses: Sequence[HazardDataResponse] - ) -> Tuple[VulnerabilityDistrib, HazardEventDistrib]: + ) -> tuple[VulnerabilityDistrib, HazardEventDistrib]: """Calculate the vulnerability and hazard event distributions for the asset.""" assert isinstance(asset, ThermalPowerGeneratingAsset) @@ -170,7 +171,7 @@ def get_distributions( include_last=True ) - curves: List[VulnerabilityCurve] = [] + curves: list[VulnerabilityCurve] = [] if asset.turbine is None: curves = [ self.vulnerability_curves[key] for key in self.vulnerability_curves @@ -332,7 +333,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" return HazardDataRequest( self.hazard_type, @@ -368,7 +369,7 @@ def get_impact( probability_bins = intensities[:-1] - intensities[1:] probability_bins = np.append(probability_bins, intensities[-1]) - curves: List[VulnerabilityCurve] = [] + curves: list[VulnerabilityCurve] = [] if asset.turbine is None: curves = [ self.vulnerability_curves[key] for key in self.vulnerability_curves @@ -449,7 +450,7 @@ def __init__( self, *, resource: str = _default_resource, - temperatures: List[float] = _default_temperatures, + temperatures: list[float] = _default_temperatures, ): """Initialize the air temperature vulnerability model for thermal power generation. @@ -495,7 +496,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" data_request = [] for temperature in self.temperatures: @@ -557,7 +558,7 @@ def get_impact( probability_bins = intensities[:-1] - intensities[1:] probability_bins = np.append(probability_bins, intensities[-1]) - curves: List[VulnerabilityCurve] = [] + curves: list[VulnerabilityCurve] = [] if asset.turbine is None: curves = [ self.vulnerability_curves[key] for key in self.vulnerability_curves @@ -677,7 +678,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" data_request = [] data_request.append( @@ -838,7 +839,7 @@ def get_impact( ) ) - curves: List[VulnerabilityCurve] = [] + curves: list[VulnerabilityCurve] = [] if asset.turbine is None: curves = [ self.vulnerability_curves[key] for key in self.vulnerability_curves @@ -850,7 +851,7 @@ def get_impact( elif asset.turbine in self.vuln_curves_by_type: curves = self.vuln_curves_by_type[asset.turbine] - impact_distrib_by_curve: List[ImpactDistrib] = [] + impact_distrib_by_curve: list[ImpactDistrib] = [] for curve in curves: scale = 1.0 threshold = design_intake_water_temperature @@ -922,9 +923,7 @@ def get_impact( self.hazard_type, [ 0.0 - for _ in range( - 0, len(intake_water_temperature_probability_bins) + 1 - ) + for _ in range(len(intake_water_temperature_probability_bins) + 1) ], intake_water_temperature_probability_bins, hazard_indicator_id=self.indicator_id, @@ -981,7 +980,7 @@ def __init__(self, *, resource: str = _default_resource): def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" data_request = [] data_request.append( @@ -1059,7 +1058,7 @@ def get_impact( ) ) - curves: List[VulnerabilityCurve] = [] + curves: list[VulnerabilityCurve] = [] if asset.turbine is None: curves = [ self.vulnerability_curves[key] for key in self.vulnerability_curves diff --git a/src/physrisk/vulnerability_models/vulnerability.py b/src/physrisk/vulnerability_models/vulnerability.py index b7159f50..e46b2787 100644 --- a/src/physrisk/vulnerability_models/vulnerability.py +++ b/src/physrisk/vulnerability_models/vulnerability.py @@ -1,6 +1,7 @@ import importlib.resources -from typing import Dict, Sequence +from collections.abc import Sequence +import physrisk.data.static.vulnerability from physrisk.kernel.assets import Asset from physrisk.kernel.hazards import ( CoastalInundation, @@ -19,18 +20,17 @@ from physrisk.kernel.vulnerability_model import ( VulnerabilityModelsFactory as PVulnerabilityModelsFactory, ) -import physrisk.data.static.vulnerability from physrisk.vulnerability_models.config_based_impact_curves import ( VulnerabilityConfigItem, config_items_from_csv, ) -from physrisk.vulnerability_models.config_based_vuln_model_param import ( - ConfigBasedVulnerabilityModelParam, -) from physrisk.vulnerability_models.config_based_vuln_model_acute import ( ConfigBasedVulnerabilityModelAcute, StandardOfProtection, ) +from physrisk.vulnerability_models.config_based_vuln_model_param import ( + ConfigBasedVulnerabilityModelParam, +) from physrisk.vulnerability_models.impact_function_selector import ( CombinedImpactFunctionSelector, ConfigBasedImpactFunctionSelector, @@ -97,7 +97,7 @@ def __init__( self, config_based_selector: ConfigBasedImpactFunctionSelector, combined_selector: CombinedImpactFunctionSelector, - programmatic_models: Dict[type[Asset], list[VulnerabilityModelBase]] = {}, + programmatic_models: dict[type[Asset], list[VulnerabilityModelBase]] = {}, hazard_scope: dict[type[Hazard], set[str] | None] | None = None, disable_api_calls: bool = False, use_oed_hazus_curves=True, @@ -141,7 +141,7 @@ def __init__( if k not in all_models: all_models[k] = v - all_asset_types = set(k.asset_type for k in all_models.keys()) + all_asset_types = set(k.asset_type for k in all_models) # add any missing models from ancestors for asset_type in list(all_asset_types): for ancestor_type in asset_type.mro(): @@ -150,9 +150,7 @@ def __init__( and ancestor_type in all_asset_types ): ancestor_keys_to_add = [ - key - for key in all_models.keys() - if key.asset_type == ancestor_type + key for key in all_models if key.asset_type == ancestor_type ] for ancestor_key in ancestor_keys_to_add: new_key = VulnModelKey( diff --git a/tests/api/test_container.py b/tests/api/test_container.py index 34bb3b06..6ddb3ca0 100644 --- a/tests/api/test_container.py +++ b/tests/api/test_container.py @@ -1,5 +1,5 @@ -import fsspec.implementations.local as local from dependency_injector import containers, providers +from fsspec.implementations import local from physrisk.data.inventory_reader import InventoryReader diff --git a/tests/api/test_data_requests.py b/tests/api/test_data_requests.py index 6e7988a5..b25a4339 100644 --- a/tests/api/test_data_requests.py +++ b/tests/api/test_data_requests.py @@ -8,9 +8,9 @@ from physrisk.api.v1.hazard_data import HazardDataRequestItem from physrisk.container import Container from physrisk.data.hazard_data_provider import HazardDataHint -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.inventory import EmbeddedInventory from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.zarr_reader import ZarrReader from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.hazards import ChronicHeat, RiverineInundation @@ -145,14 +145,14 @@ def test_zarr_reading(): ) np.testing.assert_array_almost_equal_nulp( - result.items[0].intensity_curve_set[0].intensities, np.zeros((9)) + result.items[0].intensity_curve_set[0].intensities, np.zeros(9) ) np.testing.assert_array_almost_equal_nulp( result.items[0].intensity_curve_set[1].intensities, np.linspace(0.1, 1.0, 9, dtype="f4"), ) np.testing.assert_array_almost_equal_nulp( - result.items[0].intensity_curve_set[2].intensities, np.zeros((9)) + result.items[0].intensity_curve_set[2].intensities, np.zeros(9) ) @@ -199,9 +199,11 @@ def test_zarr_reading_live(clear_credentials): import json from zipfile import ZipFile - with ZipFile("./tests/api/test_lat_lons.json.zip") as z: - with z.open("test_lat_lons.json") as f: - data = json.loads(f.read()) + with ( + ZipFile("./tests/api/test_lat_lons.json.zip") as z, + z.open("test_lat_lons.json") as f, + ): + data = json.loads(f.read()) request1 = { "items": [ diff --git a/tests/api/test_impact_requests.py b/tests/api/test_impact_requests.py index dfe40127..ae312ec3 100644 --- a/tests/api/test_impact_requests.py +++ b/tests/api/test_impact_requests.py @@ -14,9 +14,9 @@ from physrisk.api.v1.common import Asset, Assets from physrisk.api.v1.impact_req_resp import RiskMeasures, RiskMeasuresHelper from physrisk.container import Container -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.inventory import EmbeddedInventory from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.zarr_reader import ZarrReader from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.hazard_models.hazard_cache import GeometryH3BasedCache, LMDBStore diff --git a/tests/conftest.py b/tests/conftest.py index 036bfa75..6299c56e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import pytest import json import logging import os @@ -9,6 +8,7 @@ from pathlib import Path import numpy as np +import pytest from physrisk.hazard_models.hazard_cache import ( H3BasedCache, diff --git a/tests/data/test_data_retrieval.py b/tests/data/test_data_retrieval.py index 858e4759..01e10519 100644 --- a/tests/data/test_data_retrieval.py +++ b/tests/data/test_data_retrieval.py @@ -3,7 +3,7 @@ import os import time from collections import defaultdict -from typing import Sequence +from collections.abc import Sequence # import fsspec.implementations.local as local # type: ignore import numpy as np @@ -22,18 +22,18 @@ from physrisk.data.hazard_data_provider import ( HazardResourceSelector, ) -from physrisk.data.scenario_year_resolution import ( - ScenarioYear, - InterpolatedYearResolver, - interpolate_year, - resolve_exact_year, -) from physrisk.data.inventory import EmbeddedInventory, Inventory from physrisk.data.inventory_reader import InventoryReader from physrisk.data.pregenerated_hazard_model import ( PregeneratedHazardModel, ZarrHazardModel, ) +from physrisk.data.scenario_year_resolution import ( + InterpolatedYearResolver, + ScenarioYear, + interpolate_year, + resolve_exact_year, +) from physrisk.data.zarr_reader import ZarrReader from physrisk.kernel.hazard_model import HazardDataFailedResponse, HazardDataRequest from physrisk.kernel.hazards import RiverineInundation, Wind @@ -357,10 +357,8 @@ def test_years_interpolation(): assert weights[ScenarioYear("ssp585", 2040)].weights[0][1] == (2050.0 - 2040.0) / ( 2050.0 - 2025.0 ) - # assert weights[ScenarioYear("ssp585", 2050)].weights[0][0].year == 2050 assert weights[ScenarioYear("ssp585", 2050)].weights[0][1] == 1.0 - # assert weights[ScenarioYear("ssp585", 2090)].weights[0][0].year == 2060 assert weights[ScenarioYear("ssp585", 2090)].weights[0][1] == -(2090.0 - 2080.0) / ( 2080.0 - 2060.0 @@ -544,7 +542,7 @@ def test_cascade(): ) np.testing.assert_almost_equal( response[requests[9]].intensities, - [1.0, 2.0, 3.0], # + [1.0, 2.0, 3.0], ) assert isinstance(response[requests[10]], HazardDataFailedResponse) np.testing.assert_almost_equal(response[requests[15]].intensities, [1.0, 2.0, 3.0]) diff --git a/tests/data/test_geocode_google.py b/tests/data/test_geocode_google.py index 312cac7b..acffa1ce 100644 --- a/tests/data/test_geocode_google.py +++ b/tests/data/test_geocode_google.py @@ -8,18 +8,18 @@ from shapely.geometry import MultiPolygon, Polygon from physrisk.data.geocode_google import ( - GeocodeResult, - GoogleGeocoder, - Granularity, - StructureType, _ADDRESS_FIELD_MASK, _DESTINATIONS_FIELD_MASK, _DESTINATIONS_URL, _GEOCODE_ADDRESS_URL, - _RateLimiter, + GeocodeResult, + GoogleGeocoder, + Granularity, + StructureType, _parse_geojson, _parse_granularity, _parse_structure_type, + _RateLimiter, ) # --------------------------------------------------------------------------- diff --git a/tests/data/test_hazard_model_store.py b/tests/data/test_hazard_model_store.py index 9791e1cb..0164caf8 100644 --- a/tests/data/test_hazard_model_store.py +++ b/tests/data/test_hazard_model_store.py @@ -1,5 +1,5 @@ import os -from typing import Dict, List, Sequence, Tuple, Union +from collections.abc import Sequence import numpy as np import numpy.typing as npt @@ -64,8 +64,8 @@ def add_curves_global( array_path: str, longitudes: Sequence[float], latitudes: Sequence[float], - index_values: Union[List[float], npt.NDArray, List[str]], - intensities: Union[List[float], npt.NDArray], + index_values: list[float] | npt.NDArray | list[str], + intensities: list[float] | npt.NDArray, width: int = 43200, height: int = 21600, units: str = "default", @@ -90,7 +90,7 @@ def _crs_shape_transform_global( self, width: int = 43200, height: int = 21600, - index_values: Union[List[float], npt.NDArray, List[str]] = [0.0], + index_values: list[float] | npt.NDArray | list[str] = [0.0], ): return self._crs_shape_transform(width, height, index_values) @@ -100,10 +100,10 @@ def _add_curves( longitudes: Sequence[float], latitudes: Sequence[float], crs: str, - shape: Tuple[int, int, int], - trans: List[float], - index_values: Union[Sequence[float], npt.NDArray, List[str]], - intensities: Union[Sequence[float], npt.NDArray], + shape: tuple[int, int, int], + trans: list[float], + index_values: Sequence[float] | npt.NDArray | list[str], + intensities: Sequence[float] | npt.NDArray, units: str = "default", ): z = self._root.create_dataset( # type: ignore @@ -144,7 +144,7 @@ def _crs_shape_transform( self, width: int, height: int, - index_values: Union[List[float], npt.NDArray, List[str]] = [0.0], + index_values: list[float] | npt.NDArray | list[str] = [0.0], ): t = [360.0 / width, 0.0, -180.0, 0.0, -180.0 / height, 90.0, 0.0, 0.0, 1.0] return "epsg:4326", (len(index_values), height, width), t @@ -153,7 +153,7 @@ def _crs_shape_transform( def shape_transform_21600_43200( width: int = 43200, height: int = 21600, - return_periods: Union[List[float], npt.NDArray] = [0.0], + return_periods: list[float] | npt.NDArray = [0.0], ): t = [360.0 / width, 0.0, -180.0, 0.0, -180.0 / height, 90.0, 0.0, 0.0, 1.0] return (len(return_periods), height, width), t @@ -169,10 +169,10 @@ def add_curves( longitudes, latitudes, array_path: str, - shape: Tuple[int, int, int], + shape: tuple[int, int, int], curve: np.ndarray, - return_periods: List[float], - trans: List[float], + return_periods: list[float], + trans: list[float], ): z = root.create_dataset( # type: ignore array_path, @@ -316,7 +316,7 @@ def inundation_return_periods(): def mock_hazard_model_store_path_curves( - longitudes, latitudes, path_curves: Dict[str, np.ndarray] + longitudes, latitudes, path_curves: dict[str, np.ndarray] ): """Create a MemoryStore for creation of Zarr hazard model to be used with unit tests, with the specified longitudes and latitudes set to the curve supplied.""" diff --git a/tests/data/test_scenario_year_resolution.py b/tests/data/test_scenario_year_resolution.py index 9cba3c7f..08fa645d 100644 --- a/tests/data/test_scenario_year_resolution.py +++ b/tests/data/test_scenario_year_resolution.py @@ -9,12 +9,12 @@ from physrisk.data.hazard_data_provider import ( CascadingHazardDataProvider, ) +from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.data.scenario_year_resolution import ( - ScenarioYear, InterpolatedYearResolver, + ScenarioYear, resolve_exact_year, ) -from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.kernel.hazard_model import HazardDataRequest, HazardEventDataResponse from physrisk.kernel.hazards import RiverineInundation diff --git a/tests/hazard_models/test_api_hazard_model.py b/tests/hazard_models/test_api_hazard_model.py index 85a702a2..f8542122 100644 --- a/tests/hazard_models/test_api_hazard_model.py +++ b/tests/hazard_models/test_api_hazard_model.py @@ -1,12 +1,11 @@ import shapely.wkt -from physrisk.kernel.hazard_model import HazardDataRequest -from physrisk.kernel.hazards import PluvialInundation, RiverineInundation from physrisk.data.geocode import Geocoder from physrisk.hazard_models.credentials_provider import EnvCredentialsProvider from physrisk.hazard_models.hazard_cache import GeometryH3BasedCache, MemoryStore from physrisk.hazard_models.jba_hazard_model import JBAHazardModel - +from physrisk.kernel.hazard_model import HazardDataRequest +from physrisk.kernel.hazards import PluvialInundation, RiverineInundation from tests.conftest import cache_store_tests diff --git a/tests/hazard_models/test_visualisation.py b/tests/hazard_models/test_visualisation.py index 390604cb..91f50545 100644 --- a/tests/hazard_models/test_visualisation.py +++ b/tests/hazard_models/test_visualisation.py @@ -1,16 +1,14 @@ import io from collections import Counter -from typing import Tuple import numpy as np import pytest -import PIL.Image as Image +from PIL import Image from physrisk.api.v1.hazard_image import HazardImageRequest from physrisk.container import Container -from physrisk.kernel.hazard_model import Tile from physrisk.hazard_models.jba_image_creator import JBAImageCreator - +from physrisk.kernel.hazard_model import Tile # https://jbavision.jbarisk.com/cog/tiles/9/265/176.png?LAYERS=853_WR30_202512_30m_4326:WR30_202512_FLRF_U_RP1500_RE_30m_4326 @@ -64,7 +62,7 @@ def test_get_jba_legend_png(load_credentials): assert image.width > 0 and image.height > 0 -def _locate_colorbar(image: Image.Image) -> Tuple[int, int, int, int]: +def _locate_colorbar(image: Image.Image) -> tuple[int, int, int, int]: """Return (x_min, y_min, x_max, y_max) bounding the colour swatches in a legend. Works for discrete stepped legends (e.g. JBA flood depth) where each swatch diff --git a/tests/kernel/test_asset_impact.py b/tests/kernel/test_asset_impact.py index 5b37ebb7..c8afe893 100644 --- a/tests/kernel/test_asset_impact.py +++ b/tests/kernel/test_asset_impact.py @@ -1,8 +1,7 @@ """Test asset impact calculations.""" -import pytest - import numpy as np +import pytest from physrisk.kernel.assets import RealEstateAsset from physrisk.kernel.curve import ExceedanceCurve diff --git a/tests/kernel/test_chronic_asset_impact.py b/tests/kernel/test_chronic_asset_impact.py index ee9d84f6..8d7c3802 100644 --- a/tests/kernel/test_chronic_asset_impact.py +++ b/tests/kernel/test_chronic_asset_impact.py @@ -1,10 +1,10 @@ -from typing import List, Sequence, Union +from collections.abc import Sequence import numpy as np from scipy.stats import norm -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.assets import Asset, IndustrialActivity from physrisk.kernel.hazard_model import ( @@ -53,7 +53,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Request the hazard data needed by the vulnerability model for a specific asset (this is a Google-style doc string) @@ -139,7 +139,7 @@ def get_impact_distrib( fraction_loss_mean: float, fraction_loss_std: float, hazard_type: type, - hazard_paths: List[str], + hazard_paths: list[str], hazard_indicator_id: str, impact_type: ImpactType, ) -> ImpactDistrib: diff --git a/tests/kernel/test_exposure.py b/tests/kernel/test_exposure.py index 1c101616..5b5d5d81 100644 --- a/tests/kernel/test_exposure.py +++ b/tests/kernel/test_exposure.py @@ -1,15 +1,18 @@ import json -import fsspec.implementations.local as local import numpy as np import pytest +from fsspec.implementations import local import physrisk.api.v1.common from physrisk.api.v1.exposure_req_resp import ( AssetExposureRequest, AssetExposureResponse, ) -from physrisk.container import DefaultHazardModelFactory +from physrisk.container import ( + DefaultHazardModelFactory, + DictBasedVulnerabilityModelsFactory, +) from physrisk.data.inventory import EmbeddedInventory from physrisk.data.inventory_reader import InventoryReader from physrisk.data.zarr_reader import ZarrReader @@ -31,7 +34,6 @@ Hail, Wind, ) -from physrisk.container import DictBasedVulnerabilityModelsFactory from physrisk.requests import Requester from physrisk.vulnerability_models.configuration.asset_factory import ( DefaultAssetFactory, diff --git a/tests/kernel/test_financial_model.py b/tests/kernel/test_financial_model.py index efbd861e..d7a26e61 100644 --- a/tests/kernel/test_financial_model.py +++ b/tests/kernel/test_financial_model.py @@ -1,7 +1,7 @@ import numpy as np -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.assets import Asset, PowerGeneratingAsset from physrisk.kernel.financial_model import ( diff --git a/tests/kernel/test_hazard_models.py b/tests/kernel/test_hazard_models.py index 224b263a..a8019a54 100644 --- a/tests/kernel/test_hazard_models.py +++ b/tests/kernel/test_hazard_models.py @@ -1,9 +1,9 @@ +from collections.abc import Mapping, Sequence from dataclasses import dataclass -from typing import Dict, Mapping, NamedTuple, Sequence, Tuple +from typing import NamedTuple import numpy as np -from tests.data.test_hazard_model_store import TestData from physrisk.kernel.assets import RealEstateAsset from physrisk.kernel.hazard_model import ( HazardDataRequest, @@ -16,6 +16,7 @@ from physrisk.kernel.impact import ImpactKey, calculate_impacts from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels from physrisk.vulnerability_models.real_estate_models import GenericTropicalCycloneModel +from tests.data.test_hazard_model_store import TestData @dataclass @@ -45,7 +46,7 @@ def __init__(self, points: Sequence[SinglePointData]): Args: points (Sequence[SinglePointData]): List of points. """ - self.points: Dict[Tuple[PointsKey, float, float], SinglePointData] = { + self.points: dict[tuple[PointsKey, float, float], SinglePointData] = { self._get_key(p.latitude, p.longitude, p.scenario, p.year): p for p in points } @@ -61,7 +62,7 @@ def _get_key(self, latitude: float, longitude: float, scenario: str, year: int): def get_hazard_data( self, requests: Sequence[HazardDataRequest] ) -> Mapping[HazardDataRequest, HazardDataResponse]: - response: Dict[HazardDataRequest, HazardDataResponse] = {} + response: dict[HazardDataRequest, HazardDataResponse] = {} for request in requests: point = self.points[ self._get_key( @@ -95,8 +96,8 @@ def test_using_point_based_hazard_model(): for lon, lat in zip(TestData.longitudes[0:1], TestData.latitudes[0:1]) ] # fmt: off - wind_return_periods = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0]) # noqa - wind_intensities = np.array([37.279999, 44.756248, 48.712502, 51.685001, 53.520000, 55.230000, 56.302502, 57.336250, 58.452499, 59.283749, 63.312500, 65.482498, 66.352501, 67.220001, 67.767502, 68.117500, 68.372498, 69.127502, 70.897499 ]) # noqa + wind_return_periods = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0]) + wind_intensities = np.array([37.279999, 44.756248, 48.712502, 51.685001, 53.520000, 55.230000, 56.302502, 57.336250, 58.452499, 59.283749, 63.312500, 65.482498, 66.352501, 67.220001, 67.767502, 68.117500, 68.372498, 69.127502, 70.897499 ]) # fmt: on point = SinglePointData( TestData.latitudes[0], diff --git a/tests/kernel/test_image_creation.py b/tests/kernel/test_image_creation.py index 24ba5277..7e03cc28 100644 --- a/tests/kernel/test_image_creation.py +++ b/tests/kernel/test_image_creation.py @@ -1,7 +1,6 @@ import io import os import warnings -from typing import Dict, Optional import numpy as np import pytest @@ -12,13 +11,13 @@ from physrisk.api.v1.hazard_data import HazardResource, MapInfo, ScenarioYears from physrisk.container import Container from physrisk.data import colormap_provider +from physrisk.data.image_creator import ImageCreator, image_from_packed_rgba, to_rgba +from physrisk.data.inventory import Inventory +from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.data.scenario_year_resolution import ( InterpolatedYearResolver, resolve_exact_year, ) -from physrisk.data.image_creator import ImageCreator, image_from_packed_rgba, to_rgba -from physrisk.data.inventory import Inventory -from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.data.zarr_reader import ZarrReader from physrisk.hazard_models.core_hazards import ( InventoryHazardResourceSelector, @@ -248,8 +247,8 @@ def _build_requester(inventory, zarr_store, enforce_permissions: bool = True): class TestHazardModelFactory(HazardModelFactory): def hazard_model( self, - interpolation: Optional[str] = "floor", - provider_max_requests: Dict[str, int] = {}, + interpolation: str | None = "floor", + provider_max_requests: dict[str, int] = {}, interpolate_years: bool = False, ): return ZarrHazardModel( diff --git a/tests/kernel/test_live_services.py b/tests/kernel/test_live_services.py index d56a8758..6a111307 100644 --- a/tests/kernel/test_live_services.py +++ b/tests/kernel/test_live_services.py @@ -1,10 +1,11 @@ import json import logging + import pytest import requests -from physrisk.container import Container import physrisk.requests +from physrisk.container import Container from tests.conftest import get_result_expected url = "https://physrisk-api-physrisk.apps.osc-cl1.apps.os-climate.org" diff --git a/tests/risk_models/test_portfolio_models.py b/tests/risk_models/test_portfolio_models.py index 9171a41c..8ea82d3c 100644 --- a/tests/risk_models/test_portfolio_models.py +++ b/tests/risk_models/test_portfolio_models.py @@ -1,9 +1,9 @@ import logging -from typing import Dict, Optional, cast +from typing import cast import numpy as np -from dependency_injector import providers import pytest +from dependency_injector import providers from physrisk.api.v1.common import Asset as APIAsset from physrisk.api.v1.common import Assets @@ -15,8 +15,8 @@ ScoreBasedRiskMeasuresForAssets, ) from physrisk.container import Container -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.assets import Asset, ManufacturingAsset, OEDAsset from physrisk.kernel.calculation import DefaultMeasuresFactory @@ -72,7 +72,7 @@ def test_impact_aggregation() -> None: impact_bin_edges_zero = np.array([0.0, 0.0]) impact_probabilities_zero = np.array([0.0]) - impacts: Dict[ImpactKey, list[AssetImpactResult]] = {} + impacts: dict[ImpactKey, list[AssetImpactResult]] = {} # in this example, we have 1,000,000 assets. We assume that 10% of assets have a non-zero impact probability. n_assets = 10000 generator = np.random.default_rng(seed=111) @@ -151,7 +151,7 @@ def test_impact_aggregation_multiple_portfolios() -> None: # portfolio A's assets have 10x the impact severity of portfolio B's portfolio_scale = {"A": 10.0, "B": 1.0} - impacts: Dict[ImpactKey, list[AssetImpactResult]] = {} + impacts: dict[ImpactKey, list[AssetImpactResult]] = {} n_assets = 4000 for i in range(n_assets): portfolio = "A" if i % 2 == 0 else "B" @@ -261,7 +261,7 @@ def air(hazard_type, edges, probs): def ik(asset, hazard_type, sc=scenario, yr=key_year): return ImpactKey(asset=asset, hazard_type=hazard_type, scenario=sc, key_year=yr) - impacts: Dict[ImpactKey, list[AssetImpactResult]] = { + impacts: dict[ImpactKey, list[AssetImpactResult]] = { # Wind – assets 0, 1 ik(a[0], Wind): [air(Wind, wind_edges, wind_probs)], ik(a[1], Wind): [air(Wind, wind_edges, wind_probs)], @@ -411,7 +411,7 @@ def test_impact_aggregation_acute_damage_and_disruption() -> None: disruption_edges = np.array([0.0, 0.3]) disruption_probs = np.array([0.04]) # mean 0.006 - impacts: Dict[ImpactKey, list[AssetImpactResult]] = { + impacts: dict[ImpactKey, list[AssetImpactResult]] = { ImpactKey( asset=asset, hazard_type=Wind, scenario=scenario, key_year=key_year ): [ @@ -476,7 +476,7 @@ def test_impact_aggregation_acute_duplicate_impact_type_raises() -> None: edges = np.array([0.0, 0.5]) probs = np.array([0.02]) - impacts: Dict[ImpactKey, list[AssetImpactResult]] = { + impacts: dict[ImpactKey, list[AssetImpactResult]] = { ImpactKey( asset=asset, hazard_type=Wind, scenario=scenario, key_year=key_year ): [ @@ -609,7 +609,7 @@ def test_impact_aggregation_end_to_end() -> None: class TestHazardModelFactory(HazardModelFactory): def hazard_model( self, - interpolation: Optional[str] = "floor", + interpolation: str | None = "floor", provider_max_requests: dict[str, int] = {}, interpolate_years: bool = False, ): @@ -689,7 +689,7 @@ def test_impact_aggregation_downtime_analytical_matches_mc() -> None: class TestHazardModelFactory(HazardModelFactory): def hazard_model( self, - interpolation: Optional[str] = "floor", + interpolation: str | None = "floor", provider_max_requests: dict[str, int] = {}, interpolate_years: bool = False, ): @@ -1003,7 +1003,7 @@ def hp(hazard_type, indicator_id, scenario): class TestHazardModelFactory(HazardModelFactory): def hazard_model( self, - interpolation: Optional[str] = "floor", + interpolation: str | None = "floor", provider_max_requests: dict[str, int] = {}, interpolate_years: bool = False, ): @@ -1113,7 +1113,7 @@ def portfolio_calculator(self, use_case_id: str): } def analytical_mean( - hazard: str, impact_type: str, aggregation_id: Optional[str] + hazard: str, impact_type: str, aggregation_id: str | None ) -> float: vals = [ v @@ -1126,7 +1126,7 @@ def analytical_mean( # Collect MC portfolio means from portfolio_impacts (ssp585 / 2050 only), split by # aggregation_id so the two sub-portfolios (None and 'group_b') are distinguishable. - mc_by_key: dict[tuple[str, str, Optional[str]], float] = { + mc_by_key: dict[tuple[str, str, str | None], float] = { (pi.key.hazard_type, pi.impact_type, pi.key.aggregation_id): pi.impact_mean for pi in res.portfolio_impacts if pi.key.scenario_id == "ssp585" and pi.key.year == "2050" diff --git a/tests/risk_models/test_risk_models.py b/tests/risk_models/test_risk_models.py index f647f370..8240bf23 100644 --- a/tests/risk_models/test_risk_models.py +++ b/tests/risk_models/test_risk_models.py @@ -1,6 +1,6 @@ """Test asset impact calculations.""" -from typing import Dict, Optional, Sequence, Type +from collections.abc import Sequence import numpy as np from dependency_injector import providers @@ -17,8 +17,8 @@ map_to_original_category, ) from physrisk.container import Container -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.assets import Asset, RealEstateAsset from physrisk.kernel.calculation import alternate_default_vulnerability_models_scores @@ -537,8 +537,8 @@ def test_generic_model_via_requests_default_vulnerability(): class TestHazardModelFactory(HazardModelFactory): def hazard_model( self, - interpolation: Optional[str] = "floor", - provider_max_requests: Dict[str, int] = {}, + interpolation: str | None = "floor", + provider_max_requests: dict[str, int] = {}, interpolate_years: bool = False, ): return hazard_model @@ -573,7 +573,7 @@ def hazard_model( class TestMeasuresWithHazardIndicatorID(RiskMeasureCalculator): def calc_measure( self, - hazard_type: Type[Hazard], + hazard_type: type[Hazard], base_impact: Sequence[AssetImpactResult], impact: Sequence[AssetImpactResult], ) -> Measure | dict[str | None, Measure]: @@ -717,8 +717,8 @@ def test_generic_model_via_requests_custom(): class TestHazardModelFactory(HazardModelFactory): def hazard_model( self, - interpolation: Optional[str] = "floor", - provider_max_requests: Dict[str, int] = {}, + interpolation: str | None = "floor", + provider_max_requests: dict[str, int] = {}, interpolate_years: bool = False, ): return hazard_model diff --git a/tests/test_oed_occupancy_update.py b/tests/test_oed_occupancy_update.py index 422a4af6..b5e13947 100644 --- a/tests/test_oed_occupancy_update.py +++ b/tests/test_oed_occupancy_update.py @@ -13,7 +13,6 @@ import pytest import requests - OED_OCCUPANCY_CSV_URL = ( "https://raw.githubusercontent.com/OasisLMF/ODS_OpenExposureData" "/main/OpenExposureData/OccupancyValues.csv" diff --git a/tests/vulnerability_models/test_config_based_vuln_model_param.py b/tests/vulnerability_models/test_config_based_vuln_model_param.py index c624cb48..db989bba 100644 --- a/tests/vulnerability_models/test_config_based_vuln_model_param.py +++ b/tests/vulnerability_models/test_config_based_vuln_model_param.py @@ -1,5 +1,3 @@ -from typing import Optional - import numpy as np from physrisk.kernel.assets import Asset @@ -17,14 +15,14 @@ class StubSelector: """Minimal ImpactFunctionSelector stand-in that always returns the same curve.""" - def __init__(self, curve: Optional[PiecewiseLinearImpactCurve]): + def __init__(self, curve: PiecewiseLinearImpactCurve | None): self.curve = curve def select(self, asset, hazard_type, indicator_id, impact_type): return self.curve -def threshold_curve(baseline_quantile_of_points_x: Optional[float]): +def threshold_curve(baseline_quantile_of_points_x: float | None): return PiecewiseLinearImpactCurve( curve_type="threshold", indicator_units="days/year", diff --git a/tests/vulnerability_models/test_config_based_vulnerability.py b/tests/vulnerability_models/test_config_based_vulnerability.py index a381764b..45dc65c2 100644 --- a/tests/vulnerability_models/test_config_based_vulnerability.py +++ b/tests/vulnerability_models/test_config_based_vulnerability.py @@ -1,5 +1,6 @@ import pathlib -from typing import Sequence, cast +from collections.abc import Sequence +from typing import cast import numpy as np import pandas as pd @@ -10,9 +11,9 @@ from physrisk.api.v1.common import Assets from physrisk.container import Container from physrisk.data.hazard_data_provider import HazardResourceSelector -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.inventory import EmbeddedInventory from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.zarr_reader import ZarrReader from physrisk.hazard_models.core_hazards import ( CoreFloodModels, diff --git a/tests/vulnerability_models/test_config_generate.py b/tests/vulnerability_models/test_config_generate.py index 6d001935..fc6326c3 100644 --- a/tests/vulnerability_models/test_config_generate.py +++ b/tests/vulnerability_models/test_config_generate.py @@ -1,6 +1,7 @@ +from pathlib import Path + import pytest -from pathlib import Path from physrisk.vulnerability_models.configuration.build_all_config import build_all diff --git a/tests/vulnerability_models/test_example_models.py b/tests/vulnerability_models/test_example_models.py index fbe6619f..1e94b1c4 100644 --- a/tests/vulnerability_models/test_example_models.py +++ b/tests/vulnerability_models/test_example_models.py @@ -1,8 +1,8 @@ import numpy as np from scipy import stats -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.assets import Asset, RealEstateAsset from physrisk.kernel.hazard_model import HazardEventDataResponse diff --git a/tests/vulnerability_models/test_hazard_selection.py b/tests/vulnerability_models/test_hazard_selection.py index ad78438d..8109cf7d 100644 --- a/tests/vulnerability_models/test_hazard_selection.py +++ b/tests/vulnerability_models/test_hazard_selection.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence import numpy as np diff --git a/tests/vulnerability_models/test_multi_interval_matcher.py b/tests/vulnerability_models/test_multi_interval_matcher.py index 9edb298f..c763768f 100644 --- a/tests/vulnerability_models/test_multi_interval_matcher.py +++ b/tests/vulnerability_models/test_multi_interval_matcher.py @@ -6,7 +6,6 @@ parse_interval, ) - # --------------------------------------------------------------------------- # parse_interval — tests our regex, coercion, and validation logic # --------------------------------------------------------------------------- diff --git a/tests/vulnerability_models/test_oed_based_config.py b/tests/vulnerability_models/test_oed_based_config.py index b881dcf2..04e47e3f 100644 --- a/tests/vulnerability_models/test_oed_based_config.py +++ b/tests/vulnerability_models/test_oed_based_config.py @@ -1,15 +1,15 @@ -from enum import Enum import importlib.resources +from enum import Enum from io import StringIO -from typing import Optional, Protocol, runtime_checkable +from typing import Protocol, runtime_checkable import numpy as np import pandas as pd import pytest -from physrisk.kernel.assets import OEDAsset, RealEstateAsset -from physrisk.kernel.hazards import RiverineInundation, PluvialInundation, Wind import physrisk.data.static.vulnerability.oed_hazus +from physrisk.kernel.assets import OEDAsset, RealEstateAsset +from physrisk.kernel.hazards import PluvialInundation, RiverineInundation, Wind from physrisk.kernel.impact_distrib import ImpactType from physrisk.vulnerability_models.config_based_impact_curves import ( PiecewiseLinearImpactCurve, @@ -30,7 +30,6 @@ ) from physrisk.vulnerability_models.vulnerability import VulnerabilityModelsFactory - oed_occ_to_hazus_occ = """ oed_occupancy_code,oed_occupancy_name,hazus_occupancy_codes,hazus_occupancy_code_weights 1000,Unknown,"COM1,COM10,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,EPP,ESS,IND1,IND2,IND3,IND4,IND5,IND6,NGC,ORF,PSTGC,PWE,PWT1,PWT2,RES1,RES2,RES3A,RES3B,RES3C,RES3D,RES3E,RES3F,RES4,RES5,RES6,WWT", @@ -337,9 +336,9 @@ def test_fema_hazus_mappings(): @runtime_checkable class OEDProperties(Protocol): # basement: int? oed_occupancy_code: int - number_of_storeys: Optional[int] - construction_code: Optional[int] - first_floor_elevation: Optional[float] + number_of_storeys: int | None + construction_code: int | None + first_floor_elevation: float | None class IDType(str, Enum): @@ -440,7 +439,7 @@ def test_common_occupancy_codes(): 3033: "Offshore Wind", } - for code in occupancies_to_test.keys(): + for code in occupancies_to_test: for num_storeys in (1, 2, 3, 4, 9, 900, -1, -2, -3): for basement in (0, 1, 2, 4, 5, 6): for construction_code in [5000, 5101, 5100, 5201, 5351, 6000]: diff --git a/tests/vulnerability_models/test_power_generating_asset_models.py b/tests/vulnerability_models/test_power_generating_asset_models.py index 46b18cd3..571e0705 100644 --- a/tests/vulnerability_models/test_power_generating_asset_models.py +++ b/tests/vulnerability_models/test_power_generating_asset_models.py @@ -2,7 +2,6 @@ import os import tarfile -from typing import List import numpy as np import pytest @@ -76,11 +75,11 @@ def wri_power_plant_assets(): Returns: pandas.DataFrame: A DataFrame containing the Global Power Plant Database. """ - with tarfile.open( - "./tests/api/wri_global_power_plant_database.tbz2", "r:bz2" - ) as tf: - with tf.extractfile("global_power_plant_database.csv") as f: - asset_list = pd.read_csv(f, low_memory=False) + with ( + tarfile.open("./tests/api/wri_global_power_plant_database.tbz2", "r:bz2") as tf, + tf.extractfile("global_power_plant_database.csv") as f, + ): + asset_list = pd.read_csv(f, low_memory=False) return asset_list @@ -329,7 +328,7 @@ def test_thermal_power_generation_portfolio(tmp_path): assert len(os.listdir(tmp_path)) == 1 -def api_assets(assets: List[Asset]): +def api_assets(assets: list[Asset]): items = [ physrisk.api.v1.common.Asset( asset_class=type(a).__name__, diff --git a/tests/vulnerability_models/test_real_estate_models.py b/tests/vulnerability_models/test_real_estate_models.py index e943af2e..81b43c01 100644 --- a/tests/vulnerability_models/test_real_estate_models.py +++ b/tests/vulnerability_models/test_real_estate_models.py @@ -1,7 +1,7 @@ import numpy as np -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.assets import RealEstateAsset from physrisk.kernel.hazards import CoastalInundation, RiverineInundation diff --git a/tests/vulnerability_models/test_wbgt_model.py b/tests/vulnerability_models/test_wbgt_model.py index eaacbd00..e368a357 100644 --- a/tests/vulnerability_models/test_wbgt_model.py +++ b/tests/vulnerability_models/test_wbgt_model.py @@ -1,10 +1,11 @@ -from typing import Sequence, Union, cast +from collections.abc import Sequence +from typing import cast import numpy as np import pytest -from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.scenario_year_resolution import resolve_exact_year from physrisk.hazard_models.core_hazards import get_default_hazard_resource_selector from physrisk.kernel.assets import Asset, IndustrialActivity from physrisk.kernel.hazard_model import ( @@ -43,7 +44,7 @@ def work_type_mapping(self): def get_data_requests( self, asset: Asset, *, scenario: str, year: int - ) -> Union[HazardDataRequest, Sequence[HazardDataRequest]]: + ) -> HazardDataRequest | Sequence[HazardDataRequest]: """Request the hazard data needed by the vulnerability model for a specific asset (this is a Google-style doc string) @@ -157,19 +158,19 @@ def get_impact( if asset.type in ["low", "high"]: a_historical = ( wbgt_responses[0].parameter - - abs((wbgt_responses[2].parameter - wbgt_responses[0].parameter)) / 2 + - abs(wbgt_responses[2].parameter - wbgt_responses[0].parameter) / 2 ) b_historical = ( wbgt_responses[0].parameter - + abs((wbgt_responses[2].parameter - wbgt_responses[0].parameter)) / 2 + + abs(wbgt_responses[2].parameter - wbgt_responses[0].parameter) / 2 ) a_scenario = ( wbgt_responses[1].parameter - - abs((wbgt_responses[3].parameter - wbgt_responses[1].parameter)) / 2 + - abs(wbgt_responses[3].parameter - wbgt_responses[1].parameter) / 2 ) b_scenario = ( wbgt_responses[1].parameter - + abs((wbgt_responses[3].parameter - wbgt_responses[1].parameter)) / 2 + + abs(wbgt_responses[3].parameter - wbgt_responses[1].parameter) / 2 ) elif asset.type == "medium": a_historical = ( @@ -182,11 +183,11 @@ def get_impact( ) a_scenario = ( wbgt_responses[1].parameter - - abs((wbgt_responses[3].parameter - wbgt_responses[1].parameter)) / 2 + - abs(wbgt_responses[3].parameter - wbgt_responses[1].parameter) / 2 ) b_scenario = ( wbgt_responses[1].parameter - + abs((wbgt_responses[5].parameter - wbgt_responses[1].parameter)) / 2 + + abs(wbgt_responses[5].parameter - wbgt_responses[1].parameter) / 2 ) # Estimation of the variance diff --git a/tests/vulnerability_models/test_wind_models.py b/tests/vulnerability_models/test_wind_models.py index fe89f1ee..cdbe6e17 100644 --- a/tests/vulnerability_models/test_wind_models.py +++ b/tests/vulnerability_models/test_wind_models.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence import numpy as np