@zigrivers/scaffold 3.13.0 → 3.15.0
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.
- package/README.md +32 -10
- package/content/knowledge/research/research-architecture.md +385 -0
- package/content/knowledge/research/research-conventions.md +248 -0
- package/content/knowledge/research/research-dev-environment.md +303 -0
- package/content/knowledge/research/research-experiment-loop.md +429 -0
- package/content/knowledge/research/research-experiment-tracking.md +336 -0
- package/content/knowledge/research/research-ml-architecture-search.md +383 -0
- package/content/knowledge/research/research-ml-evaluation.md +407 -0
- package/content/knowledge/research/research-ml-experiment-tracking.md +466 -0
- package/content/knowledge/research/research-ml-training-patterns.md +413 -0
- package/content/knowledge/research/research-observability.md +395 -0
- package/content/knowledge/research/research-overfitting-prevention.md +306 -0
- package/content/knowledge/research/research-project-structure.md +264 -0
- package/content/knowledge/research/research-quant-backtesting.md +326 -0
- package/content/knowledge/research/research-quant-market-data.md +366 -0
- package/content/knowledge/research/research-quant-metrics.md +335 -0
- package/content/knowledge/research/research-quant-requirements.md +223 -0
- package/content/knowledge/research/research-quant-risk.md +469 -0
- package/content/knowledge/research/research-quant-strategy-patterns.md +412 -0
- package/content/knowledge/research/research-requirements.md +201 -0
- package/content/knowledge/research/research-security.md +374 -0
- package/content/knowledge/research/research-sim-compute-management.md +538 -0
- package/content/knowledge/research/research-sim-engine-patterns.md +448 -0
- package/content/knowledge/research/research-sim-parameter-spaces.md +425 -0
- package/content/knowledge/research/research-sim-validation.md +456 -0
- package/content/knowledge/research/research-testing.md +334 -0
- package/content/methodology/research-ml-research.yml +23 -0
- package/content/methodology/research-overlay.yml +65 -0
- package/content/methodology/research-quant-finance.yml +29 -0
- package/content/methodology/research-simulation.yml +23 -0
- package/dist/cli/commands/adopt.d.ts.map +1 -1
- package/dist/cli/commands/adopt.js +30 -8
- package/dist/cli/commands/adopt.js.map +1 -1
- package/dist/cli/commands/adopt.serialization.test.js +49 -0
- package/dist/cli/commands/adopt.serialization.test.js.map +1 -1
- package/dist/cli/commands/adopt.test.js +8 -0
- package/dist/cli/commands/adopt.test.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +191 -180
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/complete.d.ts.map +1 -1
- package/dist/cli/commands/complete.js +16 -12
- package/dist/cli/commands/complete.js.map +1 -1
- package/dist/cli/commands/complete.test.js +14 -5
- package/dist/cli/commands/complete.test.js.map +1 -1
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +75 -51
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +33 -27
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/reset.d.ts.map +1 -1
- package/dist/cli/commands/reset.js +44 -40
- package/dist/cli/commands/reset.js.map +1 -1
- package/dist/cli/commands/reset.test.js +42 -20
- package/dist/cli/commands/reset.test.js.map +1 -1
- package/dist/cli/commands/rework.d.ts.map +1 -1
- package/dist/cli/commands/rework.js +16 -12
- package/dist/cli/commands/rework.js.map +1 -1
- package/dist/cli/commands/rework.test.js +12 -3
- package/dist/cli/commands/rework.test.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +318 -298
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/run.test.js +92 -120
- package/dist/cli/commands/run.test.js.map +1 -1
- package/dist/cli/commands/skip.d.ts.map +1 -1
- package/dist/cli/commands/skip.js +19 -15
- package/dist/cli/commands/skip.js.map +1 -1
- package/dist/cli/commands/skip.test.js +22 -11
- package/dist/cli/commands/skip.test.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +3 -1
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/commands/update.test.js +8 -4
- package/dist/cli/commands/update.test.js.map +1 -1
- package/dist/cli/commands/version.d.ts.map +1 -1
- package/dist/cli/commands/version.js +3 -1
- package/dist/cli/commands/version.js.map +1 -1
- package/dist/cli/commands/version.test.js +9 -5
- package/dist/cli/commands/version.test.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init-flag-families.d.ts +6 -1
- package/dist/cli/init-flag-families.d.ts.map +1 -1
- package/dist/cli/init-flag-families.js +32 -1
- package/dist/cli/init-flag-families.js.map +1 -1
- package/dist/cli/init-flag-families.test.js +47 -0
- package/dist/cli/init-flag-families.test.js.map +1 -1
- package/dist/cli/output/interactive.d.ts +1 -0
- package/dist/cli/output/interactive.d.ts.map +1 -1
- package/dist/cli/output/interactive.js +5 -0
- package/dist/cli/output/interactive.js.map +1 -1
- package/dist/cli/shutdown.d.ts +51 -0
- package/dist/cli/shutdown.d.ts.map +1 -0
- package/dist/cli/shutdown.js +199 -0
- package/dist/cli/shutdown.js.map +1 -0
- package/dist/cli/shutdown.test.d.ts +2 -0
- package/dist/cli/shutdown.test.d.ts.map +1 -0
- package/dist/cli/shutdown.test.js +316 -0
- package/dist/cli/shutdown.test.js.map +1 -0
- package/dist/config/schema.d.ts +272 -16
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +25 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +103 -3
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.d.ts +12 -0
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
- package/dist/core/assembly/overlay-loader.js +30 -0
- package/dist/core/assembly/overlay-loader.js.map +1 -1
- package/dist/core/assembly/overlay-loader.test.js +66 -1
- package/dist/core/assembly/overlay-loader.test.js.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.js +48 -19
- package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.test.js +80 -0
- package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
- package/dist/e2e/init.test.js +5 -4
- package/dist/e2e/init.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.js +119 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -1
- package/dist/project/adopt.d.ts.map +1 -1
- package/dist/project/adopt.js +3 -1
- package/dist/project/adopt.js.map +1 -1
- package/dist/project/detectors/disambiguate.js +1 -1
- package/dist/project/detectors/disambiguate.js.map +1 -1
- package/dist/project/detectors/index.d.ts.map +1 -1
- package/dist/project/detectors/index.js +2 -1
- package/dist/project/detectors/index.js.map +1 -1
- package/dist/project/detectors/ml.d.ts.map +1 -1
- package/dist/project/detectors/ml.js +2 -6
- package/dist/project/detectors/ml.js.map +1 -1
- package/dist/project/detectors/research.d.ts +4 -0
- package/dist/project/detectors/research.d.ts.map +1 -0
- package/dist/project/detectors/research.js +141 -0
- package/dist/project/detectors/research.js.map +1 -0
- package/dist/project/detectors/research.test.d.ts +2 -0
- package/dist/project/detectors/research.test.d.ts.map +1 -0
- package/dist/project/detectors/research.test.js +235 -0
- package/dist/project/detectors/research.test.js.map +1 -0
- package/dist/project/detectors/shared-signals.d.ts +3 -0
- package/dist/project/detectors/shared-signals.d.ts.map +1 -0
- package/dist/project/detectors/shared-signals.js +9 -0
- package/dist/project/detectors/shared-signals.js.map +1 -0
- package/dist/project/detectors/types.d.ts +6 -2
- package/dist/project/detectors/types.d.ts.map +1 -1
- package/dist/project/detectors/types.js.map +1 -1
- package/dist/state/lock-manager.d.ts +1 -0
- package/dist/state/lock-manager.d.ts.map +1 -1
- package/dist/state/lock-manager.js +1 -1
- package/dist/state/lock-manager.js.map +1 -1
- package/dist/types/config.d.ts +7 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/wizard/copy/core.d.ts.map +1 -1
- package/dist/wizard/copy/core.js +4 -0
- package/dist/wizard/copy/core.js.map +1 -1
- package/dist/wizard/copy/index.d.ts.map +1 -1
- package/dist/wizard/copy/index.js +2 -0
- package/dist/wizard/copy/index.js.map +1 -1
- package/dist/wizard/copy/research.d.ts +3 -0
- package/dist/wizard/copy/research.d.ts.map +1 -0
- package/dist/wizard/copy/research.js +27 -0
- package/dist/wizard/copy/research.js.map +1 -0
- package/dist/wizard/copy/types.d.ts +5 -1
- package/dist/wizard/copy/types.d.ts.map +1 -1
- package/dist/wizard/flags.d.ts +7 -1
- package/dist/wizard/flags.d.ts.map +1 -1
- package/dist/wizard/questions.d.ts +4 -2
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +27 -1
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +51 -0
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +3 -2
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +3 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: research-sim-parameter-spaces
|
|
3
|
+
description: Parameter space definition for simulations including continuous, discrete, and categorical dimensions, Latin Hypercube Sampling, Sobol sequences, interaction effect detection, and sensitivity analysis methods
|
|
4
|
+
topics: [research, simulation, parameter-space, latin-hypercube, sobol-sequences, sensitivity-analysis, morris-method, sobol-indices, dimensionality-reduction, design-of-experiments]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Simulation parameter spaces define the landscape an optimizer must navigate. Unlike ML hyperparameter tuning where evaluations take seconds, simulation evaluations can take hours or days, making efficient space exploration critical. The challenge is threefold: define the space correctly (capturing interactions and constraints between parameters), sample it efficiently (maximizing information per simulation run), and analyze which dimensions actually matter (sensitivity analysis) to reduce the effective dimensionality before expensive optimization.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Define parameter spaces with explicit types (continuous, discrete, categorical), bounds, constraints, and interaction groups. Use space-filling designs (Latin Hypercube Sampling, Sobol sequences) for initial exploration rather than grid or random sampling -- they provide better coverage with fewer evaluations. Apply screening methods (Morris elementary effects) to identify active parameters before full optimization. Compute Sobol sensitivity indices to quantify main effects vs interaction effects. Reduce dimensionality by fixing insensitive parameters at nominal values, enabling tractable optimization in the active subspace.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### Parameter Space Definition
|
|
16
|
+
|
|
17
|
+
Define spaces with rich type information that optimizers and samplers can exploit:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# src/simulation/parameter_space.py
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Any
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
class ParamType(Enum):
|
|
27
|
+
CONTINUOUS = "continuous"
|
|
28
|
+
DISCRETE = "discrete"
|
|
29
|
+
CATEGORICAL = "categorical"
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Parameter:
|
|
33
|
+
"""Single dimension in the parameter space."""
|
|
34
|
+
name: str
|
|
35
|
+
param_type: ParamType
|
|
36
|
+
# Continuous/discrete bounds
|
|
37
|
+
low: float | None = None
|
|
38
|
+
high: float | None = None
|
|
39
|
+
# Discrete step size
|
|
40
|
+
step: float | None = None
|
|
41
|
+
# Categorical choices
|
|
42
|
+
choices: list[Any] | None = None
|
|
43
|
+
# Log-scale for continuous params spanning orders of magnitude
|
|
44
|
+
log_scale: bool = False
|
|
45
|
+
# Default/nominal value for sensitivity analysis
|
|
46
|
+
nominal: Any = None
|
|
47
|
+
# Group tag for interaction analysis
|
|
48
|
+
group: str | None = None
|
|
49
|
+
|
|
50
|
+
def sample_uniform(self, rng: np.random.Generator) -> Any:
|
|
51
|
+
"""Sample a single value uniformly from this dimension."""
|
|
52
|
+
if self.param_type == ParamType.CONTINUOUS:
|
|
53
|
+
if self.log_scale:
|
|
54
|
+
log_val = rng.uniform(np.log(self.low), np.log(self.high))
|
|
55
|
+
return float(np.exp(log_val))
|
|
56
|
+
return float(rng.uniform(self.low, self.high))
|
|
57
|
+
elif self.param_type == ParamType.DISCRETE:
|
|
58
|
+
steps = int((self.high - self.low) / self.step) + 1
|
|
59
|
+
return float(self.low + rng.integers(steps) * self.step)
|
|
60
|
+
else:
|
|
61
|
+
return self.choices[rng.integers(len(self.choices))]
|
|
62
|
+
|
|
63
|
+
def normalize(self, value: Any) -> float:
|
|
64
|
+
"""Map value to [0, 1] for space-filling designs."""
|
|
65
|
+
if self.param_type == ParamType.CATEGORICAL:
|
|
66
|
+
return self.choices.index(value) / max(len(self.choices) - 1, 1)
|
|
67
|
+
if self.log_scale:
|
|
68
|
+
return (np.log(value) - np.log(self.low)) / (np.log(self.high) - np.log(self.low))
|
|
69
|
+
return (value - self.low) / (self.high - self.low)
|
|
70
|
+
|
|
71
|
+
def denormalize(self, unit_value: float) -> Any:
|
|
72
|
+
"""Map [0, 1] back to parameter value."""
|
|
73
|
+
if self.param_type == ParamType.CATEGORICAL:
|
|
74
|
+
idx = int(round(unit_value * (len(self.choices) - 1)))
|
|
75
|
+
return self.choices[min(idx, len(self.choices) - 1)]
|
|
76
|
+
if self.log_scale:
|
|
77
|
+
log_val = np.log(self.low) + unit_value * (np.log(self.high) - np.log(self.low))
|
|
78
|
+
return float(np.exp(log_val))
|
|
79
|
+
raw = self.low + unit_value * (self.high - self.low)
|
|
80
|
+
if self.param_type == ParamType.DISCRETE:
|
|
81
|
+
return float(round((raw - self.low) / self.step) * self.step + self.low)
|
|
82
|
+
return float(raw)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class ParameterSpace:
|
|
87
|
+
"""Full parameter space with constraints and interaction structure."""
|
|
88
|
+
parameters: list[Parameter]
|
|
89
|
+
constraints: list[Any] = field(default_factory=list) # Callable[[dict], bool]
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def dimension(self) -> int:
|
|
93
|
+
return len(self.parameters)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def continuous_dims(self) -> list[Parameter]:
|
|
97
|
+
return [p for p in self.parameters if p.param_type == ParamType.CONTINUOUS]
|
|
98
|
+
|
|
99
|
+
def sample_valid(self, rng: np.random.Generator, max_attempts: int = 100) -> dict[str, Any]:
|
|
100
|
+
"""Sample a valid point satisfying all constraints."""
|
|
101
|
+
for _ in range(max_attempts):
|
|
102
|
+
point = {p.name: p.sample_uniform(rng) for p in self.parameters}
|
|
103
|
+
if all(c(point) for c in self.constraints):
|
|
104
|
+
return point
|
|
105
|
+
raise RuntimeError(f"Failed to sample valid point in {max_attempts} attempts")
|
|
106
|
+
|
|
107
|
+
def groups(self) -> dict[str, list[Parameter]]:
|
|
108
|
+
"""Group parameters by interaction group."""
|
|
109
|
+
groups: dict[str, list[Parameter]] = {}
|
|
110
|
+
for p in self.parameters:
|
|
111
|
+
key = p.group or "ungrouped"
|
|
112
|
+
groups.setdefault(key, []).append(p)
|
|
113
|
+
return groups
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Latin Hypercube Sampling
|
|
117
|
+
|
|
118
|
+
LHS ensures each parameter dimension is evenly covered, avoiding gaps and clusters that random sampling produces:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# src/simulation/sampling/lhs.py
|
|
122
|
+
import numpy as np
|
|
123
|
+
from src.simulation.parameter_space import ParameterSpace
|
|
124
|
+
|
|
125
|
+
def latin_hypercube_sample(
|
|
126
|
+
space: ParameterSpace,
|
|
127
|
+
n_samples: int,
|
|
128
|
+
seed: int = 42,
|
|
129
|
+
criterion: str = "maximin",
|
|
130
|
+
) -> list[dict[str, any]]:
|
|
131
|
+
"""Generate LHS design with maximin distance optimization."""
|
|
132
|
+
rng = np.random.default_rng(seed)
|
|
133
|
+
d = space.dimension
|
|
134
|
+
|
|
135
|
+
# Generate base LHS in unit hypercube
|
|
136
|
+
unit_samples = _generate_lhs(n_samples, d, rng)
|
|
137
|
+
|
|
138
|
+
# Optimize placement using maximin criterion
|
|
139
|
+
if criterion == "maximin":
|
|
140
|
+
unit_samples = _optimize_maximin(unit_samples, rng, iterations=1000)
|
|
141
|
+
|
|
142
|
+
# Map from unit cube to parameter space
|
|
143
|
+
samples = []
|
|
144
|
+
for row in unit_samples:
|
|
145
|
+
point = {}
|
|
146
|
+
for i, param in enumerate(space.parameters):
|
|
147
|
+
point[param.name] = param.denormalize(row[i])
|
|
148
|
+
# Check constraints, resample if violated
|
|
149
|
+
if all(c(point) for c in space.constraints):
|
|
150
|
+
samples.append(point)
|
|
151
|
+
|
|
152
|
+
return samples
|
|
153
|
+
|
|
154
|
+
def _generate_lhs(n: int, d: int, rng: np.random.Generator) -> np.ndarray:
|
|
155
|
+
"""Generate basic LHS design: one sample per stratum per dimension."""
|
|
156
|
+
result = np.zeros((n, d))
|
|
157
|
+
for j in range(d):
|
|
158
|
+
perm = rng.permutation(n)
|
|
159
|
+
for i in range(n):
|
|
160
|
+
result[i, j] = (perm[i] + rng.uniform()) / n
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
def _optimize_maximin(
|
|
164
|
+
samples: np.ndarray, rng: np.random.Generator, iterations: int = 1000
|
|
165
|
+
) -> np.ndarray:
|
|
166
|
+
"""Improve LHS by maximizing minimum distance between points."""
|
|
167
|
+
best = samples.copy()
|
|
168
|
+
best_min_dist = _min_distance(best)
|
|
169
|
+
|
|
170
|
+
for _ in range(iterations):
|
|
171
|
+
candidate = best.copy()
|
|
172
|
+
# Swap two elements in a random column
|
|
173
|
+
col = rng.integers(candidate.shape[1])
|
|
174
|
+
i, j = rng.choice(candidate.shape[0], size=2, replace=False)
|
|
175
|
+
candidate[i, col], candidate[j, col] = candidate[j, col], candidate[i, col]
|
|
176
|
+
|
|
177
|
+
min_dist = _min_distance(candidate)
|
|
178
|
+
if min_dist > best_min_dist:
|
|
179
|
+
best = candidate
|
|
180
|
+
best_min_dist = min_dist
|
|
181
|
+
|
|
182
|
+
return best
|
|
183
|
+
|
|
184
|
+
def _min_distance(samples: np.ndarray) -> float:
|
|
185
|
+
"""Compute minimum pairwise Euclidean distance."""
|
|
186
|
+
from scipy.spatial.distance import pdist
|
|
187
|
+
return pdist(samples).min()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Sobol Sequences
|
|
191
|
+
|
|
192
|
+
Sobol sequences provide quasi-random low-discrepancy points with better uniformity guarantees than LHS for high dimensions:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
# src/simulation/sampling/sobol.py
|
|
196
|
+
import numpy as np
|
|
197
|
+
from scipy.stats.qmc import Sobol
|
|
198
|
+
from src.simulation.parameter_space import ParameterSpace
|
|
199
|
+
|
|
200
|
+
def sobol_sample(
|
|
201
|
+
space: ParameterSpace,
|
|
202
|
+
n_samples: int,
|
|
203
|
+
seed: int = 42,
|
|
204
|
+
skip: int = 0,
|
|
205
|
+
) -> list[dict[str, any]]:
|
|
206
|
+
"""Generate Sobol quasi-random sequence mapped to parameter space."""
|
|
207
|
+
d = space.dimension
|
|
208
|
+
# Sobol requires n = 2^m samples for optimal properties
|
|
209
|
+
m = int(np.ceil(np.log2(n_samples)))
|
|
210
|
+
n_power_of_2 = 2**m
|
|
211
|
+
|
|
212
|
+
sampler = Sobol(d, scramble=True, seed=seed)
|
|
213
|
+
if skip > 0:
|
|
214
|
+
sampler.fast_forward(skip)
|
|
215
|
+
unit_samples = sampler.random(n_power_of_2)
|
|
216
|
+
|
|
217
|
+
# Map to parameter space and filter by constraints
|
|
218
|
+
samples = []
|
|
219
|
+
for row in unit_samples[:n_samples]:
|
|
220
|
+
point = {
|
|
221
|
+
param.name: param.denormalize(row[i])
|
|
222
|
+
for i, param in enumerate(space.parameters)
|
|
223
|
+
}
|
|
224
|
+
if all(c(point) for c in space.constraints):
|
|
225
|
+
samples.append(point)
|
|
226
|
+
|
|
227
|
+
return samples
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Morris Method (Elementary Effects)
|
|
231
|
+
|
|
232
|
+
Morris method is a screening technique that identifies which parameters are active using only O(d) evaluations per trajectory:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
# src/simulation/sensitivity/morris.py
|
|
236
|
+
import numpy as np
|
|
237
|
+
from typing import Callable
|
|
238
|
+
from src.simulation.parameter_space import ParameterSpace
|
|
239
|
+
|
|
240
|
+
def morris_screening(
|
|
241
|
+
space: ParameterSpace,
|
|
242
|
+
evaluate_fn: Callable[[dict], float],
|
|
243
|
+
num_trajectories: int = 10,
|
|
244
|
+
num_levels: int = 4,
|
|
245
|
+
seed: int = 42,
|
|
246
|
+
) -> dict[str, dict[str, float]]:
|
|
247
|
+
"""Compute Morris elementary effects for parameter screening.
|
|
248
|
+
|
|
249
|
+
Returns dict mapping param name -> {mu_star, sigma} where:
|
|
250
|
+
- mu_star: mean absolute elementary effect (importance)
|
|
251
|
+
- sigma: std of effects (non-linearity / interaction indicator)
|
|
252
|
+
"""
|
|
253
|
+
rng = np.random.default_rng(seed)
|
|
254
|
+
d = space.dimension
|
|
255
|
+
delta = num_levels / (2 * (num_levels - 1))
|
|
256
|
+
|
|
257
|
+
effects: dict[str, list[float]] = {p.name: [] for p in space.parameters}
|
|
258
|
+
|
|
259
|
+
for _ in range(num_trajectories):
|
|
260
|
+
# Generate trajectory: d+1 points where each step perturbs one parameter
|
|
261
|
+
trajectory = _generate_trajectory(d, num_levels, delta, rng)
|
|
262
|
+
|
|
263
|
+
# Evaluate all points in trajectory
|
|
264
|
+
values = []
|
|
265
|
+
for unit_point in trajectory:
|
|
266
|
+
point = {
|
|
267
|
+
param.name: param.denormalize(unit_point[i])
|
|
268
|
+
for i, param in enumerate(space.parameters)
|
|
269
|
+
}
|
|
270
|
+
values.append(evaluate_fn(point))
|
|
271
|
+
|
|
272
|
+
# Compute elementary effects
|
|
273
|
+
for step_idx in range(d):
|
|
274
|
+
effect = (values[step_idx + 1] - values[step_idx]) / delta
|
|
275
|
+
effects[space.parameters[step_idx].name].append(effect)
|
|
276
|
+
|
|
277
|
+
# Compute summary statistics
|
|
278
|
+
results = {}
|
|
279
|
+
for name, efs in effects.items():
|
|
280
|
+
efs_arr = np.array(efs)
|
|
281
|
+
results[name] = {
|
|
282
|
+
"mu_star": float(np.mean(np.abs(efs_arr))),
|
|
283
|
+
"sigma": float(np.std(efs_arr)),
|
|
284
|
+
"mu": float(np.mean(efs_arr)),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return results
|
|
288
|
+
|
|
289
|
+
def _generate_trajectory(
|
|
290
|
+
d: int, num_levels: int, delta: float, rng: np.random.Generator
|
|
291
|
+
) -> np.ndarray:
|
|
292
|
+
"""Generate one Morris trajectory (d+1 points)."""
|
|
293
|
+
# Start from random base point on the grid
|
|
294
|
+
grid_values = np.linspace(0, 1, num_levels)
|
|
295
|
+
base = rng.choice(grid_values, size=d)
|
|
296
|
+
|
|
297
|
+
trajectory = [base.copy()]
|
|
298
|
+
order = rng.permutation(d)
|
|
299
|
+
|
|
300
|
+
for dim in order:
|
|
301
|
+
new_point = trajectory[-1].copy()
|
|
302
|
+
direction = rng.choice([-1, 1])
|
|
303
|
+
new_point[dim] = np.clip(new_point[dim] + direction * delta, 0, 1)
|
|
304
|
+
trajectory.append(new_point)
|
|
305
|
+
|
|
306
|
+
return np.array(trajectory)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Sobol Sensitivity Indices
|
|
310
|
+
|
|
311
|
+
Sobol indices decompose output variance into contributions from individual parameters and their interactions:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
# src/simulation/sensitivity/sobol_indices.py
|
|
315
|
+
import numpy as np
|
|
316
|
+
from typing import Callable
|
|
317
|
+
from src.simulation.parameter_space import ParameterSpace
|
|
318
|
+
|
|
319
|
+
def compute_sobol_indices(
|
|
320
|
+
space: ParameterSpace,
|
|
321
|
+
evaluate_fn: Callable[[dict], float],
|
|
322
|
+
n_samples: int = 1024,
|
|
323
|
+
seed: int = 42,
|
|
324
|
+
) -> dict[str, dict[str, float]]:
|
|
325
|
+
"""Compute first-order and total Sobol sensitivity indices.
|
|
326
|
+
|
|
327
|
+
Returns dict mapping param name -> {S1, ST} where:
|
|
328
|
+
- S1: first-order index (main effect of this parameter alone)
|
|
329
|
+
- ST: total-order index (including all interactions)
|
|
330
|
+
- ST - S1: interaction contribution
|
|
331
|
+
"""
|
|
332
|
+
from scipy.stats.qmc import Sobol as SobolSampler
|
|
333
|
+
|
|
334
|
+
d = space.dimension
|
|
335
|
+
sampler = SobolSampler(2 * d, scramble=True, seed=seed)
|
|
336
|
+
raw = sampler.random(n_samples)
|
|
337
|
+
|
|
338
|
+
# Split into two independent matrices A and B
|
|
339
|
+
A = raw[:, :d]
|
|
340
|
+
B = raw[:, d:]
|
|
341
|
+
|
|
342
|
+
# Evaluate base matrices
|
|
343
|
+
y_A = np.array([_eval_unit(space, evaluate_fn, A[i]) for i in range(n_samples)])
|
|
344
|
+
y_B = np.array([_eval_unit(space, evaluate_fn, B[i]) for i in range(n_samples)])
|
|
345
|
+
|
|
346
|
+
var_total = np.var(np.concatenate([y_A, y_B]))
|
|
347
|
+
results = {}
|
|
348
|
+
|
|
349
|
+
for j, param in enumerate(space.parameters):
|
|
350
|
+
# AB_j: A with column j replaced by B's column j
|
|
351
|
+
AB_j = A.copy()
|
|
352
|
+
AB_j[:, j] = B[:, j]
|
|
353
|
+
y_AB_j = np.array([_eval_unit(space, evaluate_fn, AB_j[i]) for i in range(n_samples)])
|
|
354
|
+
|
|
355
|
+
# First-order: S1_j = V[E[Y|X_j]] / V[Y]
|
|
356
|
+
s1 = float(np.mean(y_B * (y_AB_j - y_A)) / var_total) if var_total > 0 else 0.0
|
|
357
|
+
|
|
358
|
+
# Total-order: ST_j = E[V[Y|X_~j]] / V[Y]
|
|
359
|
+
st = float(0.5 * np.mean((y_A - y_AB_j) ** 2) / var_total) if var_total > 0 else 0.0
|
|
360
|
+
|
|
361
|
+
results[param.name] = {"S1": max(0, s1), "ST": max(0, st)}
|
|
362
|
+
|
|
363
|
+
return results
|
|
364
|
+
|
|
365
|
+
def _eval_unit(space: ParameterSpace, fn: Callable, unit_point: np.ndarray) -> float:
|
|
366
|
+
"""Evaluate function at a unit-cube point mapped to parameter space."""
|
|
367
|
+
point = {
|
|
368
|
+
param.name: param.denormalize(unit_point[i])
|
|
369
|
+
for i, param in enumerate(space.parameters)
|
|
370
|
+
}
|
|
371
|
+
return fn(point)
|
|
372
|
+
|
|
373
|
+
def identify_active_subspace(
|
|
374
|
+
sobol_results: dict[str, dict[str, float]],
|
|
375
|
+
threshold: float = 0.05,
|
|
376
|
+
) -> tuple[list[str], list[str]]:
|
|
377
|
+
"""Split parameters into active (ST >= threshold) and inactive."""
|
|
378
|
+
active = [name for name, idx in sobol_results.items() if idx["ST"] >= threshold]
|
|
379
|
+
inactive = [name for name, idx in sobol_results.items() if idx["ST"] < threshold]
|
|
380
|
+
return active, inactive
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Dimensionality Reduction
|
|
384
|
+
|
|
385
|
+
After sensitivity analysis, fix inactive parameters and optimize in the reduced space:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
# src/simulation/parameter_space.py (continued)
|
|
389
|
+
|
|
390
|
+
def reduce_space(
|
|
391
|
+
space: ParameterSpace,
|
|
392
|
+
active_params: list[str],
|
|
393
|
+
) -> ParameterSpace:
|
|
394
|
+
"""Create reduced space containing only active parameters."""
|
|
395
|
+
active_set = set(active_params)
|
|
396
|
+
reduced_params = [p for p in space.parameters if p.name in active_set]
|
|
397
|
+
|
|
398
|
+
# Constraints that reference only active parameters still apply
|
|
399
|
+
reduced_constraints = []
|
|
400
|
+
for constraint in space.constraints:
|
|
401
|
+
# Keep constraint if it only involves active parameters
|
|
402
|
+
# (requires constraint introspection or explicit annotation)
|
|
403
|
+
reduced_constraints.append(constraint)
|
|
404
|
+
|
|
405
|
+
return ParameterSpace(parameters=reduced_params, constraints=reduced_constraints)
|
|
406
|
+
|
|
407
|
+
def fix_inactive_parameters(
|
|
408
|
+
space: ParameterSpace,
|
|
409
|
+
inactive_params: list[str],
|
|
410
|
+
) -> dict[str, any]:
|
|
411
|
+
"""Return fixed values for inactive parameters (use nominals)."""
|
|
412
|
+
inactive_set = set(inactive_params)
|
|
413
|
+
fixed = {}
|
|
414
|
+
for param in space.parameters:
|
|
415
|
+
if param.name in inactive_set:
|
|
416
|
+
if param.nominal is not None:
|
|
417
|
+
fixed[param.name] = param.nominal
|
|
418
|
+
elif param.param_type == ParamType.CONTINUOUS:
|
|
419
|
+
fixed[param.name] = (param.low + param.high) / 2
|
|
420
|
+
elif param.param_type == ParamType.DISCRETE:
|
|
421
|
+
fixed[param.name] = param.low + ((param.high - param.low) // 2)
|
|
422
|
+
else:
|
|
423
|
+
fixed[param.name] = param.choices[0]
|
|
424
|
+
return fixed
|
|
425
|
+
```
|