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

Add default values to movement model attributes. #112

Merged
merged 1 commit into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 6 additions & 6 deletions epymorph/data/mm/centroids.movement
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[move-steps: per-day=2; duration=[1/3, 2/3]]

[attrib: source=geo; name=population; shape=N; dtype=int;
description="The total population at each node."]
[attrib: source=geo; name=population; shape=N; dtype=int; default_value=None;
comment="The total population at each node."]

[attrib: source=geo; name=centroid; shape=N; dtype=[(longitude, float), (latitude, float)];
description="The centroids for each node as (longitude, latitude) tuples."]
[attrib: source=geo; name=centroid; shape=N; dtype=[(longitude, float), (latitude, float)]; default_value=None;
comment="The centroids for each node as (longitude, latitude) tuples."]

[attrib: source=params; name=phi; shape=S; dtype=float;
description="Influences the distance that movers tend to travel."]
[attrib: source=params; name=phi; shape=S; dtype=float; default_value=40.0;
comment="Influences the distance that movers tend to travel."]

[predef: function =
def centroids_movement():
Expand Down
8 changes: 4 additions & 4 deletions epymorph/data/mm/flat.movement
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

[move-steps: per-day=2; duration=[1/3, 2/3]]

[attrib: source=geo; name=population; shape=N; dtype=int;
description="The total population at each node."]
[attrib: source=geo; name=population; shape=N; dtype=int; default_value=None;
comment="The total population at each node."]

[attrib: source=params; name=commuter_proportion; shape=S; dtype=float;
description="The ratio of the total population that is assumed to be commuters."]
[attrib: source=params; name=commuter_proportion; shape=S; dtype=float; default_value=0.2;
comment="The ratio of the total population that is assumed to be commuters."]

[predef: function=
def flat_predef():
Expand Down
4 changes: 2 additions & 2 deletions epymorph/data/mm/icecube.movement
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

[move-steps: per-day=2; duration=[2/3, 1/3]]

[attrib: source=geo; name=population; shape=N; dtype=int;
description="The total population at each node."]
[attrib: source=geo; name=population; shape=N; dtype=int; default_value=None;
comment="The total population at each node."]

[mtype: days=all; leave=1; duration=0d; return=2; function=
def icecube(t, src):
Expand Down
2 changes: 1 addition & 1 deletion epymorph/data/mm/no.movement
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
[move-steps: per-day=1; duration=[1]]

[mtype: days=all; leave=1; duration=0d; return=1; function=
def f(t, src, dst):
def no(t, src, dst):
return 0
]
12 changes: 6 additions & 6 deletions epymorph/data/mm/pei.movement
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

[move-steps: per-day=2; duration=[1/3, 2/3]]

[attrib: source=geo; name=commuters; shape=NxN; dtype=int;
description="A node-to-node commuters matrix."]
[attrib: source=geo; name=commuters; shape=NxN; dtype=int; default_value=None;
comment="A node-to-node commuters matrix."]

[attrib: source=params; name=move_control; shape=S; dtype=float;
description="A factor which modulates the number of commuters by conducting a binomial draw with this probability and the expected commuters from the commuters matrix."]
[attrib: source=params; name=move_control; shape=S; dtype=float; default_value=0.9;
comment="A factor which modulates the number of commuters by conducting a binomial draw with this probability and the expected commuters from the commuters matrix."]

[attrib: source=params; name=theta; shape=S; dtype=float;
description="A factor which allows for randomized movement by conducting a poisson draw with this factor times the average number of commuters between two nodes from the commuters matrix."]
[attrib: source=params; name=theta; shape=S; dtype=float; default_value=0.1;
comment="A factor which allows for randomized movement by conducting a poisson draw with this factor times the average number of commuters between two nodes from the commuters matrix."]

[predef: function =
def pei_movement():
Expand Down
12 changes: 6 additions & 6 deletions epymorph/data/mm/sparsemod.movement
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

[move-steps: per-day=2; duration=[1/3, 2/3]]

[attrib: source=geo; name=centroid; shape=N; dtype=[(longitude, float), (latitude, float)];
description="The centroids for each node as (longitude, latitude) tuples."]
[attrib: source=geo; name=centroid; shape=N; dtype=[(longitude, float), (latitude, float)]; default_value=None;
comment="The centroids for each node as (longitude, latitude) tuples."]

[attrib: source=geo; name=commuters; shape=NxN; dtype=int;
description="A node-to-node commuters matrix."]
[attrib: source=geo; name=commuters; shape=NxN; dtype=int; default_value=None;
comment="A node-to-node commuters matrix."]

[attrib: source=params; name=phi; shape=S; dtype=float;
description="Influences the distance that movers tend to travel."]
[attrib: source=params; name=phi; shape=S; dtype=float; default_value=40.0;
comment="Influences the distance that movers tend to travel."]

[predef: function =
def sparsemod_predef():
Expand Down
39 changes: 31 additions & 8 deletions epymorph/movement/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
from typing import Literal, NamedTuple, cast

import pyparsing as P
import sympy
from numpy.typing import DTypeLike
from pyparsing import pyparsing_common as PC

import epymorph.movement.parser_util as p
from epymorph.data_shape import DataShape
from epymorph.error import MmParseException
from epymorph.sympy_shim import to_symbol

# A MovementSpec has the following object structure:
#
Expand All @@ -28,7 +31,7 @@ class MoveSteps(NamedTuple):


move_steps: P.ParserElement = p.tag('move-steps', [
p.field('per-day', p.integer)('num_steps'),
p.field('per-day', PC.integer)('num_steps'),
p.field('duration', p.num_list)('steps')
])

Expand All @@ -49,26 +52,46 @@ class Attribute(NamedTuple):
"""The data model for an Attribute clause."""
# TODO: maybe replace with epymorph.simulation.AttributeDef in v0.5
name: str
shape: DataShape
dtype: DTypeLike # TODO: replace with DataDType in v0.5?
source: Literal['geo', 'params']
description: str
dtype: DTypeLike # TODO: replace with DataDType in v0.5?
shape: DataShape
symbol: sympy.Symbol
default_value: int | float | str \
| tuple[int | float | str, ...] \
| None
comment: str | None


attribute: P.ParserElement = p.tag('attrib', [
p.field('source', P.one_of('geo params')),
p.field('name', p.name),
p.field('shape', p.shape),
p.field('dtype', p.dtype),
p.field('description', p.quoted),
p.field('default_value', p.scalar_value | p.none),
p.field('comment', p.quoted),
])


@attribute.set_parse_action
def marshal_attribute(results: P.ParseResults):
"""Convert a pyparsing result to an Attribute."""
fields = results.as_dict()
return Attribute(fields['name'], fields['shape'], fields['dtype'][0], fields['source'], fields['description'])
dtype = fields['dtype'][0]
default_value = fields['default_value'][0]

# We can coerce integers to floats for convenience.
if dtype == float and isinstance(default_value, int):
default_value = float(default_value)

return Attribute(
name=fields['name'],
source=fields['source'],
dtype=dtype,
shape=fields['shape'],
symbol=to_symbol(fields['name']),
default_value=default_value,
comment=fields['comment'],
)


############################################################
Expand Down Expand Up @@ -125,9 +148,9 @@ class DailyClause(NamedTuple):

daily: P.ParserElement = p.tag('mtype', [
p.field('days', ('all' | day_list)),
p.field('leave', p.integer),
p.field('leave', PC.integer),
p.field('duration', p.duration),
p.field('return', p.integer),
p.field('return', PC.integer),
p.field('function', p.fn_body)
])
"""
Expand Down
41 changes: 35 additions & 6 deletions epymorph/movement/parser_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import numpy as np
import pyparsing as P
from pyparsing import pyparsing_common as PC

from epymorph.data_shape import parse_shape

Expand All @@ -15,9 +16,14 @@
E = P.ParserElement
_ = P.Suppress
l = P.Literal
fnumber = P.pyparsing_common.fnumber
fraction = P.pyparsing_common.fraction
integer = P.pyparsing_common.integer
none = l('None')


@none.set_parse_action
def marshal_none(_results: P.ParseResults):
"""Marshal a None value."""
return P.ParseResults([None])


quoted = P.QuotedString(quote_char='"', unquote_results=True) |\
P.QuotedString(quote_char="'", unquote_results=True)
Expand Down Expand Up @@ -55,7 +61,7 @@ def combine(p1, p2):
return bracketed(_(l(tag_name) + l(":")) - field_list)


num_list: E = bracketed(P.delimited_list(fraction | fnumber))
num_list: E = bracketed(P.delimited_list(PC.fraction | PC.fnumber))
"""Parser for a list of numbers in brackets, like: `[1,2,3]`"""


Expand All @@ -74,7 +80,7 @@ def to_days(self) -> int:
raise ValueError(f"unsupported unit {self.unit}")


duration = P.Group(integer + P.one_of('d w'))
duration = P.Group(PC.integer + P.one_of('d w'))
"""Parser for a duration expression."""


Expand All @@ -100,6 +106,9 @@ def my_function():
"""


# Shapes


shape = P.one_of("S T N TxN NxN")
"""
Describes the dimensions of an array in terms of the simulation.
Expand All @@ -117,6 +126,9 @@ def marshal_shape(results: P.ParseResults):
return P.ParseResults(parse_shape(value))


# dtypes


base_dtype = P.one_of('int float str')
"""One of epymorph's base permitted data types."""

Expand Down Expand Up @@ -151,7 +163,24 @@ def marshal_struct_dtype_field(results: P.ParseResults):
@struct_dtype.set_parse_action
def marshal_struct_dtype(results: P.ParseResults):
"""Convert a pyparsing results to a structured dtype object."""
return np.dtype(results.as_list())
return [results.as_list()]


dtype = base_dtype | struct_dtype


# Scalar Values


base_scalar = quoted | PC.number

tuple_scalar = _('(') + P.delimited_list(base_scalar, ',') + _(')')


@tuple_scalar.set_parse_action
def marshal_tuple_scalar(results: P.ParseResults):
"""Convert a pyparsing result to a tuple of values."""
return tuple(results.as_list())


scalar_value = tuple_scalar | base_scalar
32 changes: 19 additions & 13 deletions epymorph/movement/test/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from epymorph.movement.parser import (Attribute, DailyClause, MoveSteps,
attribute, daily, move_steps)
from epymorph.movement.parser_util import Duration
from epymorph.sympy_shim import to_symbol


class TestMoveSteps(unittest.TestCase):
Expand Down Expand Up @@ -45,28 +46,33 @@ def test_failures(self):
class TestAttribute(unittest.TestCase):
def test_successful(self):
cases = [
'[attrib: source=geo; name=commuters; shape=NxN; dtype=int; description="hey1"]',
'[attrib: source=params; name=move_control; shape=TxN; dtype=float; description="hey2"]',
'[attrib: source=params; name=move_control; shape=TxN; dtype=float;\n description="hey3"]',
'[attrib:\nsource=params;\nname=theta;\nshape=S;\ndtype=str;\ndescription="hey4"]',
'[attrib: source=geo; name=centroids; shape=N; dtype=[(longitude, float), (latitude, float)]; description="hey5"]',
'[attrib: source=geo; name=commuters; shape=NxN; dtype=int; default_value=None; comment="hey1"]',
'[attrib: source=params; name=move_control; shape=TxN; dtype=float; default_value=42; comment="hey2"]',
'[attrib: source=params; name=move_control; shape=TxN; dtype=float; default_value=-32.7;\n comment="hey3"]',
'[attrib:\nsource=params;\nname=theta;\nshape=S;\ndtype=str;\ndefault_value="hi";\ncomment="hey4"]',
'[attrib: source=geo; name=centroids; shape=N; dtype=[(longitude, float), (latitude, float)]; default_value=(1.0, 2.0); comment="hey5"]',
]
exps = [
Attribute('commuters', Shapes.NxN, np.int64, 'geo', 'hey1'),
Attribute('move_control', Shapes.TxN, np.float64, 'params', 'hey2'),
Attribute('move_control', Shapes.TxN, np.float64, 'params', 'hey3'),
Attribute('theta', Shapes.S, np.str_, 'params', 'hey4'),
Attribute('centroids', Shapes.N, CentroidDType, 'geo', 'hey5'),
Attribute('commuters', 'geo', np.int64, Shapes.NxN,
to_symbol('commuters'), None, 'hey1'),
Attribute('move_control', 'params', np.float64, Shapes.TxN,
to_symbol('move_control'), 42.0, 'hey2'),
Attribute('move_control', 'params', np.float64, Shapes.TxN,
to_symbol('move_control'), -32.7, 'hey3'),
Attribute('theta', 'params', np.str_, Shapes.S,
to_symbol('theta'), 'hi', 'hey4'),
Attribute('centroids', 'geo', CentroidDType, Shapes.N,
to_symbol('centroids'), (1.0, 2.0), 'hey5'),
]
for c, e in zip(cases, exps):
a = attribute.parse_string(c)[0]
self.assertEqual(a, e, f"{str(a)} did not match {str(e)}")

def test_failures(self):
cases = [
'[attrib: source=blah; name=commuters; shape=NxN; dtype=int; description="hey1"]',
'[attrib: source=params; name=move_control; shape=TxN; dtype=uint8; description="hey2"]',
'[attrib: source=params; name=move_control; shape=TxA; dtype=float; description="hey3"]',
'[attrib: source=blah; name=commuters; shape=NxN; dtype=int; default_value=23; comment="hey1"]',
'[attrib: source=params; name=move_control; shape=TxN; dtype=uint8; default_value=1; comment="hey2"]',
'[attrib: source=params; name=move_control; shape=TxA; dtype=float; default_value=27.3; comment="hey3"]',
]
for c in cases:
with self.assertRaises(ParseBaseException):
Expand Down
Loading