Skip to content

Commit

Permalink
Merge pull request #669 from DHI/more
Browse files Browse the repository at this point in the history
More type hints
  • Loading branch information
ecomodeller authored Mar 14, 2024
2 parents 6127a04 + 07cc5ea commit bcc0217
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 306 deletions.
3 changes: 2 additions & 1 deletion mikeio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path
from platform import architecture
from collections.abc import Sequence
from typing import Any

# PEP0440 compatible formatted version, see:
# https://www.python.org/dev/peps/pep-0440/
Expand Down Expand Up @@ -135,7 +136,7 @@ def read(
return dfs.read(items=items, time=time, keepdims=keepdims, **kwargs)


def open(filename: str | Path, **kwargs):
def open(filename: str | Path, **kwargs: Any) -> Any:
"""Open a dfs/mesh file (and read the header)
The typical workflow for small dfs files is to read all data
Expand Down
57 changes: 24 additions & 33 deletions mikeio/dfs/_dfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,16 @@ class _Dfs123:

show_progress = False

def __init__(self, filename=None):
def __init__(self, filename: str | Path) -> None:
path = Path(filename)
if not path.exists():
raise FileNotFoundError(path)
self._filename = str(filename) if filename else None
self._end_time = None
self._is_equidistant = True
self._geometry = None # handled by subclass
dfs = DfsFileFactory.DfsGenericOpen(self._filename)
self._dfs = dfs
self._geometry: Any = None # Handled by subclass
n_items = len(dfs.ItemInfo)
self._items = self._get_item_info(list(range(n_items)))
if dfs.FileInfo.TimeAxis.TimeAxisType in {
Expand Down Expand Up @@ -310,17 +310,17 @@ def __init__(self, filename=None):
else:
self._timestep = None
self._time = None
self._n_timesteps = dfs.FileInfo.TimeAxis.NumberOfTimeSteps
self._n_timesteps: int = dfs.FileInfo.TimeAxis.NumberOfTimeSteps
projstr = dfs.FileInfo.Projection.WKTString
self._projstr = "NON-UTM" if not projstr else projstr
self._longitude = dfs.FileInfo.Projection.Longitude
self._latitude = dfs.FileInfo.Projection.Latitude
self._orientation = dfs.FileInfo.Projection.Orientation
self._deletevalue = dfs.FileInfo.DeleteValueFloat
self._projstr: str = "NON-UTM" if not projstr else projstr
self._longitude: float = dfs.FileInfo.Projection.Longitude
self._latitude: float = dfs.FileInfo.Projection.Latitude
self._orientation: float = dfs.FileInfo.Projection.Orientation
self._deletevalue: float = dfs.FileInfo.DeleteValueFloat

dfs.Close()

def __repr__(self):
def __repr__(self) -> str:
name = self.__class__.__name__
out = [f"<mikeio.{name}>"]

Expand Down Expand Up @@ -411,20 +411,17 @@ def read(

t_seconds[i] = itemdata.Time

time = pd.to_datetime(t_seconds, unit="s", origin=self.start_time) # type: ignore
time = pd.to_datetime(t_seconds, unit="s", origin=self.start_time)

items = _get_item_info(self._dfs.ItemInfo, item_numbers)

self._dfs.Close()
return Dataset(data_list, time, items, geometry=self.geometry, validate=False)

def _open(self):
def _open(self) -> None:
raise NotImplementedError("Should be implemented by subclass")

def _set_spatial_axis(self):
raise NotImplementedError("Should be implemented by subclass")

def _get_item_info(self, item_numbers):
def _get_item_info(self, item_numbers: Sequence[int]) -> List[ItemInfo]:
"""Read DFS ItemInfo
Parameters
Expand All @@ -444,8 +441,8 @@ def _get_item_info(self, item_numbers):
itemtype = EUMType(eumItem)
unit = EUMUnit(eumUnit)
data_value_type = self._dfs.ItemInfo[item].ValueType
item = ItemInfo(name, itemtype, unit, data_value_type)
items.append(item)
item_info = ItemInfo(name, itemtype, unit, data_value_type)
items.append(item_info)
return items

@property
Expand Down Expand Up @@ -490,37 +487,37 @@ def n_timesteps(self) -> int:
return self._n_timesteps

@property
def timestep(self) -> float:
def timestep(self) -> Any:
"""Time step size in seconds"""
# this will fail if the TimeAxisType is not calendar and equidistant, but that is ok
return self._dfs.FileInfo.TimeAxis.TimeStepInSeconds()

@property
def projection_string(self):
def projection_string(self) -> str:
return self._projstr

@property
def longitude(self):
def longitude(self) -> float:
"""Origin longitude"""
return self._longitude

@property
def latitude(self):
def latitude(self) -> float:
"""Origin latitude"""
return self._latitude

@property
def origin(self):
def origin(self) -> Any:
"""Origin (in own CRS)"""
return self.geometry.origin

@property
def orientation(self):
def orientation(self) -> Any:
"""Orientation (in own CRS)"""
return self.geometry.orientation

@property
def is_geo(self):
def is_geo(self) -> bool:
"""Are coordinates geographical (LONG/LAT)?"""
return self._projstr == "LONG/LAT"

Expand All @@ -530,17 +527,11 @@ def shape(self) -> Tuple[int, ...]:
"""Shape of the data array"""
pass

@property
@abstractmethod
def dx(self):
"""Step size in x direction"""
pass

def _validate_no_orientation_in_geo(self):
def _validate_no_orientation_in_geo(self) -> None:
if self.is_geo and abs(self._orientation) > 1e-6:
raise ValueError("Orientation is not supported for LONG/LAT coordinates")

def _origin_and_orientation_in_CRS(self):
def _origin_and_orientation_in_CRS(self) -> Tuple[Any, float]:
"""Project origin and orientation to projected CRS (if not LONG/LAT)"""
if self.is_geo:
origin = self._longitude, self._latitude
Expand All @@ -554,6 +545,6 @@ def _origin_and_orientation_in_CRS(self):
orientation=self._orientation,
)
# convert origin and orientation to projected CRS
origin = tuple(np.round(cart.Geo2Proj(lon, lat), 6))
origin = tuple(np.round(cart.Geo2Proj(lon, lat), 6)) # type: ignore
orientation = cart.OrientationProj
return origin, orientation
54 changes: 28 additions & 26 deletions mikeio/dfs/_dfs0.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def _write_dfs0(
if len(dataset.time) == 1:
dt = 1.0 # TODO
else:
dt = (dataset.time[1] - dataset.time[0]).total_seconds() # type: ignore
dt = (dataset.time[1] - dataset.time[0]).total_seconds()

temporal_axis = factory.CreateTemporalEqCalendarAxis(
TimeStepUnit.SECOND, system_start_time, 0, dt
Expand Down Expand Up @@ -65,7 +65,7 @@ def _write_dfs0(

delete_value = dfs.FileInfo.DeleteValueFloat

t_seconds = (dataset.time - dataset.time[0]).total_seconds().values # type: ignore
t_seconds = (dataset.time - dataset.time[0]).total_seconds().values

data = np.array(dataset.to_numpy(), order="F").astype(np.float64).T
data[np.isnan(data)] = delete_value
Expand Down Expand Up @@ -109,16 +109,16 @@ def __init__(self, filename: str | Path):
TimeAxisType.CalendarEquidistant,
TimeAxisType.CalendarNonEquidistant,
}:
self._start_time = dfs.FileInfo.TimeAxis.StartDateTime
self._start_time: datetime = dfs.FileInfo.TimeAxis.StartDateTime
else: # relative time axis
self._start_time = datetime(1970, 1, 1)

# time
self._n_timesteps = dfs.FileInfo.TimeAxis.NumberOfTimeSteps
self._n_timesteps: int = dfs.FileInfo.TimeAxis.NumberOfTimeSteps

dfs.Close()

def __repr__(self):
def __repr__(self) -> str:
out = ["<mikeio.Dfs0>"]
out.append(f"timeaxis: {repr(self._timeaxistype)}")

Expand Down Expand Up @@ -199,7 +199,9 @@ def read(

return ds

def _read(self, filename):
def _read(
self, filename: str
) -> tuple[list[np.ndarray], pd.DatetimeIndex, list[ItemInfo]]:
"""
Read all data from a dfs0 file.
"""
Expand Down Expand Up @@ -233,7 +235,7 @@ def _read(self, filename):
return data, time, items

@staticmethod
def _to_dfs_datatype(dtype):
def _to_dfs_datatype(dtype: Any = None) -> DfsSimpleType:
if dtype is None:
return DfsSimpleType.Float

Expand Down Expand Up @@ -339,7 +341,7 @@ def n_timesteps(self) -> int:
def timestep(self) -> float:
"""Time step size in seconds"""
if self._timeaxistype == TimeAxisType.CalendarEquidistant:
return self._source.FileInfo.TimeAxis.TimeStep
return self._source.FileInfo.TimeAxis.TimeStep # type: ignore
else:
raise ValueError("Time axis type not supported")

Expand All @@ -361,28 +363,28 @@ def time(self) -> pd.DatetimeIndex:


def series_to_dfs0(
self,
filename,
itemtype=None,
unit=None,
items=None,
title=None,
dtype=None,
):
self: pd.Series,
filename: str,
itemtype: EUMType | None = None,
unit: EUMUnit | None = None,
items: Sequence[ItemInfo] | None = None,
title: str | None = None,
dtype: Any | None = None,
) -> None:

df = pd.DataFrame(self)
df.to_dfs0(filename, itemtype, unit, items, title, dtype)


def dataframe_to_dfs0(
self,
filename,
itemtype=None,
unit=None,
items=None,
title=None,
dtype=None,
):
self: pd.DataFrame,
filename: str,
itemtype: EUMType | None = None,
unit: EUMUnit | None = None,
items: Sequence[ItemInfo] | None = None,
title: str = "",
dtype: Any | None = None,
) -> None:
"""
Create a dfs0
Expand Down Expand Up @@ -427,6 +429,6 @@ def dataframe_to_dfs0(


# Monkey patching onto Pandas classes
pd.DataFrame.to_dfs0 = dataframe_to_dfs0 # type: ignore
pd.DataFrame.to_dfs0 = dataframe_to_dfs0

pd.Series.to_dfs0 = series_to_dfs0 # type: ignore
pd.Series.to_dfs0 = series_to_dfs0
26 changes: 10 additions & 16 deletions mikeio/dfs/_dfs1.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ def _write_dfs1_header(filename: str | Path, ds: Dataset, title: str) -> DfsFile
class Dfs1(_Dfs123):
_ndim = 1

def __init__(self, filename):
def __init__(self, filename: str | Path) -> None:
super().__init__(filename)

self._dfs = DfsFileFactory.Dfs1FileOpen(str(filename))
self._x0 = self._dfs.SpatialAxis.X0
self._dx = self._dfs.SpatialAxis.Dx
self._nx = self._dfs.SpatialAxis.XCount
self._x0: float = self._dfs.SpatialAxis.X0
self._dx: float = self._dfs.SpatialAxis.Dx
self._nx: int = self._dfs.SpatialAxis.XCount

origin = self._longitude, self._latitude
self._geometry = Grid1D(
Expand All @@ -86,31 +86,25 @@ def __init__(self, filename):
orientation=self._orientation,
)

def _open(self):
def _open(self) -> None:
self._dfs = DfsFileFactory.Dfs1FileOpen(self._filename)

def _set_spatial_axis(self):
self._builder.SetSpatialAxis(
self._factory.CreateAxisEqD1(
eumUnit.eumUmeter, self._nx, self._x0, self._dx
)
)

@property
def geometry(self):
def geometry(self) -> Grid1D:
assert isinstance(self._geometry, Grid1D)
return self._geometry

@property
def x0(self):
def x0(self) -> float:
"""Start point of x values (often 0)"""
return self._x0

@property
def dx(self):
def dx(self) -> float:
"""Step size in x direction"""
return self._dx

@property
def nx(self):
def nx(self) -> int:
"""Number of node values"""
return self._nx
Loading

0 comments on commit bcc0217

Please sign in to comment.