@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.
Files changed (180) hide show
  1. package/README.md +32 -10
  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 +30 -8
  33. package/dist/cli/commands/adopt.js.map +1 -1
  34. package/dist/cli/commands/adopt.serialization.test.js +49 -0
  35. package/dist/cli/commands/adopt.serialization.test.js.map +1 -1
  36. package/dist/cli/commands/adopt.test.js +8 -0
  37. package/dist/cli/commands/adopt.test.js.map +1 -1
  38. package/dist/cli/commands/build.d.ts.map +1 -1
  39. package/dist/cli/commands/build.js +191 -180
  40. package/dist/cli/commands/build.js.map +1 -1
  41. package/dist/cli/commands/complete.d.ts.map +1 -1
  42. package/dist/cli/commands/complete.js +16 -12
  43. package/dist/cli/commands/complete.js.map +1 -1
  44. package/dist/cli/commands/complete.test.js +14 -5
  45. package/dist/cli/commands/complete.test.js.map +1 -1
  46. package/dist/cli/commands/init.d.ts +4 -0
  47. package/dist/cli/commands/init.d.ts.map +1 -1
  48. package/dist/cli/commands/init.js +75 -51
  49. package/dist/cli/commands/init.js.map +1 -1
  50. package/dist/cli/commands/init.test.js +33 -27
  51. package/dist/cli/commands/init.test.js.map +1 -1
  52. package/dist/cli/commands/reset.d.ts.map +1 -1
  53. package/dist/cli/commands/reset.js +44 -40
  54. package/dist/cli/commands/reset.js.map +1 -1
  55. package/dist/cli/commands/reset.test.js +42 -20
  56. package/dist/cli/commands/reset.test.js.map +1 -1
  57. package/dist/cli/commands/rework.d.ts.map +1 -1
  58. package/dist/cli/commands/rework.js +16 -12
  59. package/dist/cli/commands/rework.js.map +1 -1
  60. package/dist/cli/commands/rework.test.js +12 -3
  61. package/dist/cli/commands/rework.test.js.map +1 -1
  62. package/dist/cli/commands/run.d.ts.map +1 -1
  63. package/dist/cli/commands/run.js +318 -298
  64. package/dist/cli/commands/run.js.map +1 -1
  65. package/dist/cli/commands/run.test.js +92 -120
  66. package/dist/cli/commands/run.test.js.map +1 -1
  67. package/dist/cli/commands/skip.d.ts.map +1 -1
  68. package/dist/cli/commands/skip.js +19 -15
  69. package/dist/cli/commands/skip.js.map +1 -1
  70. package/dist/cli/commands/skip.test.js +22 -11
  71. package/dist/cli/commands/skip.test.js.map +1 -1
  72. package/dist/cli/commands/update.d.ts.map +1 -1
  73. package/dist/cli/commands/update.js +3 -1
  74. package/dist/cli/commands/update.js.map +1 -1
  75. package/dist/cli/commands/update.test.js +8 -4
  76. package/dist/cli/commands/update.test.js.map +1 -1
  77. package/dist/cli/commands/version.d.ts.map +1 -1
  78. package/dist/cli/commands/version.js +3 -1
  79. package/dist/cli/commands/version.js.map +1 -1
  80. package/dist/cli/commands/version.test.js +9 -5
  81. package/dist/cli/commands/version.test.js.map +1 -1
  82. package/dist/cli/index.d.ts.map +1 -1
  83. package/dist/cli/index.js +2 -0
  84. package/dist/cli/index.js.map +1 -1
  85. package/dist/cli/init-flag-families.d.ts +6 -1
  86. package/dist/cli/init-flag-families.d.ts.map +1 -1
  87. package/dist/cli/init-flag-families.js +32 -1
  88. package/dist/cli/init-flag-families.js.map +1 -1
  89. package/dist/cli/init-flag-families.test.js +47 -0
  90. package/dist/cli/init-flag-families.test.js.map +1 -1
  91. package/dist/cli/output/interactive.d.ts +1 -0
  92. package/dist/cli/output/interactive.d.ts.map +1 -1
  93. package/dist/cli/output/interactive.js +5 -0
  94. package/dist/cli/output/interactive.js.map +1 -1
  95. package/dist/cli/shutdown.d.ts +51 -0
  96. package/dist/cli/shutdown.d.ts.map +1 -0
  97. package/dist/cli/shutdown.js +199 -0
  98. package/dist/cli/shutdown.js.map +1 -0
  99. package/dist/cli/shutdown.test.d.ts +2 -0
  100. package/dist/cli/shutdown.test.d.ts.map +1 -0
  101. package/dist/cli/shutdown.test.js +316 -0
  102. package/dist/cli/shutdown.test.js.map +1 -0
  103. package/dist/config/schema.d.ts +272 -16
  104. package/dist/config/schema.d.ts.map +1 -1
  105. package/dist/config/schema.js +25 -1
  106. package/dist/config/schema.js.map +1 -1
  107. package/dist/config/schema.test.js +103 -3
  108. package/dist/config/schema.test.js.map +1 -1
  109. package/dist/core/assembly/overlay-loader.d.ts +12 -0
  110. package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
  111. package/dist/core/assembly/overlay-loader.js +30 -0
  112. package/dist/core/assembly/overlay-loader.js.map +1 -1
  113. package/dist/core/assembly/overlay-loader.test.js +66 -1
  114. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  115. package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
  116. package/dist/core/assembly/overlay-state-resolver.js +48 -19
  117. package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
  118. package/dist/core/assembly/overlay-state-resolver.test.js +80 -0
  119. package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
  120. package/dist/e2e/init.test.js +5 -4
  121. package/dist/e2e/init.test.js.map +1 -1
  122. package/dist/e2e/project-type-overlays.test.js +119 -0
  123. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  124. package/dist/project/adopt.d.ts.map +1 -1
  125. package/dist/project/adopt.js +3 -1
  126. package/dist/project/adopt.js.map +1 -1
  127. package/dist/project/detectors/disambiguate.js +1 -1
  128. package/dist/project/detectors/disambiguate.js.map +1 -1
  129. package/dist/project/detectors/index.d.ts.map +1 -1
  130. package/dist/project/detectors/index.js +2 -1
  131. package/dist/project/detectors/index.js.map +1 -1
  132. package/dist/project/detectors/ml.d.ts.map +1 -1
  133. package/dist/project/detectors/ml.js +2 -6
  134. package/dist/project/detectors/ml.js.map +1 -1
  135. package/dist/project/detectors/research.d.ts +4 -0
  136. package/dist/project/detectors/research.d.ts.map +1 -0
  137. package/dist/project/detectors/research.js +141 -0
  138. package/dist/project/detectors/research.js.map +1 -0
  139. package/dist/project/detectors/research.test.d.ts +2 -0
  140. package/dist/project/detectors/research.test.d.ts.map +1 -0
  141. package/dist/project/detectors/research.test.js +235 -0
  142. package/dist/project/detectors/research.test.js.map +1 -0
  143. package/dist/project/detectors/shared-signals.d.ts +3 -0
  144. package/dist/project/detectors/shared-signals.d.ts.map +1 -0
  145. package/dist/project/detectors/shared-signals.js +9 -0
  146. package/dist/project/detectors/shared-signals.js.map +1 -0
  147. package/dist/project/detectors/types.d.ts +6 -2
  148. package/dist/project/detectors/types.d.ts.map +1 -1
  149. package/dist/project/detectors/types.js.map +1 -1
  150. package/dist/state/lock-manager.d.ts +1 -0
  151. package/dist/state/lock-manager.d.ts.map +1 -1
  152. package/dist/state/lock-manager.js +1 -1
  153. package/dist/state/lock-manager.js.map +1 -1
  154. package/dist/types/config.d.ts +7 -1
  155. package/dist/types/config.d.ts.map +1 -1
  156. package/dist/wizard/copy/core.d.ts.map +1 -1
  157. package/dist/wizard/copy/core.js +4 -0
  158. package/dist/wizard/copy/core.js.map +1 -1
  159. package/dist/wizard/copy/index.d.ts.map +1 -1
  160. package/dist/wizard/copy/index.js +2 -0
  161. package/dist/wizard/copy/index.js.map +1 -1
  162. package/dist/wizard/copy/research.d.ts +3 -0
  163. package/dist/wizard/copy/research.d.ts.map +1 -0
  164. package/dist/wizard/copy/research.js +27 -0
  165. package/dist/wizard/copy/research.js.map +1 -0
  166. package/dist/wizard/copy/types.d.ts +5 -1
  167. package/dist/wizard/copy/types.d.ts.map +1 -1
  168. package/dist/wizard/flags.d.ts +7 -1
  169. package/dist/wizard/flags.d.ts.map +1 -1
  170. package/dist/wizard/questions.d.ts +4 -2
  171. package/dist/wizard/questions.d.ts.map +1 -1
  172. package/dist/wizard/questions.js +27 -1
  173. package/dist/wizard/questions.js.map +1 -1
  174. package/dist/wizard/questions.test.js +51 -0
  175. package/dist/wizard/questions.test.js.map +1 -1
  176. package/dist/wizard/wizard.d.ts +3 -2
  177. package/dist/wizard/wizard.d.ts.map +1 -1
  178. package/dist/wizard/wizard.js +3 -1
  179. package/dist/wizard/wizard.js.map +1 -1
  180. package/package.json +1 -1
@@ -0,0 +1,456 @@
1
+ ---
2
+ name: research-sim-validation
3
+ description: Simulation validation methodology including comparison against analytical solutions, mesh independence studies, convergence testing, Richardson extrapolation, uncertainty quantification, and the verification vs validation distinction
4
+ topics: [research, simulation, validation, verification, mesh-independence, convergence, richardson-extrapolation, uncertainty-quantification, analytical-solutions, mms]
5
+ ---
6
+
7
+ Simulation validation answers the fundamental question: does this simulation represent reality? Verification asks a different question: does the code correctly solve the mathematical model? Both are essential -- a perfectly verified code solving the wrong equations is useless, and an unverified code matching experiments might be right for the wrong reasons. The validation pipeline establishes trust in simulation results by systematically comparing against known solutions, demonstrating grid independence, quantifying numerical uncertainty, and documenting the conditions under which the simulation is reliable.
8
+
9
+ ## Summary
10
+
11
+ Distinguish verification (solving equations right) from validation (solving the right equations). Verify code against analytical solutions and manufactured solutions (MMS) where exact answers are known. Demonstrate mesh independence through systematic refinement studies showing solution convergence. Apply Richardson extrapolation to estimate the grid-converged solution and quantify discretization error. Perform uncertainty quantification to propagate input uncertainties through the simulation. Document validation domains -- the parameter ranges where the simulation has been shown to agree with experiments within stated tolerances.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Verification vs Validation Framework
16
+
17
+ Establish the V&V hierarchy before running any production simulations:
18
+
19
+ ```python
20
+ # src/simulation/validation/framework.py
21
+ from dataclasses import dataclass, field
22
+ from enum import Enum
23
+ from typing import Any
24
+ from pathlib import Path
25
+
26
+ class VVLevel(Enum):
27
+ """Levels in the verification & validation hierarchy."""
28
+ CODE_VERIFICATION = "code_verification" # Does code solve equations correctly?
29
+ SOLUTION_VERIFICATION = "solution_verification" # Is this specific solution converged?
30
+ VALIDATION = "validation" # Does the model represent reality?
31
+ PREDICTION = "prediction" # Extrapolation beyond validated domain
32
+
33
+ @dataclass
34
+ class VVResult:
35
+ """Result of a verification or validation test."""
36
+ level: VVLevel
37
+ test_name: str
38
+ passed: bool
39
+ expected_value: float | None = None
40
+ computed_value: float | None = None
41
+ error: float | None = None
42
+ tolerance: float | None = None
43
+ details: dict[str, Any] = field(default_factory=dict)
44
+
45
+ @dataclass
46
+ class ValidationDomain:
47
+ """Documents the parameter range where simulation is validated."""
48
+ parameter_ranges: dict[str, tuple[float, float]]
49
+ validated_outputs: list[str]
50
+ max_error_percent: float
51
+ reference: str # Paper, experiment, or analytical solution
52
+ conditions: list[str] = field(default_factory=list)
53
+
54
+ class ValidationSuite:
55
+ """Manages a collection of V&V tests for a simulation code."""
56
+
57
+ def __init__(self):
58
+ self.results: list[VVResult] = []
59
+ self.domains: list[ValidationDomain] = []
60
+
61
+ def add_analytical_test(
62
+ self,
63
+ name: str,
64
+ computed: float,
65
+ exact: float,
66
+ tolerance: float = 1e-3,
67
+ ) -> VVResult:
68
+ """Compare against known analytical solution."""
69
+ error = abs(computed - exact) / abs(exact) if exact != 0 else abs(computed)
70
+ result = VVResult(
71
+ level=VVLevel.CODE_VERIFICATION,
72
+ test_name=name,
73
+ passed=error <= tolerance,
74
+ expected_value=exact,
75
+ computed_value=computed,
76
+ error=error,
77
+ tolerance=tolerance,
78
+ )
79
+ self.results.append(result)
80
+ return result
81
+
82
+ def is_in_validated_domain(self, params: dict[str, float]) -> bool:
83
+ """Check if parameters fall within a validated domain."""
84
+ for domain in self.domains:
85
+ in_domain = all(
86
+ domain.parameter_ranges[k][0] <= params.get(k, float("inf")) <= domain.parameter_ranges[k][1]
87
+ for k in domain.parameter_ranges
88
+ )
89
+ if in_domain:
90
+ return True
91
+ return False
92
+
93
+ def summary(self) -> dict[str, Any]:
94
+ """Summary of V&V status."""
95
+ by_level = {}
96
+ for result in self.results:
97
+ level = result.level.value
98
+ by_level.setdefault(level, {"passed": 0, "failed": 0})
99
+ if result.passed:
100
+ by_level[level]["passed"] += 1
101
+ else:
102
+ by_level[level]["failed"] += 1
103
+ return by_level
104
+ ```
105
+
106
+ ### Analytical Solution Comparison
107
+
108
+ Compare simulation output against problems with known exact solutions:
109
+
110
+ ```python
111
+ # src/simulation/validation/analytical.py
112
+ import numpy as np
113
+ from dataclasses import dataclass
114
+ from typing import Callable
115
+
116
+ @dataclass
117
+ class AnalyticalTestCase:
118
+ """A test case with known exact solution."""
119
+ name: str
120
+ description: str
121
+ setup_params: dict # Parameters to configure the simulation
122
+ exact_solution: Callable[[np.ndarray], np.ndarray] # f(x) -> solution
123
+ error_norm: str = "L2" # L2, Linf, L1
124
+ expected_order: float = 2.0 # Expected convergence order
125
+
126
+ def compute_error_norms(
127
+ computed: np.ndarray,
128
+ exact: np.ndarray,
129
+ dx: float | None = None,
130
+ ) -> dict[str, float]:
131
+ """Compute multiple error norms between computed and exact solutions."""
132
+ diff = computed - exact
133
+ norms = {
134
+ "L_inf": float(np.max(np.abs(diff))),
135
+ "L2": float(np.sqrt(np.mean(diff**2))),
136
+ "L1": float(np.mean(np.abs(diff))),
137
+ }
138
+ if dx is not None:
139
+ # Proper integral norms for non-uniform grids
140
+ norms["L2_integral"] = float(np.sqrt(np.sum(diff**2 * dx)))
141
+ # Relative errors
142
+ exact_norm = np.sqrt(np.mean(exact**2))
143
+ if exact_norm > 0:
144
+ norms["relative_L2"] = norms["L2"] / exact_norm
145
+ return norms
146
+
147
+ def manufactured_solution_source(
148
+ solution_func: Callable,
149
+ operator: Callable,
150
+ ) -> Callable:
151
+ """Method of Manufactured Solutions: compute source term for a chosen solution.
152
+
153
+ Given a desired solution u(x) and the PDE operator L, compute
154
+ the source term f = L(u) so that u is the exact solution of L(u) = f.
155
+ """
156
+ def source_term(x: np.ndarray) -> np.ndarray:
157
+ return operator(solution_func, x)
158
+ return source_term
159
+ ```
160
+
161
+ ### Mesh Independence Studies
162
+
163
+ Systematically refine the mesh to demonstrate solution convergence:
164
+
165
+ ```python
166
+ # src/simulation/validation/mesh_study.py
167
+ from dataclasses import dataclass
168
+ import numpy as np
169
+ from typing import Any, Callable
170
+
171
+ @dataclass
172
+ class MeshLevel:
173
+ """One level in a mesh refinement study."""
174
+ name: str
175
+ element_count: int
176
+ characteristic_size: float # h = representative element size
177
+ result: dict[str, float] | None = None
178
+ wall_time: float = 0.0
179
+
180
+ @dataclass
181
+ class MeshStudyResult:
182
+ """Results of a mesh independence study."""
183
+ levels: list[MeshLevel]
184
+ quantity_name: str
185
+ converged: bool
186
+ convergence_order: float | None
187
+ richardson_estimate: float | None
188
+ discretization_error: float | None
189
+ gci: float | None # Grid Convergence Index
190
+
191
+ def run_mesh_independence_study(
192
+ run_simulation: Callable[[float], dict[str, float]],
193
+ mesh_sizes: list[float],
194
+ quantity: str,
195
+ refinement_ratio: float = 2.0,
196
+ safety_factor: float = 1.25,
197
+ ) -> MeshStudyResult:
198
+ """Run simulations at multiple mesh resolutions and assess convergence.
199
+
200
+ Args:
201
+ run_simulation: function(h) -> {quantity: value} for a given mesh size h
202
+ mesh_sizes: list of characteristic element sizes (coarse to fine)
203
+ quantity: name of the output quantity to track
204
+ refinement_ratio: ratio between successive mesh sizes
205
+ safety_factor: GCI safety factor (1.25 for 3+ grids, 3.0 for 2 grids)
206
+ """
207
+ levels = []
208
+ for h in sorted(mesh_sizes, reverse=True): # Coarse to fine
209
+ result = run_simulation(h)
210
+ levels.append(MeshLevel(
211
+ name=f"h={h:.4f}",
212
+ element_count=int(1.0 / h**2), # Approximate for 2D
213
+ characteristic_size=h,
214
+ result=result,
215
+ ))
216
+
217
+ # Need at least 3 levels for Richardson extrapolation
218
+ if len(levels) < 3:
219
+ values = [lev.result[quantity] for lev in levels if lev.result]
220
+ converged = len(values) >= 2 and abs(values[-1] - values[-2]) / abs(values[-1]) < 0.01
221
+ return MeshStudyResult(
222
+ levels=levels, quantity_name=quantity,
223
+ converged=converged, convergence_order=None,
224
+ richardson_estimate=None, discretization_error=None, gci=None,
225
+ )
226
+
227
+ # Richardson extrapolation with three finest grids
228
+ f1 = levels[-1].result[quantity] # Finest
229
+ f2 = levels[-2].result[quantity] # Medium
230
+ f3 = levels[-3].result[quantity] # Coarse
231
+ h1 = levels[-1].characteristic_size
232
+ h2 = levels[-2].characteristic_size
233
+
234
+ r = h2 / h1 # Refinement ratio
235
+
236
+ # Observed convergence order
237
+ if (f2 - f3) != 0 and (f1 - f2) / (f2 - f3) > 0:
238
+ p = np.log(abs((f3 - f2) / (f2 - f1))) / np.log(r)
239
+ else:
240
+ p = None
241
+
242
+ # Richardson extrapolation estimate
243
+ if p is not None and p > 0:
244
+ richardson = f1 + (f1 - f2) / (r**p - 1)
245
+ error = abs(f1 - richardson) / abs(richardson) if richardson != 0 else abs(f1 - richardson)
246
+ # Grid Convergence Index
247
+ gci = safety_factor * abs((f1 - f2) / f1) / (r**p - 1)
248
+ else:
249
+ richardson = None
250
+ error = None
251
+ gci = None
252
+
253
+ converged = error is not None and error < 0.02 # 2% threshold
254
+
255
+ return MeshStudyResult(
256
+ levels=levels,
257
+ quantity_name=quantity,
258
+ converged=converged,
259
+ convergence_order=float(p) if p else None,
260
+ richardson_estimate=float(richardson) if richardson else None,
261
+ discretization_error=float(error) if error else None,
262
+ gci=float(gci) if gci else None,
263
+ )
264
+ ```
265
+
266
+ ### Convergence Testing
267
+
268
+ Monitor iterative solver convergence to detect problems early:
269
+
270
+ ```python
271
+ # src/simulation/validation/convergence.py
272
+ from dataclasses import dataclass
273
+ import numpy as np
274
+
275
+ @dataclass
276
+ class ConvergenceMetrics:
277
+ """Metrics describing iterative convergence behavior."""
278
+ converged: bool
279
+ final_residual: float
280
+ convergence_rate: float # Average reduction per iteration
281
+ oscillating: bool # Residual oscillates rather than monotonically decreasing
282
+ stalled: bool # Residual stopped decreasing
283
+ iterations_to_converge: int | None
284
+
285
+ def analyze_convergence(
286
+ residuals: list[float],
287
+ tolerance: float = 1e-6,
288
+ stall_window: int = 50,
289
+ stall_threshold: float = 0.01,
290
+ ) -> ConvergenceMetrics:
291
+ """Analyze residual history for convergence behavior."""
292
+ if not residuals:
293
+ return ConvergenceMetrics(
294
+ converged=False, final_residual=float("inf"),
295
+ convergence_rate=0, oscillating=False, stalled=True,
296
+ iterations_to_converge=None,
297
+ )
298
+
299
+ arr = np.array(residuals)
300
+ final = arr[-1]
301
+ converged = final < tolerance
302
+
303
+ # Convergence rate: geometric mean reduction
304
+ if len(arr) > 1 and arr[0] > 0:
305
+ rate = (arr[-1] / arr[0]) ** (1 / len(arr))
306
+ else:
307
+ rate = 1.0
308
+
309
+ # Oscillation detection: sign changes in differences
310
+ if len(arr) > 2:
311
+ diffs = np.diff(arr)
312
+ sign_changes = np.sum(np.diff(np.sign(diffs)) != 0)
313
+ oscillating = sign_changes > len(diffs) * 0.4
314
+ else:
315
+ oscillating = False
316
+
317
+ # Stall detection: recent improvement < threshold
318
+ if len(arr) > stall_window:
319
+ recent = arr[-stall_window:]
320
+ improvement = 1.0 - recent[-1] / recent[0] if recent[0] > 0 else 0
321
+ stalled = improvement < stall_threshold
322
+ else:
323
+ stalled = False
324
+
325
+ # Find iteration where tolerance was first reached
326
+ below_tol = np.where(arr < tolerance)[0]
327
+ iter_to_conv = int(below_tol[0]) if len(below_tol) > 0 else None
328
+
329
+ return ConvergenceMetrics(
330
+ converged=converged,
331
+ final_residual=float(final),
332
+ convergence_rate=float(rate),
333
+ oscillating=oscillating,
334
+ stalled=stalled,
335
+ iterations_to_converge=iter_to_conv,
336
+ )
337
+ ```
338
+
339
+ ### Uncertainty Quantification
340
+
341
+ Propagate input uncertainties through the simulation to bound output uncertainty:
342
+
343
+ ```python
344
+ # src/simulation/validation/uncertainty.py
345
+ import numpy as np
346
+ from dataclasses import dataclass
347
+ from typing import Callable
348
+
349
+ @dataclass
350
+ class UncertainParameter:
351
+ """Input parameter with associated uncertainty."""
352
+ name: str
353
+ nominal: float
354
+ distribution: str # "normal", "uniform", "lognormal"
355
+ # For normal: std_dev; for uniform: half_width; for lognormal: sigma
356
+ uncertainty: float
357
+
358
+ @dataclass
359
+ class UQResult:
360
+ """Result of uncertainty quantification analysis."""
361
+ quantity: str
362
+ mean: float
363
+ std: float
364
+ ci_95: tuple[float, float]
365
+ samples: np.ndarray
366
+ sensitivity: dict[str, float] # Local sensitivity dY/dX_i * sigma_i
367
+
368
+ def monte_carlo_uq(
369
+ evaluate_fn: Callable[[dict[str, float]], float],
370
+ uncertain_params: list[UncertainParameter],
371
+ n_samples: int = 1000,
372
+ seed: int = 42,
373
+ ) -> UQResult:
374
+ """Propagate uncertainties via Monte Carlo sampling."""
375
+ rng = np.random.default_rng(seed)
376
+ samples = np.zeros(n_samples)
377
+
378
+ for i in range(n_samples):
379
+ point = {}
380
+ for param in uncertain_params:
381
+ if param.distribution == "normal":
382
+ point[param.name] = rng.normal(param.nominal, param.uncertainty)
383
+ elif param.distribution == "uniform":
384
+ point[param.name] = rng.uniform(
385
+ param.nominal - param.uncertainty,
386
+ param.nominal + param.uncertainty,
387
+ )
388
+ elif param.distribution == "lognormal":
389
+ point[param.name] = rng.lognormal(
390
+ np.log(param.nominal), param.uncertainty
391
+ )
392
+ samples[i] = evaluate_fn(point)
393
+
394
+ # Local sensitivity via finite differences at nominal
395
+ nominal_point = {p.name: p.nominal for p in uncertain_params}
396
+ y_nominal = evaluate_fn(nominal_point)
397
+ sensitivity = {}
398
+ for param in uncertain_params:
399
+ perturbed = nominal_point.copy()
400
+ delta = param.uncertainty * 0.01 # Small perturbation
401
+ perturbed[param.name] = param.nominal + delta
402
+ y_perturbed = evaluate_fn(perturbed)
403
+ dydx = (y_perturbed - y_nominal) / delta
404
+ sensitivity[param.name] = abs(dydx * param.uncertainty)
405
+
406
+ return UQResult(
407
+ quantity="output",
408
+ mean=float(np.mean(samples)),
409
+ std=float(np.std(samples)),
410
+ ci_95=(float(np.percentile(samples, 2.5)), float(np.percentile(samples, 97.5))),
411
+ samples=samples,
412
+ sensitivity=sensitivity,
413
+ )
414
+ ```
415
+
416
+ ### Validation Test Organization
417
+
418
+ Structure validation tests as a regression suite that runs with every code change:
419
+
420
+ ```python
421
+ # tests/validation/conftest.py
422
+ import pytest
423
+ from src.simulation.validation.framework import ValidationSuite
424
+
425
+ @pytest.fixture
426
+ def validation_suite():
427
+ return ValidationSuite()
428
+
429
+ # tests/validation/test_analytical.py
430
+ def test_poiseuille_flow(simulation_engine, validation_suite):
431
+ """Verify against Poiseuille flow analytical solution."""
432
+ # Analytical: u(y) = (dp/dx) * y * (H - y) / (2 * mu)
433
+ params = {"pressure_gradient": 1.0, "viscosity": 0.01, "channel_height": 1.0}
434
+ result = simulation_engine(params)
435
+
436
+ exact_max_velocity = params["pressure_gradient"] * params["channel_height"]**2 / (8 * params["viscosity"])
437
+ validation_suite.add_analytical_test(
438
+ name="poiseuille_centerline_velocity",
439
+ computed=result.outputs["max_velocity"],
440
+ exact=exact_max_velocity,
441
+ tolerance=0.01, # 1% relative error
442
+ )
443
+ assert validation_suite.results[-1].passed
444
+
445
+ def test_mesh_independence(simulation_engine, validation_suite):
446
+ """Demonstrate mesh-independent results for production configuration."""
447
+ from src.simulation.validation.mesh_study import run_mesh_independence_study
448
+
449
+ study = run_mesh_independence_study(
450
+ run_simulation=lambda h: simulation_engine({"mesh_size": h}),
451
+ mesh_sizes=[0.1, 0.05, 0.025, 0.0125],
452
+ quantity="drag_coefficient",
453
+ )
454
+ assert study.converged, f"Mesh study did not converge: GCI={study.gci}"
455
+ assert study.convergence_order >= 1.5, f"Order {study.convergence_order} below expected"
456
+ ```