@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,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
|
+
```
|