-
Notifications
You must be signed in to change notification settings - Fork 1
feat(flepimop2-op_system): adopt ModuleBase 0.3.0 API #142
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ | |
| from typing import ( | ||
| TYPE_CHECKING, | ||
| Any, | ||
| Literal, | ||
| NamedTuple, | ||
| cast, | ||
| ) | ||
|
|
@@ -54,7 +55,7 @@ | |
|
|
||
| from op_system import CompiledRhs, compile_spec | ||
|
|
||
| __version__ = "0.2.0" | ||
| __version__ = "0.3.0" | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Callable | ||
|
|
@@ -69,14 +70,30 @@ class _AxesMeta(NamedTuple): | |
| axis_coords: dict[str, np.ndarray] | ||
|
|
||
|
|
||
| class OpSystemSystem(SystemABC, module="flepimop2.system.op_system"): # noqa: D101 | ||
| class OpSystemSystem(SystemABC): | ||
| """flepimop2 System adapter that compiles an inline ``op_system`` spec. | ||
|
|
||
| The ``module`` discriminator is declared explicitly as a | ||
| ``Literal[...]`` field per the | ||
| ``flepimop2`` module-authoring guide; this is equivalent to the | ||
| ``module="..."`` class-keyword shortcut but makes the field visible | ||
| to static analyzers and to direct readers of the class. | ||
|
|
||
| The compiled RHS is built once during pydantic validation in | ||
| :meth:`model_post_init` and exposed through :meth:`step` / | ||
| :meth:`bind`. The connector accepts no extra top-level fields | ||
| (``extra="forbid"``); unknown keys must live inside ``spec`` itself. | ||
| """ | ||
|
|
||
| module: Literal["flepimop2.system.op_system"] = "flepimop2.system.op_system" | ||
|
|
||
| state_change: StateChangeEnum = StateChangeEnum.FLOW | ||
|
|
||
| spec: dict[str, object] = Field( | ||
| default=..., description="Inline op_system RHS specification (already loaded)" | ||
| ) | ||
|
|
||
| model_config = ConfigDict(extra="allow") | ||
| model_config = ConfigDict(extra="forbid") | ||
|
Comment on lines
-79
to
+96
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, this might be something to think about more on the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the default was set to extra ignore assuming people would have custom fields we wouldn't know about. Might make sense to go back to forbid, but offer a class option akin to "module"? |
||
|
|
||
| def model_post_init(self, context: Any) -> None: # noqa: ANN401 | ||
| """Compile `op_system` specification and prepare stepper and shape helpers. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,10 @@ | |
| import numpy as np | ||
| import pytest | ||
| from flepimop2.axis import AxisCollection | ||
| from flepimop2.module import PatchConflictMode | ||
| from flepimop2.parameter.abc import ModelStateSpecification, ParameterRequest | ||
| from flepimop2.typing import SystemProtocol | ||
| from pydantic import ValidationError | ||
|
|
||
| from flepimop2.system.op_system import OpSystemSystem | ||
|
|
||
|
|
@@ -580,3 +582,37 @@ def test_requested_parameters_honours_time_axis_option() -> None: | |
| sys = OpSystemSystem(spec=spec) | ||
| requested = sys.requested_parameters(AxisCollection()) | ||
| assert requested["beta"].axes == ("day",) | ||
|
|
||
|
|
||
| def test_module_field_is_literal() -> None: | ||
| """The `module` discriminator is exposed as an explicit Literal field.""" | ||
| field = OpSystemSystem.model_fields["module"] | ||
| assert field.default == "flepimop2.system.op_system" | ||
|
|
||
|
|
||
| def test_extra_top_level_fields_rejected(sir_spec: dict[str, object]) -> None: | ||
| """Unknown top-level kwargs are rejected (``extra='forbid'``).""" | ||
| with pytest.raises(ValidationError): | ||
| OpSystemSystem(spec=sir_spec, bogus_field=1) # type: ignore[call-arg] | ||
|
|
||
|
|
||
| def test_patch_replace_recompiles(sir_spec: dict[str, object]) -> None: | ||
| """Patching with REPLACE swaps in the new spec and recompiles the RHS.""" | ||
| sys_a = OpSystemSystem(spec=sir_spec) | ||
| si_spec: dict[str, object] = { | ||
| "kind": "expr", | ||
| "state": ["S", "I"], | ||
| "equations": {"S": "-beta * S * I", "I": "beta * S * I"}, | ||
| } | ||
| sys_b = OpSystemSystem(spec=si_spec) | ||
| patched = sys_a.patch(sys_b, conflict=PatchConflictMode.REPLACE) | ||
| y0 = np.array([0.9, 0.1], dtype=np.float64) | ||
| out = patched.step(np.float64(0.0), y0, beta=1.0) | ||
| np.testing.assert_allclose(out, np.array([-0.09, 0.09]), rtol=1e-12) | ||
|
|
||
|
|
||
| def test_patch_type_mismatch_raises(sir_spec: dict[str, object]) -> None: | ||
| """Patching against a non-matching concrete type raises ``TypeError``.""" | ||
| sys = OpSystemSystem(spec=sir_spec) | ||
| with pytest.raises(TypeError, match="module patching requires matching"): | ||
| sys.patch("not a module", conflict=PatchConflictMode.REPLACE) # type: ignore[arg-type] | ||
|
Comment on lines
+614
to
+618
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you need to test this, this is getting at the behavior of |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://accidda.github.io/flepimop2/0.3/development/creating-an-external-provider-package/#step-4-implement-the-npzbackend-class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this is more succinct: https://accidda.github.io/flepimop2/0.3/development/pydantic-for-modelers/#the-module-class-keyword
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bit odd to explicitly reference in the public documentation why implemented a particular way. Implementation details make sense as internal comments, not public API.