Skip to content

Commit

Permalink
Add default values to movement model attributes. (#112)
Browse files Browse the repository at this point in the history
Also: "description" is now "comment"
  • Loading branch information
JavadocMD authored May 16, 2024
1 parent d09a0ed commit e1f00fa
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 52 deletions.
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

0 comments on commit e1f00fa

Please sign in to comment.