@zigrivers/scaffold 3.14.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.
Files changed (117) hide show
  1. package/README.md +31 -9
  2. package/content/knowledge/research/research-architecture.md +385 -0
  3. package/content/knowledge/research/research-conventions.md +248 -0
  4. package/content/knowledge/research/research-dev-environment.md +303 -0
  5. package/content/knowledge/research/research-experiment-loop.md +429 -0
  6. package/content/knowledge/research/research-experiment-tracking.md +336 -0
  7. package/content/knowledge/research/research-ml-architecture-search.md +383 -0
  8. package/content/knowledge/research/research-ml-evaluation.md +407 -0
  9. package/content/knowledge/research/research-ml-experiment-tracking.md +466 -0
  10. package/content/knowledge/research/research-ml-training-patterns.md +413 -0
  11. package/content/knowledge/research/research-observability.md +395 -0
  12. package/content/knowledge/research/research-overfitting-prevention.md +306 -0
  13. package/content/knowledge/research/research-project-structure.md +264 -0
  14. package/content/knowledge/research/research-quant-backtesting.md +326 -0
  15. package/content/knowledge/research/research-quant-market-data.md +366 -0
  16. package/content/knowledge/research/research-quant-metrics.md +335 -0
  17. package/content/knowledge/research/research-quant-requirements.md +223 -0
  18. package/content/knowledge/research/research-quant-risk.md +469 -0
  19. package/content/knowledge/research/research-quant-strategy-patterns.md +412 -0
  20. package/content/knowledge/research/research-requirements.md +201 -0
  21. package/content/knowledge/research/research-security.md +374 -0
  22. package/content/knowledge/research/research-sim-compute-management.md +538 -0
  23. package/content/knowledge/research/research-sim-engine-patterns.md +448 -0
  24. package/content/knowledge/research/research-sim-parameter-spaces.md +425 -0
  25. package/content/knowledge/research/research-sim-validation.md +456 -0
  26. package/content/knowledge/research/research-testing.md +334 -0
  27. package/content/methodology/research-ml-research.yml +23 -0
  28. package/content/methodology/research-overlay.yml +65 -0
  29. package/content/methodology/research-quant-finance.yml +29 -0
  30. package/content/methodology/research-simulation.yml +23 -0
  31. package/dist/cli/commands/adopt.d.ts.map +1 -1
  32. package/dist/cli/commands/adopt.js +22 -1
  33. package/dist/cli/commands/adopt.js.map +1 -1
  34. package/dist/cli/commands/adopt.serialization.test.js +41 -0
  35. package/dist/cli/commands/adopt.serialization.test.js.map +1 -1
  36. package/dist/cli/commands/init.d.ts +4 -0
  37. package/dist/cli/commands/init.d.ts.map +1 -1
  38. package/dist/cli/commands/init.js +32 -2
  39. package/dist/cli/commands/init.js.map +1 -1
  40. package/dist/cli/init-flag-families.d.ts +6 -1
  41. package/dist/cli/init-flag-families.d.ts.map +1 -1
  42. package/dist/cli/init-flag-families.js +32 -1
  43. package/dist/cli/init-flag-families.js.map +1 -1
  44. package/dist/cli/init-flag-families.test.js +47 -0
  45. package/dist/cli/init-flag-families.test.js.map +1 -1
  46. package/dist/config/schema.d.ts +272 -16
  47. package/dist/config/schema.d.ts.map +1 -1
  48. package/dist/config/schema.js +25 -1
  49. package/dist/config/schema.js.map +1 -1
  50. package/dist/config/schema.test.js +103 -3
  51. package/dist/config/schema.test.js.map +1 -1
  52. package/dist/core/assembly/overlay-loader.d.ts +12 -0
  53. package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
  54. package/dist/core/assembly/overlay-loader.js +30 -0
  55. package/dist/core/assembly/overlay-loader.js.map +1 -1
  56. package/dist/core/assembly/overlay-loader.test.js +66 -1
  57. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  58. package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
  59. package/dist/core/assembly/overlay-state-resolver.js +48 -19
  60. package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
  61. package/dist/core/assembly/overlay-state-resolver.test.js +80 -0
  62. package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
  63. package/dist/e2e/project-type-overlays.test.js +119 -0
  64. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  65. package/dist/project/adopt.d.ts.map +1 -1
  66. package/dist/project/adopt.js +3 -1
  67. package/dist/project/adopt.js.map +1 -1
  68. package/dist/project/detectors/disambiguate.js +1 -1
  69. package/dist/project/detectors/disambiguate.js.map +1 -1
  70. package/dist/project/detectors/index.d.ts.map +1 -1
  71. package/dist/project/detectors/index.js +2 -1
  72. package/dist/project/detectors/index.js.map +1 -1
  73. package/dist/project/detectors/ml.d.ts.map +1 -1
  74. package/dist/project/detectors/ml.js +2 -6
  75. package/dist/project/detectors/ml.js.map +1 -1
  76. package/dist/project/detectors/research.d.ts +4 -0
  77. package/dist/project/detectors/research.d.ts.map +1 -0
  78. package/dist/project/detectors/research.js +141 -0
  79. package/dist/project/detectors/research.js.map +1 -0
  80. package/dist/project/detectors/research.test.d.ts +2 -0
  81. package/dist/project/detectors/research.test.d.ts.map +1 -0
  82. package/dist/project/detectors/research.test.js +235 -0
  83. package/dist/project/detectors/research.test.js.map +1 -0
  84. package/dist/project/detectors/shared-signals.d.ts +3 -0
  85. package/dist/project/detectors/shared-signals.d.ts.map +1 -0
  86. package/dist/project/detectors/shared-signals.js +9 -0
  87. package/dist/project/detectors/shared-signals.js.map +1 -0
  88. package/dist/project/detectors/types.d.ts +6 -2
  89. package/dist/project/detectors/types.d.ts.map +1 -1
  90. package/dist/project/detectors/types.js.map +1 -1
  91. package/dist/types/config.d.ts +7 -1
  92. package/dist/types/config.d.ts.map +1 -1
  93. package/dist/wizard/copy/core.d.ts.map +1 -1
  94. package/dist/wizard/copy/core.js +4 -0
  95. package/dist/wizard/copy/core.js.map +1 -1
  96. package/dist/wizard/copy/index.d.ts.map +1 -1
  97. package/dist/wizard/copy/index.js +2 -0
  98. package/dist/wizard/copy/index.js.map +1 -1
  99. package/dist/wizard/copy/research.d.ts +3 -0
  100. package/dist/wizard/copy/research.d.ts.map +1 -0
  101. package/dist/wizard/copy/research.js +27 -0
  102. package/dist/wizard/copy/research.js.map +1 -0
  103. package/dist/wizard/copy/types.d.ts +5 -1
  104. package/dist/wizard/copy/types.d.ts.map +1 -1
  105. package/dist/wizard/flags.d.ts +7 -1
  106. package/dist/wizard/flags.d.ts.map +1 -1
  107. package/dist/wizard/questions.d.ts +4 -2
  108. package/dist/wizard/questions.d.ts.map +1 -1
  109. package/dist/wizard/questions.js +27 -1
  110. package/dist/wizard/questions.js.map +1 -1
  111. package/dist/wizard/questions.test.js +51 -0
  112. package/dist/wizard/questions.test.js.map +1 -1
  113. package/dist/wizard/wizard.d.ts +3 -2
  114. package/dist/wizard/wizard.d.ts.map +1 -1
  115. package/dist/wizard/wizard.js +3 -1
  116. package/dist/wizard/wizard.js.map +1 -1
  117. 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
+ ```