epymorph.database
A Database in epymorph is a way to organize values with our namespace system of three hierarchical components, as in: "strata::module::attribute_id". This gives us a lot of flexibility when specifying data requirements and values which fulfill those requirements. For example, you can provide a value for "::::population" to indicate that every strata and every module should use the same value if they need a "population" attribute. Or you can provide "*::init::population" to indicate that only initializers should use this value, and, presumably, another value is more appropriate for other modules like movement.
Hierarchical Database instances are also included which provide the ability to "layer" data for a simulation -- if the outermost database has a matching value, that value is used, otherwise the search for a match proceeds to the inner layers (recursively).
Match
Database
Database(data: dict[NamePattern, T])
A simple database implementation which provides namespaced key/value pairs. Namespaces are in the form "a::b::c", where "a" is the strata, "b" is the module, and "c" is the attribute name. Values are permitted to be assigned with wildcards (specified by asterisks), so that key "*::b::c" matches queries for "a::b::c" as well as "z::b::c".
This is intended for tracking parameter values as given by the user,
in constrast to DataResolver
which is for tracking fully-evaluated parameters.
Parameters:
-
data
(dict[NamePattern, T]
) –the values in this database
query
query(key: str | AbsoluteName) -> Match[T] | None
Query this database for a key match.
Parameters:
-
key
(str | AbsoluteName
) –the name to find; if given as a string, we must be able to parse it as a valid AbsoluteName
Returns:
DatabaseWithFallback
DatabaseWithFallback(
data: dict[NamePattern, T], fallback: Database[T]
)
A specialization of Database which has a fallback Database. If a match is not found in this DB's keys, the fallback is checked (recursively).
Parameters:
-
data
(dict[NamePattern, T]
) –the highest priority values in the database
-
fallback
(Database[T]
) –a database containing fallback values; if a match cannot be found in
data
, this database will be checked
query
query(key: str | AbsoluteName) -> Match[T] | None
Query this database for a key match.
Parameters:
-
key
(str | AbsoluteName
) –the name to find; if given as a string, we must be able to parse it as a valid AbsoluteName
Returns:
DatabaseWithStrataFallback
A specialization of Database which has a set of fallback Databases, one per strata. For example, we might query this DB for "a::b::c". If we do not have a match in our own key/values, but we do have a fallback DB for "a", we will query that fallback (which could continue recursively).
Parameters:
-
data
(dict[NamePattern, T]
) –the highest-priority values in the database
-
children
(dict[str, Database[T]]
) –fallback databases by strata; if a match can't be found in
data
, the database for the matching strata (if any) will be checked
query
query(key: str | AbsoluteName) -> Match[T] | None
Query this database for a key match.
Parameters:
-
key
(str | AbsoluteName
) –the name to find; if given as a string, we must be able to parse it as a valid AbsoluteName
Returns:
DataResolver
DataResolver(
dim: Dimensions,
values: dict[AbsoluteName, AttributeArray]
| None = None,
)
A sort of database for data attributes in the context of a simulation. Data (typically parameter values) are provided by the user as a collection of key-value pairs, with the values in several forms. These are evaluated to turn them into our internal data representation which is most useful for simulation execution (a numpy array, to be exact).
While the keys can be described using NamePatterns, when they are resolved they are tracked by their full AbsoluteName to facilitate lookups by the systems that use them. It is possible that different AbsoluteNames actually resolve to the same value (which is done in a memory-efficient way).
Meanwhile the usage of values adds its own complexity. When a value is used to fulfill the data requirements of a system, we want that value to be of a known type and shape. If two requirements are fulfilled by the same value, it it possible that the requirements will have different specifications for type and shape. Rather than be over-strict, and enforce that this can never happen, we allow this provided the given value can be successfully coerced to fit both requirements independently. For example, a scalar integer value can be coerced to both an N-shaped array of floats as well as a T-shaped array of integers. DataResolver accomplishes this flexibility in an efficient way by storing both the original values and all adapted values. If multiple requirements specify the same type/shape adaptation for one value, the adaptation only needs to happen once.
DataResolver is partially mutable -- new values can be added but values cannot be overwritten or removed.
Parameters:
-
dim
(Dimensions
) –the critical dimensions of the context in which these values have been evaluated; this is needed to perform shape adaptations
-
values
(dict[AbsoluteName, AttributeArray]
, default:None
) –a collection of values that should be in the resolver to begin with
raw_values
property
raw_values: Mapping[AbsoluteName, AttributeArray]
The mapping of raw values in the resolver, by absolute name.
WARNING: It is not safe to modify this mapping!
has
has(name: AbsoluteName) -> bool
Tests whether or not a given name is in this resolver.
Parameters:
-
name
(AbsoluteName
) –the name to test
Returns:
-
bool
–True if
name
is in this resolver
get_raw
get_raw(
name: str | NamePattern | AbsoluteName,
) -> AttributeArray
Retrieve a raw value that matches the given name.
Parameters:
-
name
(str | NamePattern | AbsoluteName
) –The name of the value to retrieve. This can be an AbsoluteName, a NamePattern, or a string. A string will be parsed as an AbsoluteName if possible, and fall back to parsing it as a NamePattern. In any case, the name must match exactly one value in order to return successfully.
Returns:
-
AttributeArray
–The requested value.
Raises:
-
ValueError
–If the name is not present in the resolver, or if the name is ambiguous and matches more than one value.
add
add(name: AbsoluteName, value: AttributeArray) -> None
Adds a value to this resolver. You may not overwrite an existing name.
Parameters:
-
name
(AbsoluteName
) –the name for the value
-
value
(AttributeArray
) –the value to add
Raises:
-
ValueError
–if
name
is already in this resolver
resolve
resolve(
name: AbsoluteName, definition: AttributeDef
) -> AttributeArray
Resolves a value known by name
to fit the given requirement definition
.
Parameters:
-
name
(AbsoluteName
) –the name of the value to resolve
-
definition
(AttributeDef
) –the definition of the requirement being fulfilled (which is needed because it contains the type and shape information)
Returns:
-
AttributeArray
–the resolved value, adapted if necessary
Raises:
-
AttributeException
–if the resolution fails
resolve_txn_series
resolve_txn_series(
requirements: Iterable[
tuple[AbsoluteName, AttributeDef]
],
tau_steps: int,
) -> Iterator[list[AttributeValue]]
Generates a series of values for the given requirements. Each item produced by the generator is a sequence of scalar values, one for each attribute (in the given order).
The sequence of items is generated in simulation order -- - day=0, tau step=0, node=0 => [beta, gamma, xi] - day=0, tau_step=0; node=1 => [beta, gamma, xi] - day=0, tau_step=1; node=0 => [beta, gamma, xi] - day=0, tau_step=1; node=1 => [beta, gamma, xi] - and so on.
This is a convenient alternative to resolving all of the TxN arrays separately, and managing the iteration yourself.
Parameters:
-
requirements
(Iterable[tuple[AbsoluteName, AttributeDef]]
) –The name-definition pairs for all of the attributes to include.
-
tau_steps
(int
) –The number of tau steps per day; since T in a TxN array is simulation days, this simply repeats values such that all of a day's tau steps see the same value.
to_dict
to_dict(
*, simplify_names: bool = False
) -> (
dict[AbsoluteName, AttributeArray]
| dict[str, AttributeArray]
)
Extract a dictionary from this DataResolver of all of its (non-adapted) keys and values.
Parameters:
-
simplify_names
(bool
, default:False
) –by default, names are returned as
AbsoluteName
objects; if True, return stringified names as a convenience
Returns:
-
dict[AbsoluteName, AttributeArray] | dict[str, AttributeArray]
–the dictionary of all values in this resolver, with names either simplified or not according to
simplify_names
Requirement
Resolution
MissingValue
dataclass
Bases: Resolution
Requirement was not resolved.
ParameterValue
dataclass
ParameterValue(cacheable: bool, pattern: NamePattern)
DefaultValue
dataclass
DefaultValue(default_value: AttributeValue)
ResolutionTree
dataclass
ResolutionTree(
resolution: Resolution,
children: tuple[ResolutionTree, ...],
)
Just the resolution part of a requirements tree; children are the dependencies of this resolution. If two values share the same resolution tree, and if the tree is comprised entirely of pure functions and values, then the resolution result would be the same.
RecursiveValue
Bases: Protocol
A parameter value that itself may depend on other parameter values.
requirements
instance-attribute
requirements: Sequence[AttributeDef]
Defines the data requirements for this value.
randomized
instance-attribute
randomized: bool
Should this value be re-evaluated every time it's referenced? (Mostly useful for randomized results.)
ReqTree
dataclass
A requirements tree describes how data requirements are resolved for a RUME.
Models used in the RUME have a set of requirements, each of which may be fulfilled
by RUME parameters or default values. RUME parameters may also have data
requirements, and those requirements may have requirements, etc., hence the need
to represent this as a tree structure. The top of a ReqTree
is a ReqRoot
and
each requirement is a ReqNode
. Each ReqNode
tracks the attribute itself
(its AbsoluteName
and AttributeDef
) and whether it is fulfilled and if so how.
to_string
abstractmethod
Convert this ReqTree to a string.
Parameters:
-
format_name
(Callable[[AbsoluteName], str]
, default:str
) –A method to convert an absolute name into a string. This allows you to control how absolute names are rendered.
-
depth
(int
, default:0
) –The resolution "depth" of this node in the tree. A requirement that itself is required by one other requirement would have a depth of 1. Top-level requirements have a depth of 0.
evaluate
evaluate(
scope: GeoScope | None,
time_frame: TimeFrame | None,
ipm: BaseCompartmentModel | None,
rng: Generator | None,
) -> DataResolver
Evaluate this tree. See: evaluate_requirements()
.
of
staticmethod
of(
requirements: Mapping[AbsoluteName, AttributeDef],
params: Database[V],
) -> ReqTree
Compute the requirements tree for the given set of requirements
and a database supplying values. Note that missing values do not
stop us from computing the tree -- these nodes will have MissingValue
as the resolution.
Parameters:
-
requirements
(Mapping[AbsoluteName, AttributeDef]
) –the top-level requirements of the tree
-
params
(Database[V]
) –the database of values, where each value may be "recursive" in the sense of having its own data requirements
Raises:
-
DataAttributeError
–If the tree cannot be evaluated, for instance, due to containing circular dependencies.
ReqNode
dataclass
ReqNode(
children: tuple[ReqNode, ...],
name: AbsoluteName,
definition: AttributeDef,
resolution: Resolution,
value: V | None,
)
A non-root node of a requirements tree, identifying a requirement, how (or if) it was resolved, and its value (if available).
to_string
Convert this ReqTree to a string.
Parameters:
-
format_name
(Callable[[AbsoluteName], str]
, default:str
) –A method to convert an absolute name into a string. This allows you to control how absolute names are rendered.
-
depth
(int
, default:0
) –The resolution "depth" of this node in the tree. A requirement that itself is required by one other requirement would have a depth of 1. Top-level requirements have a depth of 0.
as_res_tree
as_res_tree() -> ResolutionTree
Extracts the resolution tree from this requirements tree.
assert_can_adapt
assert_can_adapt(
data_type: AttributeType,
data_shape: DataShape,
dim: Dimensions,
value: AttributeArray,
) -> None
Check that we can adapt the given value
to the given type and shape,
given dimensional information. Raises AttributeException if not.
adapt
adapt(
data_type: AttributeType,
data_shape: DataShape,
dim: Dimensions,
value: AttributeArray,
) -> AttributeArray
Adapt the given value
to the given type and shape, given dimensional
information. Raises AttributeException if this fails.
is_recursive_value
is_recursive_value(
value: object,
) -> TypeGuard[RecursiveValue]
TypeGuard for RecursiveValues, implemented by single dispatch.
evaluate_param
evaluate_param(
value: object,
name: AbsoluteName,
data: DataResolver,
scope: GeoScope | None,
time_frame: TimeFrame | None,
ipm: BaseCompartmentModel | None,
rng: Generator | None,
) -> AttributeArray
Evaluate a parameter, transforming acceptable input values (type: ParamValue) to
the form required internally by epymorph (AttributeArray). This handles different
types of parameters by single dispatch, so there should be a registered
implementation for every unique type. It is possible that the user is attempting to
evaluate parameters with a partial context (scope
, time_frame
, ipm
, rng
),
and so one or more of these may be missing. In that case, parameter evaluation
is expected to happen on a "best effort" basis -- if no parameter requires the
missing scope elements, parameter evaluation succeeds. Otherwise, this is expected
to raise an AttributeException
.
Parameters:
-
value
(object
) –the value being evaluated
-
name
(AbsoluteName
) –the full name given to the value in the simulation context
-
data
(DataResolver
) –a DataResolver instance which should contain values for all of the data requirements needed by this value (only
RecursiveValues
have requirements) -
scope
(GeoScope
) –the geographic scope information of this simulation context, if available
-
time_frame
(TimeFrame
) –the temporal scope information of this simulation context, if available
-
ipm
(BaseCompartmentModel
) –the disease model for this simulation context, if available
-
rng
(Generator
) –the random number generator to use, if available
Returns:
-
AttributeArray
–the evaluated value
evaluate_requirements
evaluate_requirements(
req: ReqTree,
scope: GeoScope | None,
time_frame: TimeFrame | None,
ipm: BaseCompartmentModel | None,
rng: Generator | None,
) -> DataResolver
Evaluate all parameters in req
, using the given simulation context
(scope
, time_frame
, ipm
, rng
). You may attempt to evaluate parameters with
a partial context, so one or more of these may be missing. In that case, parameter
evaluation happens on a "best effort" basis -- if no parameter requires the missing
scope elements, parameter evaluation succeeds; otherwise raises an
AttributeException
.
Parameters:
-
req
(ReqTree
) –the requirements tree
-
scope
(GeoScope
) –the geographic scope information of this simulation context, if available
-
time_frame
(TimeFrame
) –the temporal scope information of this simulation context, if available
-
ipm
(BaseCompartmentModel
) –the disease model for this simulation context, if available
-
rng
(Generator
) –the random number generator to use, if available
Returns:
-
DataResolver
–the resolver containing all evaluated parameters