Skip to content

Commit

Permalink
Fix green infra and add tests (#349)
Browse files Browse the repository at this point in the history
* Fix green infra and add tests

* Fix black

* Add black linting version

* Add new black version

* Fix pydantic v2 validators

* Fix validator

* Fix black
  • Loading branch information
dladrichem authored Feb 26, 2024
1 parent c14d985 commit df7df8b
Show file tree
Hide file tree
Showing 7 changed files with 840 additions and 40 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
- name: Install dependencies
run: |
pip install black
- name: Check black version
run: black --version
- name: Run black
run: black --check .

Expand Down
13 changes: 3 additions & 10 deletions flood_adapt/integrator/sfincs_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,16 +346,9 @@ def add_green_infrastructure(
# Make sure no multipolygons are there
gdf_green_infra = gdf_green_infra.explode()

# Determine volume capacity of green infrastructure
if green_infrastructure.height.value != 0.0:
height = (
green_infrastructure.height.convert(UnitTypesLength("meters"))
* green_infrastructure.percent_area
)
volume = None
elif green_infrastructure.volume.value != 0.0:
height = None
volume = green_infrastructure.volume.convert(UnitTypesVolume("m3"))
# Volume is always already calculated and is converted to m3 for SFINCS
height = None
volume = green_infrastructure.volume.convert(UnitTypesVolume("m3"))

# HydroMT function: create storage volume
self.sf_model.setup_storage_volume(
Expand Down
11 changes: 7 additions & 4 deletions flood_adapt/object_model/hazard/measure/green_infrastructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
IGreenInfrastructure,
)
from flood_adapt.object_model.interface.site import ISite
from flood_adapt.object_model.io.unitfulvalue import UnitfulArea, UnitfulLength
from flood_adapt.object_model.io.unitfulvalue import (
UnitfulArea,
UnitfulHeight,
)


class GreenInfrastructure(HazardMeasure, IGreenInfrastructure):
Expand Down Expand Up @@ -55,7 +58,7 @@ def save(self, filepath: Union[str, os.PathLike]):
@staticmethod
def calculate_volume(
area: UnitfulArea,
height: UnitfulLength = UnitfulLength(value=0.0, units="meters"),
height: UnitfulHeight,
percent_area: float = 100.0,
) -> float:
"""Determine volume from area of the polygon and infiltration height
Expand All @@ -64,8 +67,8 @@ def calculate_volume(
----------
area : UnitfulArea
Area of polygon with units (calculated using calculate_polygon_area)
height : UnitfulLength, optional
Water height with units, by default 0.0
height : UnitfulHeight
Water height with units
percent_area : float, optional
Percentage area covered by green infrastructure [%], by default 100.0
Expand Down
90 changes: 70 additions & 20 deletions flood_adapt/object_model/interface/measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
from enum import Enum
from typing import Any, Optional, Union

from pydantic import BaseModel, validator
from pydantic import BaseModel, Field, field_validator, model_validator, validator

from flood_adapt.object_model.io.unitfulvalue import (
UnitfulDischarge,
UnitfulHeight,
UnitfulLength,
UnitfulLengthRefValue,
UnitfulVolume,
UnitTypesLength,
UnitTypesVolume,
)


Expand Down Expand Up @@ -50,7 +49,7 @@ class SelectionType(str, Enum):
class MeasureModel(BaseModel):
"""BaseModel describing the expected variables and data types of attributes common to all measures"""

name: str
name: str = Field(..., min_length=1)
description: Optional[str] = ""
type: Union[HazardType, ImpactType]

Expand All @@ -59,8 +58,27 @@ class HazardMeasureModel(MeasureModel):
"""BaseModel describing the expected variables and data types of attributes common to all impact measures"""

type: HazardType
polygon_file: Optional[str] = None
selection_type: SelectionType
polygon_file: Optional[str] = None

@field_validator("polygon_file")
@classmethod
def validate_polygon_file(cls, v: Optional[str]) -> Optional[str]:
if len(v) == 0:
raise ValueError("Polygon file path cannot be empty")
return v

@model_validator(mode="after")
def validate_selection_type(self) -> "HazardMeasureModel":
if (
self.selection_type
not in [SelectionType.aggregation_area, SelectionType.all]
and self.polygon_file is None
):
raise ValueError(
"If `selection_type` is not 'aggregation_area' or 'all', then `polygon_file` needs to be set."
)
return self


class ImpactMeasureModel(MeasureModel):
Expand Down Expand Up @@ -136,23 +154,55 @@ class PumpModel(HazardMeasureModel):
class GreenInfrastructureModel(HazardMeasureModel):
"""BaseModel describing the expected variables and data types of the "green infrastructure" hazard measure"""

volume: UnitfulVolume = UnitfulVolume(value=0.0, units=UnitTypesVolume.m3)
height: UnitfulLength = UnitfulLength(value=0.0, units=UnitTypesLength.meters)
volume: UnitfulVolume
height: Optional[UnitfulHeight] = None
aggregation_area_type: Optional[str] = None
aggregation_area_name: Optional[str] = None
percent_area: float = 100

@validator("volume")
def validate_volume(cls, volume: UnitfulVolume, values: Any) -> UnitfulVolume:
if volume.value <= 0:
raise ValueError("Volume cannot be zero or negative")
return volume

@validator("height")
def validate_height(cls, height: UnitfulLength, values: Any) -> UnitfulLength:
if height.value <= 0:
raise ValueError("Height cannot be zero or negative")
return height
percent_area: Optional[float] = Field(None, ge=0, le=100)

@model_validator(mode="after")
def validate_hazard_type_values(self) -> "GreenInfrastructureModel":
if self.type == HazardType.total_storage:
if self.height is not None or self.percent_area is not None:
raise ValueError(
"Height and percent_area cannot be set for total storage type measures"
)
return self
elif self.type == HazardType.water_square:
if self.percent_area is not None:
raise ValueError(
"Percentage_area cannot be set for water square type measures"
)
elif not isinstance(self.height, UnitfulHeight):
raise ValueError(
"Height needs to be set for water square type measures"
)
return self
elif self.type == HazardType.greening:
if not isinstance(self.height, UnitfulHeight) or not isinstance(
self.percent_area, float
):
raise ValueError(
"Height and percent_area needs to be set for greening type measures"
)
else:
raise ValueError(
"Type must be one of 'water_square', 'greening', or 'total_storage'"
)
return self

@model_validator(mode="after")
def validate_selection_type_values(self) -> "GreenInfrastructureModel":
if self.selection_type == SelectionType.aggregation_area:
if self.aggregation_area_name is None:
raise ValueError(
"If `selection_type` is 'aggregation_area', then `aggregation_area_name` needs to be set."
)
if self.aggregation_area_type is None:
raise ValueError(
"If `selection_type` is 'aggregation_area', then `aggregation_area_type` needs to be set."
)
return self


class IMeasure(ABC):
Expand Down
24 changes: 18 additions & 6 deletions flood_adapt/object_model/io/unitfulvalue.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from enum import Enum

from pydantic import BaseModel
from pydantic import BaseModel, Field, root_validator


class UnitTypesLength(str, Enum):
Expand Down Expand Up @@ -100,15 +100,27 @@ def convert(self, new_units: UnitTypesLength) -> float:
new_conversion = 3.28084
elif new_units == "inch":
new_conversion = 1.0 / 0.0254
elif self.units == "miles":
elif new_units == "miles":
new_conversion = 1.0 / 1609.344
else:
ValueError("Invalid length units")
return conversion * new_conversion * self.value


class UnitfulHeight(UnitfulLength):
"""A special type of length that is always positive and non-zero. Used for heights."""

value: float = Field(..., gt=0)

@root_validator(pre=True)
def convert_length_to_height(cls, obj):
if isinstance(obj, UnitfulLength):
return UnitfulHeight(value=obj.value, units=obj.units)
return obj


class UnitfulArea(ValueUnitPair):
value: float
value: float = Field(..., gt=0)
units: UnitTypesArea

def convert(self, new_units: UnitTypesArea) -> float:
Expand All @@ -127,7 +139,7 @@ def convert(self, new_units: UnitTypesArea) -> float:
# first, convert to meters
if self.units == "cm2":
conversion = 1.0 / 10000 # meters
if self.units == "mm2":
elif self.units == "mm2":
conversion = 1.0 / 1000000 # meters
elif self.units == "m2":
conversion = 1.0 # meters
Expand All @@ -139,7 +151,7 @@ def convert(self, new_units: UnitTypesArea) -> float:
# second, convert to new units
if new_units == "cm2":
new_conversion = 10000.0
if new_units == "mm2":
elif new_units == "mm2":
new_conversion = 1000000.0
elif new_units == "m2":
new_conversion = 1.0
Expand Down Expand Up @@ -267,7 +279,7 @@ def convert(self, new_units: UnitTypesIntensity) -> float:


class UnitfulVolume(ValueUnitPair):
value: float
value: float = Field(..., gt=0)
units: UnitTypesVolume

def convert(self, new_units: UnitTypesVolume) -> float:
Expand Down
Loading

0 comments on commit df7df8b

Please sign in to comment.