Skip to content

Commit

Permalink
Add support for date-typed attributes.
Browse files Browse the repository at this point in the history
Clean up old commented code.
  • Loading branch information
Tyler Coles committed Jul 15, 2024
1 parent 340ba58 commit 130756b
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 45 deletions.
75 changes: 34 additions & 41 deletions epymorph/data_type.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
"""
Types for source data and attributes in epymorph.
"""
from datetime import date
from typing import Any, Sequence

import numpy as np
from numpy.typing import DTypeLike, NDArray

# _DataPyBasic = int | float | str
# _DataPyTuple = tuple[_DataPyBasic, ...]
# support recursively-nested lists
# _DataPyList = Sequence[Union[_DataPyBasic, _DataPyTuple, '_DataPyList']]
# _DataPy = _DataPyBasic | _DataPyTuple | _DataPyList

# DataPyScalar = _DataPyBasic | _DataPyTuple
# DataScalar = _DataPyBasic | _DataPyTuple | _DataNpScalar
# """The allowed scalar types (either python or numpy equivalents)."""

# Types for attribute declarations:
# these are expressed as Python types for simplicity.

ScalarType = type[int | float | str]
ScalarType = type[int | float | str | date]
StructType = Sequence[tuple[str, ScalarType]]
AttributeType = ScalarType | StructType
"""The allowed type declarations for epymorph attributes."""

ScalarValue = int | float | str
ScalarValue = int | float | str | date
StructValue = tuple[ScalarValue, ...]
AttributeValue = ScalarValue | StructValue
"""The allowed types for epymorph attribute values (specifically: default values)."""

ScalarDType = np.int64 | np.float64 | np.str_
ScalarDType = np.int64 | np.float64 | np.str_ | np.datetime64
StructDType = np.void
AttributeDType = ScalarDType | StructDType
"""The subset of numpy dtypes for use in epymorph: these map 1:1 with AttributeType."""
Expand All @@ -45,10 +36,19 @@ def dtype_as_np(dtype: AttributeType) -> np.dtype:
return np.dtype(np.float64)
if dtype == str:
return np.dtype(np.str_)
if isinstance(dtype, list):
return np.dtype(dtype)
if dtype == date:
return np.dtype(np.datetime64)
if isinstance(dtype, Sequence):
return np.dtype(list(dtype))
dtype = list(dtype)
if len(dtype) == 0:
raise ValueError(f"Unsupported dtype: {dtype}")
try:
return np.dtype([
(field_name, dtype_as_np(field_dtype))
for field_name, field_dtype in dtype
])
except TypeError:
raise ValueError(f"Unsupported dtype: {dtype}") from None
raise ValueError(f"Unsupported dtype: {dtype}")


Expand All @@ -60,50 +60,43 @@ def dtype_str(dtype: AttributeType) -> str:
return "float"
if dtype == str:
return "str"
if dtype == date:
return "date"
if isinstance(dtype, Sequence):
values = (f"({x[0]}, {dtype_str(x[1])})" for x in dtype)
return f"[{', '.join(values)}]"
dtype = list(dtype)
if len(dtype) == 0:
raise ValueError(f"Unsupported dtype: {dtype}")
try:
values = [
f"({field_name}, {dtype_str(field_dtype)})"
for field_name, field_dtype in dtype
]
return f"[{', '.join(values)}]"
except TypeError:
raise ValueError(f"Unsupported dtype: {dtype}") from None
raise ValueError(f"Unsupported dtype: {dtype}")


def dtype_check(dtype: AttributeType, value: Any) -> bool:
"""Checks that a value conforms to a given dtype. (Python types only.)"""
if dtype in (int, float, str):
if dtype in (int, float, str, date):
return isinstance(value, dtype)
if isinstance(dtype, Sequence):
dtype = list(dtype)
if not isinstance(value, tuple):
return False
if len(value) != len(dtype):
return False
return all((
dtype_check(vtype, v)
for ((_, vtype), v) in zip(dtype, value)
dtype_check(field_dtype, field_value)
for ((_, field_dtype), field_value) in zip(dtype, value)
))
raise ValueError(f"Unsupported dtype: {dtype}")


# ParamFunction = Callable[[int, int], DataScalar]
# """
# Params may be defined as functions of time (day) and geo node (index),
# returning a python or numpy scalar value.
# """

# RawParam = _DataPy | _DataNp | ParamFunction
# """
# Types for raw parameter values. Users can supply any of these forms when constructing
# simulation parameters.
# """

# AttributeScalar = _DataNpScalar
# AttributeArray = _DataNpArray
# """
# The type of all data attributes, whether in geo or params (after normalization).
# """


CentroidType: AttributeType = [('longitude', float), ('latitude', float)]
"""Structured epymorph type declaration for long/lat coordinates."""
CentroidDType: DTypeLike = [('longitude', float), ('latitude', float)]
CentroidDType: DTypeLike = [('longitude', np.float64), ('latitude', np.float64)]
"""Structured numpy dtype for long/lat coordinates."""

# SimDType being centrally-located means we can change it reliably.
Expand Down
33 changes: 29 additions & 4 deletions epymorph/test/data_type_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# pylint: disable=missing-docstring
import unittest
from datetime import date

import numpy as np

Expand All @@ -12,17 +13,36 @@ def test_dtype_as_np(self):
self.assertEqual(dtype_as_np(int), np.int64)
self.assertEqual(dtype_as_np(float), np.float64)
self.assertEqual(dtype_as_np(str), np.str_)
self.assertEqual(dtype_as_np(date), np.datetime64)

struct = [('foo', float), ('bar', int), ('baz', str)]
self.assertEqual(dtype_as_np(struct), np.dtype(struct))
struct = [('foo', float), ('bar', int), ('baz', str), ('bux', date)]
self.assertEqual(
dtype_as_np(struct),
[('foo', np.float64), ('bar', np.int64),
('baz', np.str_), ('bux', np.datetime64)]
)

def test_dtype_str(self):
self.assertEqual(dtype_str(int), "int")
self.assertEqual(dtype_str(float), "float")
self.assertEqual(dtype_str(str), "str")
self.assertEqual(dtype_str(date), "date")

struct = [('foo', float), ('bar', int), ('baz', str)]
self.assertEqual(dtype_str(struct), "[(foo, float), (bar, int), (baz, str)]")
struct = [('foo', float), ('bar', int), ('baz', str), ('bux', date)]
self.assertEqual(
dtype_str(struct),
"[(foo, float), (bar, int), (baz, str), (bux, date)]"
)

def test_dtype_invalid(self):
with self.assertRaises(ValueError):
dtype_as_np([int, float, str]) # type: ignore
with self.assertRaises(ValueError):
dtype_as_np([]) # type: ignore
with self.assertRaises(ValueError):
dtype_as_np(tuple()) # type: ignore
with self.assertRaises(ValueError):
dtype_as_np(('foo', 'bar', 'baz')) # type: ignore

def test_dtype_check(self):
self.assertTrue(dtype_check(int, 1))
Expand All @@ -31,6 +51,8 @@ def test_dtype_check(self):
self.assertTrue(dtype_check(float, 191827312.231234))
self.assertTrue(dtype_check(str, "hi"))
self.assertTrue(dtype_check(str, ""))
self.assertTrue(dtype_check(date, date(2024, 1, 1)))
self.assertTrue(dtype_check(date, date(1066, 10, 14)))
self.assertTrue(dtype_check([('x', int), ('y', int)], (1, 2)))
self.assertTrue(dtype_check([('a', str), ('b', float)], ("hi", 9273.3)))

Expand All @@ -43,6 +65,9 @@ def test_dtype_check(self):
self.assertFalse(dtype_check(float, 8273))
self.assertFalse(dtype_check(float, (32.0, 12.7, 99.9)))

self.assertFalse(dtype_check(date, '2024-01-01'))
self.assertFalse(dtype_check(date, 123))

dt1 = [('x', int), ('y', int)]
self.assertFalse(dtype_check(dt1, 1))
self.assertFalse(dtype_check(dt1, 78923.1))
Expand Down

0 comments on commit 130756b

Please sign in to comment.