Executor
The dissmodel.executor module provides the standardised interface for packaging,
deploying, and reproducing simulations. It separates scientific logic from
execution infrastructure, so the same model code runs locally via CLI or
remotely via the platform API without modification.
The module exposes three building blocks:
| Component | Description |
|---|---|
ModelExecutor |
Abstract base class defining the four-phase lifecycle |
ExecutorRegistry |
Central registry mapping model names to executor classes |
execute_lifecycle |
Canonical orchestration function used by CLI and platform |
Lifecycle
Every executor follows the same four-phase lifecycle, orchestrated by
execute_lifecycle:
validate(record) # stateless pre-flight checks — no I/O
data = load(record) # resolve URI, apply column/band maps, return data
result = run(data, record) # simulation — no I/O here
record = save(result, record)
Each phase is timed independently. Timings are written to record.metrics
automatically (time_validate_sec, time_load_sec, time_run_sec,
time_save_sec, time_total_sec).
Minimal implementation
import geopandas as gpd
from dissmodel.executor import ModelExecutor, ExperimentRecord
from dissmodel.executor.cli import run_cli
from dissmodel.io import load_dataset, save_dataset
class MyExecutor(ModelExecutor):
name = "my_model"
def load(self, record: ExperimentRecord) -> gpd.GeoDataFrame:
gdf, checksum = load_dataset(record.source.uri)
record.source.checksum = checksum
return gdf
def run(self, data: gpd.GeoDataFrame, record: ExperimentRecord) -> gpd.GeoDataFrame:
# data is already loaded — no I/O here
params = record.parameters
# ... simulation logic ...
return data
def save(self, result, record: ExperimentRecord) -> ExperimentRecord:
uri = record.output_path or "output.gpkg"
checksum = save_dataset(result, uri)
record.output_path = uri
record.output_sha256 = checksum
record.status = "completed"
return record
if __name__ == "__main__":
run_cli(MyExecutor)
Auto-registration
Subclasses of ModelExecutor that define a name class attribute are
registered automatically in ExecutorRegistry via Python's
__init_subclass__. No boilerplate is required:
from dissmodel.executor import ExecutorRegistry
ExecutorRegistry.list() # ["my_model", ...]
ExecutorRegistry.get("my_model") # → MyExecutor class
The name attribute is also the key used in the TOML model registry:
[model]
class = "my_model"
package = "my_package"
execute_lifecycle
execute_lifecycle is the single source of truth for orchestration. It is
used by both dissmodel/executor/cli.py and the platform job_runner.py,
ensuring behavioural parity without code duplication.
from dissmodel.executor import execute_lifecycle
executor = MyExecutor()
record, timings = execute_lifecycle(executor, record)
print(timings)
# {
# "time_validate_sec": 0.0,
# "time_load_sec": 1.243,
# "time_run_sec": 0.872,
# "time_save_sec": 0.004,
# "time_total_sec": 2.119
# }
API Reference
dissmodel.executor.model_executor.ModelExecutor
Bases: ABC
Base interface for DisSModel executors.
Subclasses register themselves automatically in ExecutorRegistry via init_subclass — no boilerplate required.
The platform orchestrates the full lifecycle in order:
validate(record) # stateless pre-flight checks
data = load(record) # I/O — called once by the platform
result = run(data, record) # simulation — no I/O here
record = save(result, record)
Minimal implementation
class MyExecutor(ModelExecutor): name = "my_model"
def load(self, record: ExperimentRecord):
return gpd.read_file(record.source.uri)
def run(self, data, record: ExperimentRecord):
gdf = data # already loaded — no I/O here
# ... run simulation ...
return gdf
def save(self, result, record: ExperimentRecord) -> ExperimentRecord:
record.status = "completed"
return record
CLI usage
if name == "main": from dissmodel.executor.cli import run_cli run_cli(MyExecutor)
Source code in dissmodel/executor/model_executor.py
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
load(record)
abstractmethod
Load and resolve the input dataset.
Called by the platform before run(). The return value is passed directly as the first argument to run() — no second load occurs.
Responsibilities: - Resolve URI (s3://, http://, local path) - Apply column_map (vector) or band_map (raster) - Fill record.source.checksum with sha256 of the loaded data - Return data in the format expected by run()
Source code in dissmodel/executor/model_executor.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
run(data, record)
abstractmethod
Execute the simulation.
data is the direct return value of load(), injected by the platform.
No I/O should happen here — all loading is done by load().
Receives record with resolved_spec and parameters already merged. Returns raw result — format defined by the subclass and consumed by save().
Source code in dissmodel/executor/model_executor.py
75 76 77 78 79 80 81 82 83 84 85 86 | |
save(result, record)
abstractmethod
Persist the result and return the updated record.
Responsibilities: - Save output to destination (local path or s3://) - Fill record.output_path and record.output_sha256 - Set record.status = "completed" - Return the complete record
Source code in dissmodel/executor/model_executor.py
88 89 90 91 92 93 94 95 96 97 98 | |
validate(record)
Stateless pre-flight checks on the record — no data loading.
Called by the platform before load(). Override to check canonical columns/bands, value ranges, parameter constraints, etc. Raise ValueError with an actionable message if invalid.
Default implementation: no-op.
Source code in dissmodel/executor/model_executor.py
102 103 104 105 106 107 108 109 110 111 | |
dissmodel.executor.registry.ExecutorRegistry
Central registry mapping model names to executor classes.
Registration is automatic — subclasses of ModelExecutor register themselves via init_subclass without any boilerplate.
Source code in dissmodel/executor/registry.py
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
get(name)
classmethod
Resolve executor class by name. Raises KeyError with a clear message if not registered.
Source code in dissmodel/executor/registry.py
24 25 26 27 28 29 30 31 32 33 34 35 36 | |
list()
classmethod
Return all registered executor names.
Source code in dissmodel/executor/registry.py
38 39 40 41 | |
register(executor_cls)
classmethod
Called automatically by ModelExecutor.init_subclass.
Source code in dissmodel/executor/registry.py
19 20 21 22 | |
dissmodel.executor.runner.execute_lifecycle(executor, record)
Canonical lifecycle orchestration for DisSModel executors.
Runs validate → load → run → save in order, times each phase independently, and populates record.metrics with the results.
Used by both the CLI runner and the platform job_runner to ensure behavioural parity without code duplication.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
executor
|
'ModelExecutor'
|
An instantiated ModelExecutor subclass. |
required |
record
|
'ExperimentRecord'
|
The ExperimentRecord for this job. May be mutated in-place by load() (e.g. source.checksum) and save() (output_path, status). |
required |
Returns:
| Name | Type | Description |
|---|---|---|
record |
'ExperimentRecord'
|
The completed ExperimentRecord with status, output_path, and metrics populated. |
timings |
dict[str, float]
|
Dict with individual phase times and total: time_validate_sec, time_load_sec, time_run_sec, time_save_sec, time_total_sec. |
Source code in dissmodel/executor/runner.py
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |