stochops 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
stochops/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """stochops: A fluent Monte Carlo & Latin Hypercube Sampling simulation engine for OpenSeesPy."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from stochops.adaptive.solver_fallback import AdaptiveAnalysisRunner, ConvergenceError
6
+ from stochops.builder import SimulationBuilder, SimulationEngine
7
+ from stochops.execution.process_pool import ProcessPoolEngine
8
+ from stochops.parameters import (
9
+ BaseParameter,
10
+ ContinuousParam,
11
+ DiscreteParam,
12
+ Distribution,
13
+ GroundMotionSetParam,
14
+ )
15
+ from stochops.protocols import (
16
+ ExecutionEngine,
17
+ ParametricModel,
18
+ ResultSink,
19
+ Sampler,
20
+ )
21
+ from stochops.samplers.lhs import LHSSampler
22
+ from stochops.samplers.mc import MCSampler
23
+ from stochops.sinks.parquet import ParquetSink
24
+
25
+ try:
26
+ __version__ = version("stochops")
27
+ except PackageNotFoundError: # pragma: no cover
28
+ __version__ = "0.0.0-dev"
29
+
30
+ __all__ = [
31
+ # Core Orchestration
32
+ "SimulationBuilder",
33
+ "SimulationEngine",
34
+ # Parameter Definitions
35
+ "BaseParameter",
36
+ "ContinuousParam",
37
+ "DiscreteParam",
38
+ "Distribution",
39
+ "GroundMotionSetParam",
40
+ # Sampling Strategies
41
+ "LHSSampler",
42
+ "MCSampler",
43
+ # Parallel Execution Engines
44
+ "ProcessPoolEngine",
45
+ # Data Persistence Sinks
46
+ "ParquetSink",
47
+ # Adaptive Solver Utilities
48
+ "AdaptiveAnalysisRunner",
49
+ "ConvergenceError",
50
+ # Strategy Protocols
51
+ "ParametricModel",
52
+ "Sampler",
53
+ "ExecutionEngine",
54
+ "ResultSink",
55
+ ]
@@ -0,0 +1,6 @@
1
+ """Adaptive solver utilities for stochops."""
2
+
3
+ from stochops.adaptive.solver_fallback import AdaptiveAnalysisRunner
4
+ from stochops.exceptions import ConvergenceError
5
+
6
+ __all__ = ["AdaptiveAnalysisRunner", "ConvergenceError"]
@@ -0,0 +1,89 @@
1
+ """Adaptive solver fallback and automated substepping for OpenSeesPy analyses."""
2
+
3
+ import openseespy.opensees as ops
4
+
5
+ from stochops.exceptions import ConvergenceError
6
+
7
+ __all__ = ["AdaptiveAnalysisRunner", "ConvergenceError"]
8
+
9
+
10
+ class AdaptiveAnalysisRunner:
11
+ """Orchestrates adaptive algorithm switching and recursive substepping during OpenSeesPy analyses.
12
+
13
+ Intercepts numerical divergence during transient step evaluations by cycling through alternative
14
+ solution algorithms and executing recursive step-halving before failing.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ algorithms: list[str] | None = None,
20
+ enable_substepping: bool = True,
21
+ max_substep_depth: int = 4,
22
+ ) -> None:
23
+ """Initializes the AdaptiveAnalysisRunner.
24
+
25
+ Args:
26
+ algorithms: Ordered list of OpenSees solution algorithms to attempt upon non-convergence.
27
+ Defaults to ["Newton", "KrylovNewton", "NewtonWithLineSearch", "BFGS", "Broyden"].
28
+ enable_substepping: Whether to execute recursive step-halving if all algorithms fail.
29
+ max_substep_depth: Maximum recursion depth for substepping (depth 4 = dt / 16).
30
+ """
31
+ self.algorithms = algorithms or [
32
+ "Newton",
33
+ "KrylovNewton",
34
+ "NewtonWithLineSearch",
35
+ "BFGS",
36
+ "Broyden",
37
+ ]
38
+ self.enable_substepping = enable_substepping
39
+ self.max_substep_depth = max_substep_depth
40
+ self.primary_algorithm = self.algorithms[0]
41
+
42
+ def analyze_step(self, dt: float) -> None:
43
+ """Executes a single dynamic transient analysis step with adaptive retries.
44
+
45
+ Args:
46
+ dt: Time step increment for the analysis step.
47
+
48
+ Raises:
49
+ ConvergenceError: If all algorithm options and substepping levels fail to converge.
50
+ """
51
+ # 1. Attempt primary algorithm
52
+ ops.algorithm(self.primary_algorithm)
53
+ ok = ops.analyze(1, dt)
54
+ if ok == 0:
55
+ return
56
+
57
+ # 2. Fallback sequence: try alternative solution algorithms
58
+ for fallback_alg in self.algorithms[1:]:
59
+ ops.algorithm(fallback_alg)
60
+ ok = ops.analyze(1, dt)
61
+ if ok == 0:
62
+ # Reset algorithm to primary for subsequent steps
63
+ ops.algorithm(self.primary_algorithm)
64
+ return
65
+
66
+ # 3. Fallback sequence: recursive step-halving
67
+ if self.enable_substepping:
68
+ self._execute_substep(dt / 2.0, current_depth=1)
69
+ ops.algorithm(self.primary_algorithm)
70
+ return
71
+
72
+ raise ConvergenceError(
73
+ f"Analysis step failed to converge at dt={dt} across all candidate algorithms."
74
+ )
75
+
76
+ def _execute_substep(self, reduced_dt: float, current_depth: int) -> None:
77
+ """Recursively splits a failing step into two half-steps."""
78
+ if current_depth > self.max_substep_depth:
79
+ raise ConvergenceError(
80
+ f"Maximum substepping depth ({self.max_substep_depth}) exceeded at dt={reduced_dt}."
81
+ )
82
+
83
+ # Execute two half-steps to cover the original step interval
84
+ for _ in range(2):
85
+ try:
86
+ self.analyze_step(reduced_dt)
87
+ except ConvergenceError:
88
+ # If a half-step fails, recurse deeper
89
+ self._execute_substep(reduced_dt / 2.0, current_depth + 1)
stochops/builder.py ADDED
@@ -0,0 +1,140 @@
1
+ """Simulation builder and engine orchestration for stochops."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from stochops.protocols import (
7
+ ExecutionEngine,
8
+ ParametricModel,
9
+ ResultSink,
10
+ Sampler,
11
+ )
12
+
13
+
14
+ class SimulationEngine:
15
+ """Orchestrates batch generation, execution dispatch, and data streaming."""
16
+
17
+ def __init__(
18
+ self,
19
+ model: ParametricModel | Callable[[dict[str, Any]], dict[str, Any]],
20
+ sampler: Sampler,
21
+ execution_engine: ExecutionEngine,
22
+ sink: ResultSink,
23
+ batch_size: int = 100,
24
+ ) -> None:
25
+ self.model = model
26
+ self.sampler = sampler
27
+ self.execution_engine = execution_engine
28
+ self.sink = sink
29
+ self.batch_size = batch_size
30
+
31
+ def run(self, total_samples: int) -> None:
32
+ """Executes the simulation pipeline across total_samples in chunks of batch_size.
33
+
34
+ Args:
35
+ total_samples: Total number of Monte Carlo / LHS realizations to run.
36
+ """
37
+ if total_samples <= 0:
38
+ raise ValueError("total_samples must be greater than zero.")
39
+
40
+ remaining = total_samples
41
+ try:
42
+ while remaining > 0:
43
+ current_batch_size = min(self.batch_size, remaining)
44
+
45
+ # 1. Generate sample realizations
46
+ samples = self.sampler.sample(current_batch_size)
47
+
48
+ # 2. Dispatch batch across worker processes
49
+ results = self.execution_engine.run_batch(self.model, samples)
50
+
51
+ # 3. Stream results to persistent sink
52
+ self.sink.write(results)
53
+
54
+ remaining -= current_batch_size
55
+ finally:
56
+ # Always ensure storage buffers are flushed, file locks released,
57
+ # and long-lived worker resources are shut down.
58
+ self.sink.close()
59
+ self.execution_engine.close()
60
+
61
+
62
+ class SimulationBuilder:
63
+ """Fluent builder for constructing an immutable SimulationEngine instance."""
64
+
65
+ def __init__(self) -> None:
66
+ self._model: (
67
+ ParametricModel | Callable[[dict[str, Any]], dict[str, Any]] | None
68
+ ) = None
69
+ self._sampler: Sampler | None = None
70
+ self._execution_engine: ExecutionEngine | None = None
71
+ self._sink: ResultSink | None = None
72
+ self._batch_size: int = 100
73
+
74
+ def model(
75
+ self, model: ParametricModel | Callable[[dict[str, Any]], dict[str, Any]]
76
+ ) -> "SimulationBuilder":
77
+ """Sets the structural model evaluation callback."""
78
+ self._model = model
79
+ return self
80
+
81
+ def sampler(self, sampler: Sampler) -> "SimulationBuilder":
82
+ """Sets the sampling strategy (e.g., LHSSampler)."""
83
+ self._sampler = sampler
84
+ return self
85
+
86
+ def execution_engine(
87
+ self, engine: ExecutionEngine
88
+ ) -> "SimulationBuilder":
89
+ """Sets the parallel dispatch engine (e.g., ProcessPoolEngine)."""
90
+ self._execution_engine = engine
91
+ return self
92
+
93
+ def sink(self, sink: ResultSink) -> "SimulationBuilder":
94
+ """Sets the data persistence sink (e.g., ParquetSink)."""
95
+ self._sink = sink
96
+ return self
97
+
98
+ def batch_size(self, batch_size: int) -> "SimulationBuilder":
99
+ """Sets the chunk size for batch processing and memory management."""
100
+ if batch_size <= 0:
101
+ raise ValueError("batch_size must be greater than zero.")
102
+ self._batch_size = batch_size
103
+ return self
104
+
105
+ def build(self) -> SimulationEngine:
106
+ """Validates strategy protocol slots and constructs the SimulationEngine.
107
+
108
+ Raises:
109
+ ValueError: If any required strategy (model, sampler, engine, sink) is missing.
110
+ """
111
+ missing_slots: list[str] = []
112
+
113
+ if self._model is None:
114
+ missing_slots.append("model (via .model())")
115
+ if self._sampler is None:
116
+ missing_slots.append("sampler (via .sampler())")
117
+ if self._execution_engine is None:
118
+ missing_slots.append("execution_engine (via .execution_engine())")
119
+ if self._sink is None:
120
+ missing_slots.append("sink (via .sink())")
121
+
122
+ if missing_slots:
123
+ raise ValueError(
124
+ "Cannot build SimulationEngine. Missing required strategy slots: "
125
+ + ", ".join(missing_slots)
126
+ )
127
+
128
+ # Assert non-None for mypy strict checking after validation
129
+ assert self._model is not None
130
+ assert self._sampler is not None
131
+ assert self._execution_engine is not None
132
+ assert self._sink is not None
133
+
134
+ return SimulationEngine(
135
+ model=self._model,
136
+ sampler=self._sampler,
137
+ execution_engine=self._execution_engine,
138
+ sink=self._sink,
139
+ batch_size=self._batch_size,
140
+ )
stochops/exceptions.py ADDED
@@ -0,0 +1,5 @@
1
+ """Domain-specific exceptions for stochops."""
2
+
3
+
4
+ class ConvergenceError(RuntimeError):
5
+ """Raised when all adaptive fallback strategies and substepping attempts fail to converge."""
@@ -0,0 +1,5 @@
1
+ """Parallel execution engines for stochops."""
2
+
3
+ from stochops.execution.process_pool import ProcessPoolEngine
4
+
5
+ __all__ = ["ProcessPoolEngine"]
@@ -0,0 +1,151 @@
1
+ """Process pool execution engine implementation for stochops.
2
+
3
+ Provides process-isolated parallel evaluation using Python's multiprocessing spawn context
4
+ to prevent C++ static memory contamination and segfaults in OpenSeesPy.
5
+ """
6
+
7
+ import multiprocessing as mp
8
+ from collections.abc import Callable
9
+ from concurrent.futures import BrokenExecutor, ProcessPoolExecutor
10
+ from typing import Any
11
+
12
+ from stochops.exceptions import ConvergenceError
13
+ from stochops.protocols.model import ParametricModel
14
+
15
+ # Tokens used to conservatively classify failures thrown by user models as
16
+ # solver divergences even when the raised exception type is unknown to stochops.
17
+ _DIVERGENCE_TOKENS = ("convergence", "converge", "krylov", "substep", "step failed")
18
+ _MAX_RETRIES = 1
19
+
20
+
21
+ def _worker_entrypoint(
22
+ evaluator: ParametricModel | Callable[[dict[str, Any]], dict[str, Any]],
23
+ sample_id: int,
24
+ sample: dict[str, Any],
25
+ ) -> dict[str, Any]:
26
+ """Top-level worker execution wrapper for process boundary isolation.
27
+
28
+ Traps model execution exceptions (e.g. ConvergenceError, runtime errors) to ensure
29
+ individual realization failures do not crash the parallel worker pool.
30
+ """
31
+ record: dict[str, Any] = {
32
+ "sample_id": sample_id,
33
+ **sample,
34
+ }
35
+
36
+ try:
37
+ edps = evaluator(sample)
38
+ if not isinstance(edps, dict):
39
+ raise TypeError(f"Model evaluator must return a dictionary of EDPs, got {type(edps).__name__}")
40
+
41
+ return {
42
+ **record,
43
+ "status": "CONVERGED",
44
+ "error_type": None,
45
+ "error_message": None,
46
+ **edps,
47
+ }
48
+ except Exception as err:
49
+ error_class = type(err).__name__
50
+ status_flag = _classify_failure(err)
51
+ return {
52
+ **record,
53
+ "status": status_flag,
54
+ "error_type": error_class,
55
+ "error_message": str(err),
56
+ }
57
+
58
+
59
+ def _classify_failure(err: Exception) -> str:
60
+ """Classifies a trapped worker exception as ``DIVERGED`` or ``FAILED``.
61
+
62
+ Uses structural type checks (``isinstance``) against stochops' own
63
+ ``ConvergenceError`` in preference to fragile string matching on exception
64
+ class names, with a conservative message-token fallback for exceptions
65
+ raised by user models that do not derive from stochops exceptions.
66
+ """
67
+ if isinstance(err, ConvergenceError):
68
+ return "DIVERGED"
69
+
70
+ message = str(err).lower()
71
+ if any(token in message for token in _DIVERGENCE_TOKENS):
72
+ return "DIVERGED"
73
+
74
+ return "FAILED"
75
+
76
+
77
+ class ProcessPoolEngine:
78
+ """Parallel execution engine implementing the ExecutionEngine protocol.
79
+
80
+ Uses a spawned process pool executor to isolate each worker's memory space,
81
+ ensuring OpenSees C++ static variables do not leak across consecutive realizations.
82
+
83
+ The worker pool is created lazily on the first batch and reused across
84
+ subsequent batches, so OpenSeesPy is imported exactly once per worker
85
+ process. Call :meth:`close` to release the pool, or it is released
86
+ automatically when :class:`~stochops.builder.SimulationEngine.run` finishes.
87
+
88
+ ``num_workers`` defaults to the system CPU count, which may be aggressive
89
+ for memory-heavy finite-element models; consider passing a lower value.
90
+ """
91
+
92
+ def __init__(self, num_workers: int | None = None) -> None:
93
+ """Initializes the ProcessPoolEngine.
94
+
95
+ Args:
96
+ num_workers: Number of parallel worker processes. Defaults to system CPU count.
97
+ """
98
+ self.num_workers = num_workers or mp.cpu_count()
99
+ self._executor: ProcessPoolExecutor | None = None
100
+
101
+ def _get_executor(self) -> ProcessPoolExecutor:
102
+ """Returns the lazily-created persistent process pool."""
103
+ if self._executor is None:
104
+ # Force 'spawn' context to guarantee clean process isolation for C++ libraries
105
+ ctx = mp.get_context("spawn")
106
+ self._executor = ProcessPoolExecutor(max_workers=self.num_workers, mp_context=ctx)
107
+ return self._executor
108
+
109
+ def run_batch(
110
+ self,
111
+ evaluator: ParametricModel | Callable[[dict[str, Any]], dict[str, Any]],
112
+ samples: list[dict[str, Any]],
113
+ ) -> list[dict[str, Any]]:
114
+ """Dispatches a batch of parameter realizations across the worker pool.
115
+
116
+ Args:
117
+ evaluator: The model evaluation callback.
118
+ samples: List of parameter dictionaries to evaluate.
119
+
120
+ Returns:
121
+ List of result dictionaries containing execution metadata, input parameters,
122
+ and evaluated EDPs (or failure diagnostics).
123
+ """
124
+ if not samples:
125
+ return []
126
+
127
+ last_error: Exception | None = None
128
+ for attempt in range(_MAX_RETRIES + 1):
129
+ try:
130
+ executor = self._get_executor()
131
+ futures = [
132
+ executor.submit(_worker_entrypoint, evaluator, idx, sample)
133
+ for idx, sample in enumerate(samples)
134
+ ]
135
+ return [future.result() for future in futures]
136
+ except BrokenExecutor as err:
137
+ # Worker crashed (e.g. segfault). Recreate the pool and retry once.
138
+ last_error = err
139
+ if attempt < _MAX_RETRIES and self._executor is not None:
140
+ self._executor.shutdown(wait=False, cancel_futures=True)
141
+ self._executor = None
142
+ continue
143
+ break
144
+
145
+ raise RuntimeError("Process pool workers crashed repeatedly; batching aborted.") from last_error
146
+
147
+ def close(self) -> None:
148
+ """Shuts down the persistent worker pool, releasing all worker processes."""
149
+ if self._executor is not None:
150
+ self._executor.shutdown(wait=True)
151
+ self._executor = None
File without changes
stochops/parameters.py ADDED
@@ -0,0 +1,152 @@
1
+ """Parameter definitions and distribution transformations for stochops."""
2
+
3
+ import warnings
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ from scipy.stats import gumbel_r, lognorm, norm, uniform
12
+
13
+
14
+ # Distribution Enum
15
+ class Distribution(str, Enum):
16
+ NORMAL = "normal"
17
+ LOGNORMAL = "lognormal"
18
+ GUMBEL = "gumbel"
19
+ UNIFORM = "uniform"
20
+
21
+
22
+ # Discrete Transformation Registry
23
+ TransformFunc = Callable[[float, float, float], float]
24
+
25
+ def _normal_ppf(u: float, mean: float, std: float) -> float:
26
+ return float(norm.ppf(u, loc=mean, scale=std))
27
+
28
+ def _lognormal_ppf(u: float, mean: float, std: float) -> float:
29
+ shape = np.sqrt(np.log(1.0 + (std / mean) ** 2))
30
+ scale = mean / np.sqrt(1.0 + (std / mean) ** 2)
31
+ return float(lognorm.ppf(u, s=shape, scale=scale))
32
+
33
+ def _gumbel_ppf(u: float, mean: float, std: float) -> float:
34
+ beta = std * np.sqrt(6) / np.pi
35
+ mu = mean - 0.5772156649 * beta
36
+ return float(gumbel_r.ppf(u, loc=mu, scale=beta))
37
+
38
+ def _uniform_ppf(u: float, mean: float, std: float) -> float:
39
+ low = mean - np.sqrt(3) * std
40
+ high = mean + np.sqrt(3) * std
41
+ return float(uniform.ppf(u, loc=low, scale=high - low))
42
+
43
+ DIST_REGISTRY: dict[Distribution, TransformFunc] = {
44
+ Distribution.NORMAL: _normal_ppf,
45
+ Distribution.LOGNORMAL: _lognormal_ppf,
46
+ Distribution.GUMBEL: _gumbel_ppf,
47
+ Distribution.UNIFORM: _uniform_ppf,
48
+ }
49
+
50
+
51
+ # 3. Base Parameter Contract
52
+ class BaseParameter(ABC):
53
+ name: str
54
+
55
+ @abstractmethod
56
+ def transform_unit_draw(self, u: float) -> Any:
57
+ ...
58
+
59
+
60
+ # 4. Lean Continuous Parameter Class
61
+ @dataclass(frozen=True)
62
+ class ContinuousParam(BaseParameter):
63
+ name: str
64
+ dist: Distribution
65
+ mean: float
66
+ std: float
67
+ max_value: float | None = None
68
+ min_value: float | None = None
69
+
70
+ def transform_unit_draw(self, u: float) -> float:
71
+ u_clamped = float(np.clip(u, 1e-9, 1.0 - 1e-9))
72
+
73
+ # O(1) Registry Dispatch
74
+ transform_fn = DIST_REGISTRY[self.dist] # Guaranteed valid by __post_init__
75
+ val = transform_fn(u_clamped, self.mean, self.std)
76
+
77
+ if self.min_value is not None:
78
+ val = max(self.min_value, val)
79
+ if self.max_value is not None:
80
+ val = min(self.max_value, val)
81
+
82
+ return val
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class DiscreteParam(BaseParameter):
87
+ name: str
88
+ choices: list[Any]
89
+ probabilities: list[float] | None = None
90
+
91
+ def __post_init__(self) -> None:
92
+ if not self.choices:
93
+ raise ValueError("DiscreteParam choices cannot be empty.")
94
+
95
+ if self.probabilities is not None:
96
+ if len(self.probabilities) != len(self.choices):
97
+ raise ValueError(
98
+ f"DiscreteParam probabilities length {len(self.probabilities)} "
99
+ f"does not match choices length {len(self.choices)}."
100
+ )
101
+ probs = np.asarray(self.probabilities, dtype=float)
102
+ if not np.all(np.isfinite(probs)) or np.any(probs < 0.0):
103
+ raise ValueError(
104
+ "DiscreteParam probabilities must be finite and non-negative."
105
+ )
106
+ if probs.sum() <= 0.0:
107
+ raise ValueError(
108
+ "DiscreteParam probabilities must have a positive sum."
109
+ )
110
+ # Normalize so the probabilities sum to 1.0
111
+ normalized = (probs / probs.sum()).tolist()
112
+ object.__setattr__(self, "probabilities", normalized)
113
+
114
+ def transform_unit_draw(self, u: float) -> Any:
115
+ u_clamped = float(np.clip(u, 0.0, 1.0 - 1e-9))
116
+ if self.probabilities is None:
117
+ idx = min(int(np.floor(u_clamped * len(self.choices))), len(self.choices) - 1)
118
+ return self.choices[idx]
119
+
120
+ cdf = np.cumsum(self.probabilities)
121
+ idx = min(int(np.searchsorted(cdf, u_clamped, side="right")), len(self.choices) - 1)
122
+ return self.choices[idx]
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class GroundMotionSetParam(BaseParameter):
127
+ name: str
128
+ records: list[Any]
129
+
130
+ def transform_unit_draw(self, u: float) -> Any:
131
+ u_clamped = float(np.clip(u, 0.0, 1.0 - 1e-9))
132
+ idx = min(int(np.floor(u_clamped * len(self.records))), len(self.records) - 1)
133
+ return self.records[idx]
134
+
135
+ @dataclass(frozen=True)
136
+ class Parameter:
137
+ """Legacy simple parameter dataclass.
138
+
139
+ Deprecated in favour of :class:`ContinuousParam`. Kept for backwards
140
+ compatibility; constructing one emits a ``DeprecationWarning``.
141
+ """
142
+ name: str
143
+ dist: Distribution
144
+ mean: float
145
+ std: float
146
+
147
+ def __post_init__(self) -> None:
148
+ warnings.warn(
149
+ "Parameter is deprecated; use ContinuousParam instead.",
150
+ DeprecationWarning,
151
+ stacklevel=2,
152
+ )
@@ -0,0 +1,11 @@
1
+ from stochops.protocols.engine import ExecutionEngine
2
+ from stochops.protocols.model import ParametricModel
3
+ from stochops.protocols.sampler import Sampler
4
+ from stochops.protocols.sink import ResultSink
5
+
6
+ __all__ = [
7
+ "ParametricModel",
8
+ "Sampler",
9
+ "ExecutionEngine",
10
+ "ResultSink",
11
+ ]
@@ -0,0 +1,35 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, Protocol, runtime_checkable
3
+
4
+ from stochops.protocols.model import ParametricModel
5
+
6
+
7
+ @runtime_checkable
8
+ class ExecutionEngine(Protocol):
9
+ """Protocol for worker dispatch and isolation strategies."""
10
+
11
+ def run_batch(
12
+ self,
13
+ evaluator: ParametricModel | Callable[[dict[str, Any]], dict[str, Any]],
14
+ samples: list[dict[str, Any]],
15
+ ) -> list[dict[str, Any]]:
16
+ """Dispatches a batch of samples across worker processes.
17
+
18
+ Args:
19
+ evaluator: The model evaluation callable.
20
+ samples: List of parameter dictionaries to evaluate.
21
+
22
+ Returns:
23
+ List of result dictionaries containing execution metadata, parameters,
24
+ and evaluated EDPs.
25
+ """
26
+ ...
27
+
28
+ def close(self) -> None:
29
+ """Releases any long-lived worker resources held by the engine.
30
+
31
+ Called once by the simulation engine after a campaign finishes.
32
+ Implementations must be safe to call multiple times and must be a
33
+ no-op if no resources were ever allocated.
34
+ """
35
+ ...
@@ -0,0 +1,19 @@
1
+ from typing import Any, Protocol, runtime_checkable
2
+
3
+
4
+ @runtime_checkable
5
+ class ParametricModel(Protocol):
6
+ """Protocol for user-defined structural evaluation callbacks."""
7
+
8
+ def __call__(self, parameters: dict[str, Any]) -> dict[str, Any]:
9
+ """Executes a single structural realization given input parameters.
10
+
11
+ Args:
12
+ parameters: Dictionary mapping parameter names to sampled physical values
13
+ (e.g., {"fc": 30e6, "fy": 420e6}).
14
+
15
+ Returns:
16
+ Dictionary of extracted Engineering Demand Parameters (EDPs)
17
+ (e.g., {"max_drift": 0.012, "base_shear_kN": 150.5}).
18
+ """
19
+ ...