Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support directional region validation #440

Merged
merged 8 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/user_guide/region.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ Regions can have attributes, for example a description or ISO3-codes. If the att
`iso3_codes` is provided, the item(s) are validated against a list of valid codes taken
from the `pycountry <https://github.com/flyingcircusio/pycountry>`_ package.

Directional data
----------------

For reporting of directional data (e.g., trade flows), "directional regions" can be
defined using a *>* separator. The region before the separator is the *origin*,
the region after the separator is the *destination*.

.. code:: yaml

- Trade Connections:
- China>Europe

Both the origin and destination regions must be defined in the region codelist.

Common regions
--------------

Expand Down
16 changes: 16 additions & 0 deletions nomenclature/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,22 @@ def check_iso3_codes(cls, v: list[str], info: ValidationInfo) -> list[str]:
raise ValueError(errors)
return v

@property
def is_directional(self) -> bool:
return ">" in self.name

@property
def destination(self) -> str:
if not self.is_directional:
raise ValueError("Non directional region does not have a destination")
return self.name.split(">")[1]

@property
def origin(self) -> str:
if not self.is_directional:
raise ValueError("Non directional region does not have an origin")
return self.name.split(">")[0]


class MetaCode(Code):
"""Code object with allowed values list
Expand Down
19 changes: 19 additions & 0 deletions nomenclature/codelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,10 +788,29 @@ def from_directory(
)
)
mapping[code.name] = code

if errors:
raise ValueError(errors)
return cls(name=name, mapping=mapping)

@field_validator("mapping")
@classmethod
def check_directional_regions(cls, v: dict[str, RegionCode]):
missing_regions = []
for region in v.values():
if region.is_directional:
if region.origin not in v:
missing_regions.append(
f"Region '{region.origin}' not defined for '{region.name}'"
phackstock marked this conversation as resolved.
Show resolved Hide resolved
)
if region.destination not in v:
missing_regions.append(
f"Region '{region.destination}' not defined for '{region.name}'"
phackstock marked this conversation as resolved.
Show resolved Hide resolved
)
danielhuppmann marked this conversation as resolved.
Show resolved Hide resolved
if missing_regions:
raise ValueError("\n".join(missing_regions))
return v

@property
def hierarchy(self) -> list[str]:
"""Return the hierarchies defined in the RegionCodeList
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- countries:
- Austria
- directional:
- Austria>Germany
12 changes: 7 additions & 5 deletions tests/data/codelist/region_codelist/simple/region.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
- common:
- World:
definition: The entire world
- World:
definition: The entire world
- countries:
- Some Country:
iso2: XY
iso3: XYZ
- Some Country:
iso2: XY
iso3: XYZ
- directional:
- Some Country>World
15 changes: 14 additions & 1 deletion tests/test_codelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ def test_region_codelist():
assert code["Some Country"].hierarchy == "countries"
assert code["Some Country"].iso2 == "XY"

assert "Some Country>World" in code
assert code["Some Country>World"].hierarchy == "directional"


def test_region_codelist_nonexisting_country_name():
"""Check that countries are validated against `nomenclature.countries`"""
Expand All @@ -205,6 +208,17 @@ def test_region_codelist_nonexisting_country_name():
)


def test_directional_region_codelist_nonexisting_country_name():
"""Check that directional regions have defined origin and destination"""
with pytest.raises(ValueError, match="Region 'Germany' not .* 'Austria>Germany'"):
RegionCodeList.from_directory(
"region",
MODULE_TEST_DATA_DIR
/ "region_codelist"
/ "directional_non-existing_component",
)


def test_region_codelist_str_country_name():
"""Check that country name as string is validated against `nomenclature.countries`"""
code = RegionCodeList.from_directory(
Expand Down Expand Up @@ -380,7 +394,6 @@ def test_RegionCodeList_filter():
def test_RegionCodeList_hierarchy():
"""Verifies that the hierarchy method returns a list"""


rcl = RegionCodeList.from_directory(
"Region", MODULE_TEST_DATA_DIR / "region_to_filter_codelist"
)
Expand Down
6 changes: 2 additions & 4 deletions tests/test_model_registration_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ def test_parse_model_registration(tmp_path):
)

# Test model mapping
with open(tmp_path / "Model 1.1_mapping.yaml", "r", encoding="utf-8") \
as file:
with open(tmp_path / "Model 1.1_mapping.yaml", "r", encoding="utf-8") as file:
obs_model_mapping = yaml.safe_load(file)
with open(
TEST_DATA_DIR
Expand All @@ -30,8 +29,7 @@ def test_parse_model_registration(tmp_path):
assert obs_model_mapping == exp_model_mapping

# Test model regions
with open(tmp_path / "Model 1.1_regions.yaml", "r", encoding="utf-8") \
as file:
with open(tmp_path / "Model 1.1_regions.yaml", "r", encoding="utf-8") as file:
obs_model_regions = yaml.safe_load(file)
exp_model_regions = [
{"Model 1.1": ["Model 1.1|Region 1", "Region 2", "Model 1.1|Region 3"]}
Expand Down
Loading