Skip to content
Draft
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
9 changes: 9 additions & 0 deletions src/flepimop2/_cli/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
default=False,
help="Should this command be run using dry run?",
),
"out_config": click.option(
"-o",
"--out-config",
type=click.Path(
exists=False, dir_okay=False, writable=True, path_type=pathlib.Path
),
default=None,
help="Path to write the resolved configuration to.",
),
"path": click.argument(
"path",
type=click.Path(
Expand Down
28 changes: 19 additions & 9 deletions src/flepimop2/_cli/_process_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from flepimop2._cli._cli_command import CliCommand
from flepimop2._utils._click import _get_config_target
from flepimop2.configuration import ConfigurationModel
from flepimop2.configuration._action import ActionModel
from flepimop2.process.abc import build as build_process


Expand All @@ -21,24 +22,33 @@ def run( # type: ignore[override]
self,
*,
config: Path,
dry_run: bool,
target: str | None = None,
out_config: Path | None = None,
dry_run: bool = False,
) -> None:
"""
Execute the processing step.

Args:
config: Path to the configuration file.
dry_run: Whether dry run mode is enabled.
target: Optional target process config to use.
out_config: Optional path to write the resolved configuration to.
dry_run: Whether dry run mode is enabled.
"""
configmodel = ConfigurationModel.from_yaml(config)
processconfig = configmodel.process
processtarget = _get_config_target(processconfig, target, "process")
config_model = ConfigurationModel.from_yaml(config)
process_config = config_model.process
process_target = _get_config_target(process_config, target, "process")

self.info(f"Processing configuration file: {config}")
self.info(f"Process section: {processconfig}")
self.info(f"Process target: {processtarget}")
self.info(f"Process section: {process_config}")
self.info(f"Process target: {process_target}")

action = ActionModel(action="process", name=config_model.name)

process_instance = build_process(process_target)
process_instance.execute(configuration=config_model, dry_run=dry_run)

process_instance = build_process(processtarget)
process_instance.execute(dry_run=dry_run)
if out_config is not None:
config_model.history.append(action)
config_model.to_yaml(out_config)
self.debug(f"Wrote resolved configuration to: {out_config}")
19 changes: 14 additions & 5 deletions src/flepimop2/_cli/_simulate_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from flepimop2._utils._click import _get_config_target
from flepimop2.backend.abc import build as build_backend
from flepimop2.configuration import ConfigurationModel
from flepimop2.configuration._action import ActionModel
from flepimop2.engine.abc import build as build_engine
from flepimop2.meta import RunMeta
from flepimop2.parameter.abc import build as build_parameter
from flepimop2.system.abc import build as build_system

Expand All @@ -28,16 +28,18 @@ def run( # type: ignore[override]
self,
*,
config: Path,
dry_run: bool,
target: str | None = None,
out_config: Path | None = None,
dry_run: bool = False,
) -> None:
"""
Execute the simulation.

Args:
config: Path to the configuration file.
dry_run: Whether dry run mode is enabled.
target: Optional target simulate config to use.
out_config: Optional path to write the resolved configuration to.
dry_run: Whether dry run mode is enabled.
"""
config_model = ConfigurationModel.from_yaml(config)
simulate_config = _get_config_target(config_model.simulate, target, "simulate")
Expand Down Expand Up @@ -77,5 +79,12 @@ def run( # type: ignore[override]
engine = build_engine(engine_config)
backend = build_backend(backend_config)

res = engine.run(system, simulate_config.t_eval, initial_state, params)
backend.save(res, RunMeta())
result = engine.run(system, simulate_config.t_eval, initial_state, params)

action = ActionModel(action="simulate", name=config_model.name)
backend.save(result, action)

if out_config is not None:
config_model.history.append(action)
config_model.to_yaml(out_config)
self.debug(f"Wrote resolved configuration to: {out_config}")
10 changes: 5 additions & 5 deletions src/flepimop2/backend/abc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@

from flepimop2._utils._module import _build
from flepimop2.configuration import ModuleModel
from flepimop2.meta import RunMeta
from flepimop2.configuration._action import ActionModel


class BackendABC(ABC):
"""Abstract base class for flepimop2 file IO backends."""

def save(self, data: NDArray[np.float64], run_meta: RunMeta) -> None:
def save(self, data: NDArray[np.float64], run_meta: ActionModel) -> None:
"""
Save a numpy array to storage.

Expand All @@ -24,7 +24,7 @@ def save(self, data: NDArray[np.float64], run_meta: RunMeta) -> None:
"""
return self._save(data, run_meta)

def read(self, run_meta: RunMeta) -> NDArray[np.float64]:
def read(self, run_meta: ActionModel) -> NDArray[np.float64]:
"""
Read a numpy array from storage.

Expand All @@ -37,12 +37,12 @@ def read(self, run_meta: RunMeta) -> NDArray[np.float64]:
return self._read(run_meta)

@abstractmethod
def _save(self, data: NDArray[np.float64], run_meta: RunMeta) -> None:
def _save(self, data: NDArray[np.float64], run_meta: ActionModel) -> None:
"""Backend-specific implementation for saving data."""
...

@abstractmethod
def _read(self, run_meta: RunMeta) -> NDArray[np.float64]:
def _read(self, run_meta: ActionModel) -> NDArray[np.float64]:
"""Backend-specific implementation for reading data."""
...

Expand Down
8 changes: 4 additions & 4 deletions src/flepimop2/backend/csv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from flepimop2.backend.abc import BackendABC
from flepimop2.configuration import ModuleModel
from flepimop2.meta import RunMeta
from flepimop2.configuration._action import ActionModel


class CsvBackend(ModuleModel, BackendABC):
Expand Down Expand Up @@ -39,7 +39,7 @@ def _validate_root(cls, root: Path) -> Path:
raise TypeError(msg)
return root

def _get_file_path(self, run_meta: RunMeta) -> Path:
def _get_file_path(self, run_meta: ActionModel) -> Path:
"""
Generate a dynamic file path based on run metadata.

Expand All @@ -54,7 +54,7 @@ def _get_file_path(self, run_meta: RunMeta) -> Path:
filename = f"{name_part}{run_meta.action}_{timestamp_str}.csv"
return self.root / filename

def _save(self, data: NDArray[np.float64], run_meta: RunMeta) -> None:
def _save(self, data: NDArray[np.float64], run_meta: ActionModel) -> None:
"""
Save a numpy array to a CSV file.

Expand All @@ -66,7 +66,7 @@ def _save(self, data: NDArray[np.float64], run_meta: RunMeta) -> None:
file_path.parent.mkdir(parents=True, exist_ok=True)
np.savetxt(file_path, data, delimiter=",")

def _read(self, run_meta: RunMeta) -> NDArray[np.float64]:
def _read(self, run_meta: ActionModel) -> NDArray[np.float64]:
"""
Read a numpy array from a CSV file.

Expand Down
2 changes: 2 additions & 0 deletions src/flepimop2/configuration/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Representations of parsed configuration files."""

__all__ = [
"ActionModel",
"ConfigurationModel",
"IdentifierString",
"ModuleGroupModel",
"ModuleModel",
"SimulateSpecificationModel",
]

from flepimop2.configuration._action import ActionModel
from flepimop2.configuration._configuration import ConfigurationModel
from flepimop2.configuration._module import ModuleGroupModel, ModuleModel
from flepimop2.configuration._simulate import SimulateSpecificationModel
Expand Down
29 changes: 29 additions & 0 deletions src/flepimop2/configuration/_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from datetime import UTC, datetime

from pydantic import BaseModel, Field


class ActionModel(BaseModel):
"""
Action model for flepimop2 configuration history and run metadata.

This model serves as both the metadata for individual runs and as entries
in the configuration history.

Attributes:
action: The action performed in the run (e.g., "simulate", "process").
timestamp: The timestamp when the run was executed.
name: An optional name for the run, typically pulled from the config.

Examples:
>>> from flepimop2.configuration._action import ActionModel
>>> action = ActionModel(action="simulate", name="test_run")
>>> action.action
'simulate'
>>> action.name
'test_run'
"""

action: str = Field(min_length=1, default="simulate")
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
name: str | None = None
3 changes: 3 additions & 0 deletions src/flepimop2/configuration/_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pydantic import Field, model_validator

from flepimop2.configuration._action import ActionModel
from flepimop2.configuration._module import ModuleGroupModel
from flepimop2.configuration._simulate import SimulateSpecificationModel
from flepimop2.configuration._types import IdentifierString
Expand All @@ -26,6 +27,7 @@ class ConfigurationModel(
process: A dictionary of process configurations.
parameters: A dictionary of parameter configurations.
simulate: A dictionary of simulation configurations.
history: A list of actions that have been performed.
"""

name: str | None = None
Expand All @@ -37,6 +39,7 @@ class ConfigurationModel(
simulate: dict[IdentifierString, SimulateSpecificationModel] = Field(
default_factory=dict
)
history: list[ActionModel] = Field(default_factory=list)

def _check_simulate_engines_or_systems(
self, kind: Literal["engine", "system", "backend"]
Expand Down
32 changes: 0 additions & 32 deletions src/flepimop2/meta.py

This file was deleted.

13 changes: 9 additions & 4 deletions src/flepimop2/process/abc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
from typing import Any

from flepimop2._utils._module import _build
from flepimop2.configuration import ModuleModel
from flepimop2._utils._module import _load_builder as _load_builder
from flepimop2._utils._module import _resolve_module_name as _resolve_module_name
from flepimop2.configuration import ConfigurationModel, ModuleModel
from flepimop2.exceptions import Flepimop2ValidationError, ValidationIssue


class ProcessABC(ABC):
"""Abstract base class for flepimop2 processing steps."""

def execute(self, *, dry_run: bool = False) -> None:
def execute(
self, *, configuration: ConfigurationModel, dry_run: bool = False
) -> None:
"""
Execute a processing step.

Args:
configuration: The configuration model containing all config data.
dry_run: If True, the process will not actually execute but will simulate
execution.

Expand All @@ -26,10 +31,10 @@ def execute(self, *, dry_run: bool = False) -> None:
if result:
raise Flepimop2ValidationError(result)
return None
return self._process(dry_run=dry_run)
return self._process(configuration=configuration, dry_run=dry_run)

@abstractmethod
def _process(self, *, dry_run: bool) -> None:
def _process(self, *, configuration: ConfigurationModel, dry_run: bool) -> None:
"""Backend-specific implementation for processing data."""
...

Expand Down
8 changes: 6 additions & 2 deletions src/flepimop2/process/shell/__init__.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking a bit about a simple shell invocation - what's a plausible standard way to pass the config information to a shell command invocation? I think approximately the path to the config file?

As in, let's imagine I want to define an arbitrary shell command, that's going to be executed in the working location that I call flepimop2. How would I run that command pointing to resolved location of the config file?

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from pydantic import Field

from flepimop2.configuration import ModuleModel
from flepimop2.configuration import ConfigurationModel, ModuleModel
from flepimop2.process.abc import ProcessABC


Expand All @@ -23,10 +23,14 @@ class ShellProcess(ModuleModel, ProcessABC):
command: str = Field(min_length=1)
args: list[str] = Field(default_factory=list)

def _process(self, *, dry_run: bool) -> None:
def _process(self, *, configuration: ConfigurationModel, dry_run: bool) -> None: # noqa: ARG002
"""
Execute a shell command.

Args:
configuration: The configuration model containing all config data.
dry_run: If True, the command will be echoed instead of executed.

Raises:
RuntimeError: If the command execution fails.
"""
Expand Down
3 changes: 2 additions & 1 deletion tests/_utils/_module/test__find_target_class/abc_only.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""A test module with a ProcessABC subclass but not BaseModel."""

from flepimop2.configuration import ConfigurationModel
from flepimop2.process.abc import ProcessABC


class TestProcess(ProcessABC):
"""A test process that inherits from ABC but not BaseModel."""

def _process(self, *, dry_run: bool) -> None:
def _process(self, *, configuration: ConfigurationModel, dry_run: bool) -> None:
"""Dummy process implementation."""
Loading