
Convergence Study
- 16 installs
- 869 repo stars
- Updated June 8, 2026
- beita6969/scienceclaw
convergence-study is a Claude skill that runs spatial and temporal convergence analysis with Richardson extrapolation and the Grid Convergence Index for numerical solution verification.
About
This skill performs spatial and temporal convergence analysis to verify that numerical solutions converge at the expected rate as the mesh or timestep is refined. A developer uses it to compute observed convergence order, estimate discretization error with Richardson extrapolation, and report the Grid Convergence Index for formal solution verification. It provides CLI scripts that output structured JSON.
- Runs spatial and temporal convergence analysis for numerical solution verification
- Computes observed order, Richardson extrapolation, and Grid Convergence Index (GCI)
- Ships CLI scripts for h-refinement, dt-refinement, and GCI calculation
Convergence Study by the numbers
- 16 all-time installs (skills.sh)
- Ranked #1,318 of 2,065 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
convergence-study capabilities & compatibility
Free; scripts use only the Python math stdlib, NumPy not required
- Capabilities
- code execution · data stats analysis
- Use cases
- testing · data analysis · research
- Pricing
- Free
What convergence-study says it does
Provide script-driven convergence analysis for verifying that numerical solutions converge at the expected rate as the mesh or timestep is refined.
Report GCI** for formal solution verification using `gci_calculator.py`
Safety factor | GCI safety factor (1.25 default)
npx skills add https://github.com/beita6969/scienceclaw --skill convergence-studyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 869 |
| Last updated | June 8, 2026 |
| Repository | beita6969/scienceclaw ↗ |
What it does
Verify a numerical solver converges at its expected order using Richardson extrapolation and GCI.
Who is it for?
Verifying numerical solver accuracy via observed order, Richardson extrapolation, and GCI
Skip if: Non-numerical testing or general software unit tests
When should I use this skill?
You have refinement-level solution values and need to confirm convergence order or estimate discretization error
What you get
Confirmed observed convergence order and reported GCI for formal solution verification
- observed convergence order
- Richardson-extrapolated value
- GCI report
By the numbers
- 4 CLI scripts
- GCI default safety factor 1.25
- requires 3+ refinement levels for order verification
Files
Convergence Study
Goal
Provide script-driven convergence analysis for verifying that numerical solutions converge at the expected rate as the mesh or timestep is refined.
Requirements
- Python 3.8+
- NumPy (not required; scripts use only math stdlib)
Inputs to Gather
| Input | Description | Example |
|---|---|---|
| Grid spacings | Sequence of mesh sizes (coarse to fine) | 0.4,0.2,0.1,0.05 |
| Timestep sizes | Sequence of dt values | 0.04,0.02,0.01 |
| Solution values | QoI at each refinement level | 1.16,1.04,1.01,1.0025 |
| Expected order | Formal order of the numerical scheme | 2.0 |
| Safety factor | GCI safety factor (1.25 default) | 1.25 |
Script Outputs (JSON Fields)
| Script | Key Outputs |
|---|---|
scripts/h_refinement.py | results.observed_orders, results.mean_order, results.richardson_extrapolated_value, results.convergence_assessment |
scripts/dt_refinement.py | Same as h_refinement but for temporal convergence |
scripts/richardson_extrapolation.py | results.extrapolated_value, results.error_estimate, results.observed_order |
scripts/gci_calculator.py | results.observed_order, results.gci_fine, results.gci_coarse, results.asymptotic_ratio, results.in_asymptotic_range |
Workflow
1. Run grid/timestep refinement study with at least 3 levels 2. Compute observed convergence order with h_refinement.py or dt_refinement.py 3. Compare observed order to expected order of the scheme 4. Estimate discretization error via Richardson extrapolation 5. Report GCI for formal solution verification using gci_calculator.py 6. Document convergence results and any anomalies
Decision Guidance
Do you have 3+ refinement levels?
+-- YES --> Run h_refinement.py or dt_refinement.py
| +-- Observed order matches expected? --> Solution verified
| +-- Order too low? --> Check: pre-asymptotic, coding error, insufficient resolution
| +-- Order too high? --> Check: superconvergence or cancellation effects
+-- NO (only 2 levels) --> Use richardson_extrapolation.py with assumed order
(less reliable without order verification)CLI Examples
# Spatial convergence with 4 grid levels
python3 scripts/h_refinement.py --spacings 0.4,0.2,0.1,0.05 --values 1.16,1.04,1.01,1.0025 --expected-order 2.0 --json
# Temporal convergence with 3 timestep levels
python3 scripts/dt_refinement.py --timesteps 0.04,0.02,0.01 --values 2.12,2.03,2.0075 --expected-order 2.0 --json
# Richardson extrapolation with assumed 2nd-order
python3 scripts/richardson_extrapolation.py --spacings 0.02,0.01 --values 1.0032,1.0008 --order 2.0 --json
# GCI for 3-mesh verification
python3 scripts/gci_calculator.py --spacings 0.04,0.02,0.01 --values 1.0128,1.0032,1.0008 --jsonError Handling
| Error | Cause | Resolution |
|---|---|---|
spacings and values must have the same length | Mismatched input arrays | Provide equal-length lists |
At least 2 refinement levels required | Too few data points | Add more refinement levels |
Exactly 3 refinement levels required | GCI needs 3 levels | Provide fine/medium/coarse |
Oscillatory convergence detected | Non-monotone convergence | Check mesh quality or scheme |
Interpretation Guidance
| Scenario | Meaning | Action |
|---|---|---|
| Observed order matches expected | Solution in asymptotic range | Report GCI, extrapolate |
| Observed order < expected | Pre-asymptotic or coding bug | Refine further or debug |
| Negative observed order | Solution diverging | Check implementation |
| GCI asymptotic ratio near 1.0 | Grids in asymptotic range | Results are reliable |
| GCI asymptotic ratio far from 1.0 | Not in asymptotic range | Refine further |
References
references/convergence_theory.md- Formal convergence order, log-log analysis, asymptotic rangereferences/gci_guidelines.md- Roache's GCI method, ASME V&V 20, safety factors
Convergence Theory
Formal Convergence Order
A numerical method has order p if the discretization error satisfies:
e(h) = f(h) - f_exact = C * h^p + O(h^{p+1})
where h is the grid spacing (or timestep), C is a constant, and f_exact is the exact solution.
Observed Convergence Order
Given solutions on three grids with spacings h1 < h2 < h3 and uniform refinement ratio r = h2/h1 = h3/h2, the observed order is:
p_obs = log(|f3 - f2| / |f2 - f1|) / log(r)
For non-uniform refinement ratios, the general formula uses consecutive pairs and the local ratio.
Log-Log Analysis
On a log-log plot of error vs. spacing, a convergent solution shows a straight line with slope equal to the convergence order. Deviations indicate:
- Steeper slope at coarse grids: Pre-asymptotic regime (higher-order terms
still contribute)
- Flattening at fine grids: Round-off error or iterative error dominance
- Irregular slope: Possible implementation error or singularity
Asymptotic Range
A solution is in the asymptotic range when the leading-order error term dominates:
e(h) ~ C * h^p
Indicators of being in the asymptotic range:
1. Observed order matches expected order (within ~10%) 2. Consecutive observed-order estimates are consistent 3. Richardson extrapolation gives consistent results from different grid pairs
Pre-Asymptotic Behavior
When the solution is NOT in the asymptotic range:
- Higher-order terms C2h^{p+1} + C3h^{p+2} + ... are significant
- Observed order varies between consecutive grid pairs (>50% variation)
- Richardson extrapolation may be unreliable
- Resolution: use finer grids until asymptotic behavior is observed
Richardson Extrapolation
Given two solutions f1 (fine, spacing h1) and f2 (coarse, spacing h2) with refinement ratio r = h2/h1 and assumed order p:
f_extrapolated = f1 + (f1 - f2) / (r^p - 1)
This removes the leading-order error term, giving an estimate with error O(h^{p+1}).
Error Estimate
The discretization error estimate for the fine-grid solution is:
error ~ |f1 - f2| / (r^p - 1)
Reliability
Richardson extrapolation is most reliable when:
- The solution is in the asymptotic range
- The assumed order matches the actual convergence order
- The refinement ratio is moderate (r = 1.5 to 3.0)
It becomes unreliable when:
- Too few grid levels to verify the order
- Oscillatory convergence (sign changes in differences)
- Observed order differs significantly from assumed order
Monotone vs. Oscillatory Convergence
Monotone convergence: Solution values approach the exact solution from one side. Differences (f3-f2) and (f2-f1) have the same sign.
Oscillatory convergence: Solution values alternate above and below the exact solution. Differences have opposite signs. Standard Richardson extrapolation and GCI do not apply.
Manufactured Solutions
For verification, the Method of Manufactured Solutions (MMS) provides:
1. Choose an exact solution f_exact(x,t) 2. Substitute into the PDE to compute the source term 3. Solve numerically with the computed source 4. Compare numerical and exact solutions
This guarantees the exact solution is known, enabling precise convergence studies without approximation of the reference solution.
Grid Convergence Index (GCI) Guidelines
Overview
The Grid Convergence Index (GCI) is a standardized method for reporting discretization uncertainty in CFD and numerical simulations. It was developed by Patrick Roache and is recommended by ASME V&V 20 and AIAA standards.
Three-Grid GCI Procedure
Requirements
- Three systematically refined grids with spacings h1 < h2 < h3
- Constant or near-constant refinement ratios r21 = h2/h1, r32 = h3/h2
- Recommended: r >= 1.3 (to ensure measurable differences)
- Monotone convergence (no oscillation in solution values)
Step-by-Step Calculation
1. Compute refinement ratios:
r21 = h2 / h1 r32 = h3 / h2
2. Compute observed order p:
p = |ln|e32/e21|| / ln(r21)
where e32 = f3 - f2 and e21 = f2 - f1.
For non-uniform ratios, an iterative procedure using:
p = |ln|e32/e21| + ln((r21^p - s) / (r32^p - s))| / ln(r21)
where s = sign(e32/e21). This reduces to the simple formula when r21 = r32.
3. Compute GCI for fine grid:
GCI_fine = Fs * |e21/f1| / (r21^p - 1)
where Fs is the safety factor.
4. Compute GCI for coarse grid:
GCI_coarse = Fs * |e32/f2| / (r32^p - 1)
5. Check asymptotic ratio:
AR = GCI_coarse / (r21^p * GCI_fine)
If AR is approximately 1.0 (within 10%), the grids are in the asymptotic range and the GCI is reliable.
6. Richardson extrapolated value:
f_extrap = f1 + (f1 - f2) / (r21^p - 1)
Safety Factors
| Scenario | Safety Factor Fs | Rationale |
|---|---|---|
| 3+ grids with observed order | 1.25 | Order verified, lower uncertainty |
| 2 grids with assumed order | 3.0 | Order not verified, higher uncertainty |
| Oscillatory convergence | N/A | GCI not applicable |
The factor of 1.25 is analogous to a 95% confidence interval for well-behaved convergence data.
ASME V&V 20 Standard
The ASME V&V 20 standard (Verification and Validation in Computational Fluid Dynamics and Heat Transfer) recommends:
- Using at least 3 systematically refined grids
- Reporting GCI with the fine-grid solution
- Checking the asymptotic ratio
- Documenting the observed convergence order
- Reporting the Richardson-extrapolated value as the best estimate
Practical Guidelines
Grid Design
- Use constant refinement ratio across all directions
- Recommended ratio: r = 1.5 to 2.0
- Avoid r < 1.3 (differences may be in round-off noise)
- Avoid r > 3.0 (large jumps may skip pre-asymptotic behavior)
Common Issues
1. Observed order much higher than expected: Possible superconvergence or error cancellation. Verify with more grid levels.
2. Observed order much lower than expected: Solution may not be in the asymptotic range. Use finer grids.
3. Negative observed order: Solution is diverging. Check for coding errors, boundary condition issues, or inadequate resolution.
4. Oscillatory convergence: The GCI method does not apply. Consider using bounding approaches or the range of solutions as the uncertainty.
5. Very small GCI (< 0.1%): Solution may be grid-independent already, or the quantity of interest is insensitive to grid refinement.
Reporting
When reporting GCI results, include:
- The three grid spacings and corresponding solution values
- The observed convergence order
- The GCI value for the fine grid (as a percentage)
- The Richardson-extrapolated value
- The asymptotic ratio
- Whether the solution is in the asymptotic range
Example Report Format
Grid spacings: h1=0.01, h2=0.02, h3=0.04 Refinement ratio: r = 2.0 Solution values: f1=1.0008, f2=1.0032, f3=1.0128 Observed order: p = 2.00 GCI_fine = 0.027% Richardson extrapolated value: 1.00000 Asymptotic ratio: 1.000 Conclusion: Solution is in asymptotic range; GCI is reliable.
#!/usr/bin/env python3
"""Temporal convergence study via dt-refinement analysis."""
import argparse
import json
import math
import sys
def parse_args():
parser = argparse.ArgumentParser(
description="Analyze temporal convergence by computing observed order from timestep refinement data.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--timesteps", type=str, required=True,
help="Comma-separated timestep sizes (e.g. 0.04,0.02,0.01)",
)
parser.add_argument(
"--values", type=str, required=True,
help="Comma-separated solution values at each timestep level",
)
parser.add_argument(
"--expected-order", type=float, default=None,
help="Expected convergence order for assessment",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def compute_dt_refinement(timesteps, values, expected_order=None):
"""Compute temporal convergence order from timestep refinement data.
Parameters
----------
timesteps : list of float
Timestep sizes (must be positive).
values : list of float
Solution values corresponding to each timestep.
expected_order : float or None
Expected convergence order for assessment.
Returns
-------
dict
Results with observed_orders, mean_order, richardson_extrapolated_value,
in_asymptotic_range, convergence_assessment, and notes.
"""
if len(timesteps) != len(values):
raise ValueError("timesteps and values must have the same length")
if len(timesteps) < 2:
raise ValueError("At least 2 refinement levels required")
for t in timesteps:
if not math.isfinite(t) or t <= 0:
raise ValueError("All timesteps must be positive finite numbers")
for v in values:
if not math.isfinite(v):
raise ValueError("All values must be finite numbers")
# Sort by timestep descending (coarsest first)
paired = sorted(zip(timesteps, values), key=lambda x: -x[0])
timesteps_sorted = [p[0] for p in paired]
values_sorted = [p[1] for p in paired]
notes = []
observed_orders = []
# Compute observed orders from consecutive triplets using log-ratio
if len(timesteps_sorted) >= 3:
for i in range(len(timesteps_sorted) - 2):
dt_coarse = timesteps_sorted[i]
dt_mid = timesteps_sorted[i + 1]
dt_fine = timesteps_sorted[i + 2]
f_coarse = values_sorted[i]
f_mid = values_sorted[i + 1]
f_fine = values_sorted[i + 2]
e_coarse = abs(f_coarse - f_mid)
e_fine = abs(f_mid - f_fine)
if e_fine == 0 or e_coarse == 0:
notes.append(
"Zero error difference at levels %d-%d; "
"cannot compute order for this pair" % (i, i + 2)
)
continue
r_coarse = dt_coarse / dt_mid
if r_coarse <= 0:
continue
p = math.log(e_coarse / e_fine) / math.log(r_coarse)
observed_orders.append(p)
if p < 0:
notes.append(
"Negative observed order (%.2f) at levels %d-%d: "
"possible divergence" % (p, i, i + 2)
)
# Mean order
mean_order = None
if observed_orders:
mean_order = sum(observed_orders) / len(observed_orders)
# Check for pre-asymptotic behavior
in_asymptotic_range = True
if len(observed_orders) >= 2:
for i in range(len(observed_orders) - 1):
p1 = observed_orders[i]
p2 = observed_orders[i + 1]
avg = (abs(p1) + abs(p2)) / 2.0
if avg > 0 and abs(p1 - p2) / avg > 0.5:
in_asymptotic_range = False
notes.append("Pre-asymptotic behavior detected: observed order varies >50%% between consecutive pairs")
break
elif len(observed_orders) == 1:
in_asymptotic_range = True
else:
in_asymptotic_range = None
# Richardson extrapolation from finest two levels
richardson_extrapolated_value = None
dt_fine = timesteps_sorted[-1]
dt_next = timesteps_sorted[-2]
f_fine = values_sorted[-1]
f_next = values_sorted[-2]
r = dt_next / dt_fine
if mean_order is not None and mean_order > 0:
richardson_extrapolated_value = f_fine + (f_fine - f_next) / (r ** mean_order - 1)
elif expected_order is not None and expected_order > 0:
richardson_extrapolated_value = f_fine + (f_fine - f_next) / (r ** expected_order - 1)
notes.append("Richardson extrapolation used expected order (no observed order available)")
# Convergence assessment
convergence_assessment = "unknown"
if expected_order is not None and mean_order is not None:
if abs(mean_order - expected_order) / expected_order <= 0.1:
convergence_assessment = "PASS: observed order (%.2f) within 10%% of expected (%.2f)" % (
mean_order, expected_order,
)
else:
convergence_assessment = "FAIL: observed order (%.2f) differs from expected (%.2f) by >10%%" % (
mean_order, expected_order,
)
elif mean_order is not None:
convergence_assessment = "Observed order: %.2f (no expected order given for comparison)" % mean_order
elif len(timesteps_sorted) < 3:
convergence_assessment = "Insufficient levels to compute observed order (need >= 3)"
return {
"inputs": {
"timesteps": timesteps_sorted,
"values": values_sorted,
"expected_order": expected_order,
},
"results": {
"observed_orders": observed_orders,
"mean_order": mean_order,
"richardson_extrapolated_value": richardson_extrapolated_value,
"in_asymptotic_range": in_asymptotic_range,
"convergence_assessment": convergence_assessment,
"notes": notes,
},
}
def main():
args = parse_args()
try:
timesteps = [float(x) for x in args.timesteps.split(",")]
values = [float(x) for x in args.values.split(",")]
except ValueError:
print("Error: timesteps and values must be comma-separated numbers", file=sys.stderr)
sys.exit(2)
try:
result = compute_dt_refinement(timesteps, values, args.expected_order)
except ValueError as exc:
print("Error: %s" % exc, file=sys.stderr)
sys.exit(2)
if args.json:
print(json.dumps(result, indent=2))
else:
r = result["results"]
print("Temporal Convergence Study (dt-refinement)")
print(" levels: %d" % len(timesteps))
if r["observed_orders"]:
print(" observed orders: %s" % ", ".join("%.4f" % o for o in r["observed_orders"]))
if r["mean_order"] is not None:
print(" mean order: %.4f" % r["mean_order"])
if r["richardson_extrapolated_value"] is not None:
print(" Richardson extrapolated value: %.6g" % r["richardson_extrapolated_value"])
if r["in_asymptotic_range"] is not None:
print(" in asymptotic range: %s" % r["in_asymptotic_range"])
print(" assessment: %s" % r["convergence_assessment"])
for note in r["notes"]:
print(" note: %s" % note)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Grid Convergence Index (GCI) calculator following Roache's method."""
import argparse
import json
import math
import sys
def parse_args():
parser = argparse.ArgumentParser(
description="Compute Grid Convergence Index (GCI) for solution verification.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--spacings", type=str, required=True,
help="Comma-separated grid spacings for exactly 3 levels (e.g. 0.04,0.02,0.01)",
)
parser.add_argument(
"--values", type=str, required=True,
help="Comma-separated solution values at each spacing level",
)
parser.add_argument(
"--safety-factor", type=float, default=1.25,
help="Safety factor for GCI (default 1.25 for 3+ grids)",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def compute_gci(spacings, values, safety_factor=1.25):
"""Compute Grid Convergence Index.
Parameters
----------
spacings : list of float
Exactly 3 grid spacings (positive).
values : list of float
Solution values at each spacing level.
safety_factor : float
Safety factor (default 1.25).
Returns
-------
dict
Results with observed_order, gci_fine, gci_coarse, asymptotic_ratio,
in_asymptotic_range, refinement_ratio_21, refinement_ratio_32,
and extrapolated_value.
"""
if len(spacings) != 3 or len(values) != 3:
raise ValueError("Exactly 3 refinement levels required for GCI calculation")
if safety_factor <= 0:
raise ValueError("safety_factor must be positive")
for s in spacings:
if not math.isfinite(s) or s <= 0:
raise ValueError("All spacings must be positive finite numbers")
for v in values:
if not math.isfinite(v):
raise ValueError("All values must be finite numbers")
# Sort by spacing ascending: h1 (fine) < h2 (medium) < h3 (coarse)
paired = sorted(zip(spacings, values), key=lambda x: x[0])
h1, f1 = paired[0] # fine
h2, f2 = paired[1] # medium
h3, f3 = paired[2] # coarse
r21 = h2 / h1
r32 = h3 / h2
e21 = f2 - f1
e32 = f3 - f2
# Check for oscillatory convergence
if e21 != 0 and e32 != 0 and (e32 * e21) < 0:
raise ValueError("Oscillatory convergence detected")
# Check for identical solutions
if e21 == 0 and e32 == 0:
return {
"inputs": {
"spacings": [h1, h2, h3],
"values": [f1, f2, f3],
"safety_factor": safety_factor,
},
"results": {
"observed_order": None,
"gci_fine": 0.0,
"gci_coarse": 0.0,
"asymptotic_ratio": None,
"in_asymptotic_range": None,
"refinement_ratio_21": r21,
"refinement_ratio_32": r32,
"extrapolated_value": f1,
},
}
# Observed order
if e21 == 0:
raise ValueError("Fine and medium solutions are identical but coarse differs; cannot compute order")
if e32 == 0:
raise ValueError("Medium and coarse solutions are identical but fine differs; cannot compute order")
p = abs(math.log(abs(e32 / e21))) / math.log(r21)
# GCI fine and coarse
gci_fine = safety_factor * abs(e21 / f1) / (r21 ** p - 1) if f1 != 0 else float("inf")
gci_coarse = safety_factor * abs(e32 / f2) / (r32 ** p - 1) if f2 != 0 else float("inf")
# Asymptotic ratio
if gci_fine != 0 and math.isfinite(gci_fine) and math.isfinite(gci_coarse):
asymptotic_ratio = gci_coarse / (r21 ** p * gci_fine)
else:
asymptotic_ratio = None
in_asymptotic_range = None
if asymptotic_ratio is not None:
in_asymptotic_range = abs(asymptotic_ratio - 1.0) < 0.1
# Richardson extrapolated value
extrapolated_value = f1 + (f1 - f2) / (r21 ** p - 1)
return {
"inputs": {
"spacings": [h1, h2, h3],
"values": [f1, f2, f3],
"safety_factor": safety_factor,
},
"results": {
"observed_order": p,
"gci_fine": gci_fine,
"gci_coarse": gci_coarse,
"asymptotic_ratio": asymptotic_ratio,
"in_asymptotic_range": in_asymptotic_range,
"refinement_ratio_21": r21,
"refinement_ratio_32": r32,
"extrapolated_value": extrapolated_value,
},
}
def main():
args = parse_args()
try:
spacings = [float(x) for x in args.spacings.split(",")]
values = [float(x) for x in args.values.split(",")]
except ValueError:
print("Error: spacings and values must be comma-separated numbers", file=sys.stderr)
sys.exit(2)
try:
result = compute_gci(spacings, values, args.safety_factor)
except ValueError as exc:
print("Error: %s" % exc, file=sys.stderr)
sys.exit(2)
if args.json:
print(json.dumps(result, indent=2))
else:
r = result["results"]
print("Grid Convergence Index (GCI)")
print(" refinement ratio r21: %.4f" % r["refinement_ratio_21"])
print(" refinement ratio r32: %.4f" % r["refinement_ratio_32"])
if r["observed_order"] is not None:
print(" observed order: %.4f" % r["observed_order"])
print(" GCI fine: %.6g" % r["gci_fine"])
print(" GCI coarse: %.6g" % r["gci_coarse"])
if r["asymptotic_ratio"] is not None:
print(" asymptotic ratio: %.4f" % r["asymptotic_ratio"])
if r["in_asymptotic_range"] is not None:
print(" in asymptotic range: %s" % r["in_asymptotic_range"])
print(" extrapolated value: %.6g" % r["extrapolated_value"])
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Spatial convergence study via h-refinement analysis."""
import argparse
import json
import math
import sys
def parse_args():
parser = argparse.ArgumentParser(
description="Analyze spatial convergence by computing observed order from grid refinement data.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--spacings", type=str, required=True,
help="Comma-separated grid spacings (e.g. 0.4,0.2,0.1)",
)
parser.add_argument(
"--values", type=str, required=True,
help="Comma-separated solution values at each spacing level",
)
parser.add_argument(
"--expected-order", type=float, default=None,
help="Expected convergence order for assessment",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def compute_h_refinement(spacings, values, expected_order=None):
"""Compute spatial convergence order from grid refinement data.
Parameters
----------
spacings : list of float
Grid spacings (must be positive).
values : list of float
Solution values corresponding to each spacing.
expected_order : float or None
Expected convergence order for assessment.
Returns
-------
dict
Results with observed_orders, mean_order, richardson_extrapolated_value,
in_asymptotic_range, convergence_assessment, and notes.
"""
if len(spacings) != len(values):
raise ValueError("spacings and values must have the same length")
if len(spacings) < 2:
raise ValueError("At least 2 refinement levels required")
for s in spacings:
if not math.isfinite(s) or s <= 0:
raise ValueError("All spacings must be positive finite numbers")
for v in values:
if not math.isfinite(v):
raise ValueError("All values must be finite numbers")
# Sort by spacing descending (coarsest first)
paired = sorted(zip(spacings, values), key=lambda x: -x[0])
spacings_sorted = [p[0] for p in paired]
values_sorted = [p[1] for p in paired]
notes = []
observed_orders = []
# Compute observed orders from consecutive triplets using log-ratio
if len(spacings_sorted) >= 3:
for i in range(len(spacings_sorted) - 2):
h_coarse = spacings_sorted[i]
h_mid = spacings_sorted[i + 1]
h_fine = spacings_sorted[i + 2]
f_coarse = values_sorted[i]
f_mid = values_sorted[i + 1]
f_fine = values_sorted[i + 2]
e_coarse = abs(f_coarse - f_mid)
e_fine = abs(f_mid - f_fine)
if e_fine == 0 or e_coarse == 0:
notes.append(
"Zero error difference at levels %d-%d; "
"cannot compute order for this pair" % (i, i + 2)
)
continue
r_coarse = h_coarse / h_mid
r_fine = h_mid / h_fine
if r_coarse <= 0 or r_fine <= 0:
continue
p = math.log(e_coarse / e_fine) / math.log(r_coarse)
observed_orders.append(p)
if p < 0:
notes.append(
"Negative observed order (%.2f) at levels %d-%d: "
"possible divergence" % (p, i, i + 2)
)
# Mean order
mean_order = None
if observed_orders:
mean_order = sum(observed_orders) / len(observed_orders)
# Check for pre-asymptotic behavior
in_asymptotic_range = True
if len(observed_orders) >= 2:
for i in range(len(observed_orders) - 1):
p1 = observed_orders[i]
p2 = observed_orders[i + 1]
avg = (abs(p1) + abs(p2)) / 2.0
if avg > 0 and abs(p1 - p2) / avg > 0.5:
in_asymptotic_range = False
notes.append("Pre-asymptotic behavior detected: observed order varies >50%% between consecutive pairs")
break
elif len(observed_orders) == 1:
in_asymptotic_range = True
else:
in_asymptotic_range = None
# Richardson extrapolation from finest two levels
richardson_extrapolated_value = None
h_fine = spacings_sorted[-1]
h_next = spacings_sorted[-2]
f_fine = values_sorted[-1]
f_next = values_sorted[-2]
r = h_next / h_fine
if mean_order is not None and mean_order > 0:
richardson_extrapolated_value = f_fine + (f_fine - f_next) / (r ** mean_order - 1)
elif expected_order is not None and expected_order > 0:
richardson_extrapolated_value = f_fine + (f_fine - f_next) / (r ** expected_order - 1)
notes.append("Richardson extrapolation used expected order (no observed order available)")
# Convergence assessment
convergence_assessment = "unknown"
if expected_order is not None and mean_order is not None:
if abs(mean_order - expected_order) / expected_order <= 0.1:
convergence_assessment = "PASS: observed order (%.2f) within 10%% of expected (%.2f)" % (
mean_order, expected_order,
)
else:
convergence_assessment = "FAIL: observed order (%.2f) differs from expected (%.2f) by >10%%" % (
mean_order, expected_order,
)
elif mean_order is not None:
convergence_assessment = "Observed order: %.2f (no expected order given for comparison)" % mean_order
elif len(spacings_sorted) < 3:
convergence_assessment = "Insufficient levels to compute observed order (need >= 3)"
return {
"inputs": {
"spacings": spacings_sorted,
"values": values_sorted,
"expected_order": expected_order,
},
"results": {
"observed_orders": observed_orders,
"mean_order": mean_order,
"richardson_extrapolated_value": richardson_extrapolated_value,
"in_asymptotic_range": in_asymptotic_range,
"convergence_assessment": convergence_assessment,
"notes": notes,
},
}
def main():
args = parse_args()
try:
spacings = [float(x) for x in args.spacings.split(",")]
values = [float(x) for x in args.values.split(",")]
except ValueError:
print("Error: spacings and values must be comma-separated numbers", file=sys.stderr)
sys.exit(2)
try:
result = compute_h_refinement(spacings, values, args.expected_order)
except ValueError as exc:
print("Error: %s" % exc, file=sys.stderr)
sys.exit(2)
if args.json:
print(json.dumps(result, indent=2))
else:
r = result["results"]
print("Spatial Convergence Study (h-refinement)")
print(" levels: %d" % len(spacings))
if r["observed_orders"]:
print(" observed orders: %s" % ", ".join("%.4f" % o for o in r["observed_orders"]))
if r["mean_order"] is not None:
print(" mean order: %.4f" % r["mean_order"])
if r["richardson_extrapolated_value"] is not None:
print(" Richardson extrapolated value: %.6g" % r["richardson_extrapolated_value"])
if r["in_asymptotic_range"] is not None:
print(" in asymptotic range: %s" % r["in_asymptotic_range"])
print(" assessment: %s" % r["convergence_assessment"])
for note in r["notes"]:
print(" note: %s" % note)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Richardson extrapolation for solution verification."""
import argparse
import json
import math
import sys
def parse_args():
parser = argparse.ArgumentParser(
description="Perform Richardson extrapolation to estimate grid/timestep-independent solution.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--spacings", type=str, required=True,
help="Comma-separated spacings (e.g. 0.02,0.01)",
)
parser.add_argument(
"--values", type=str, required=True,
help="Comma-separated solution values at each spacing level",
)
parser.add_argument(
"--order", type=float, required=True,
help="Assumed convergence order",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def compute_richardson_extrapolation(spacings, values, order):
"""Perform Richardson extrapolation.
Parameters
----------
spacings : list of float
Grid spacings or timestep sizes (positive).
values : list of float
Solution values at each level.
order : float
Assumed convergence order (positive).
Returns
-------
dict
Results with extrapolated_value, error_estimate, relative_error_estimate,
and optionally observed_order and order_consistent.
"""
if len(spacings) != len(values):
raise ValueError("spacings and values must have the same length")
if len(spacings) < 2:
raise ValueError("At least 2 refinement levels required")
if order <= 0:
raise ValueError("order must be positive")
for s in spacings:
if not math.isfinite(s) or s <= 0:
raise ValueError("All spacings must be positive finite numbers")
for v in values:
if not math.isfinite(v):
raise ValueError("All values must be finite numbers")
# Sort by spacing descending (coarsest first)
paired = sorted(zip(spacings, values), key=lambda x: -x[0])
spacings_sorted = [p[0] for p in paired]
values_sorted = [p[1] for p in paired]
# Richardson extrapolation from finest two levels
h_fine = spacings_sorted[-1]
h_coarse = spacings_sorted[-2]
f_fine = values_sorted[-1]
f_coarse = values_sorted[-2]
r = h_coarse / h_fine
extrapolated_value = f_fine + (f_fine - f_coarse) / (r ** order - 1)
error_estimate = abs(f_fine - f_coarse) / (r ** order - 1)
relative_error_estimate = error_estimate / abs(extrapolated_value) if extrapolated_value != 0 else float("inf")
result = {
"extrapolated_value": extrapolated_value,
"error_estimate": error_estimate,
"relative_error_estimate": relative_error_estimate,
}
# With 3+ levels, compute observed order and check consistency
if len(spacings_sorted) >= 3:
h1 = spacings_sorted[-1] # finest
h2 = spacings_sorted[-2]
h3 = spacings_sorted[-3] # coarsest of three
f1 = values_sorted[-1]
f2 = values_sorted[-2]
f3 = values_sorted[-3]
e_fine = abs(f2 - f1)
e_coarse = abs(f3 - f2)
if e_fine > 0 and e_coarse > 0:
r_ratio = h2 / h1
observed_order = math.log(e_coarse / e_fine) / math.log(r_ratio)
result["observed_order"] = observed_order
result["order_consistent"] = abs(observed_order - order) / order <= 0.1
else:
result["observed_order"] = None
result["order_consistent"] = None
return {
"inputs": {
"spacings": spacings_sorted,
"values": values_sorted,
"order": order,
},
"results": result,
}
def main():
args = parse_args()
try:
spacings = [float(x) for x in args.spacings.split(",")]
values = [float(x) for x in args.values.split(",")]
except ValueError:
print("Error: spacings and values must be comma-separated numbers", file=sys.stderr)
sys.exit(2)
try:
result = compute_richardson_extrapolation(spacings, values, args.order)
except ValueError as exc:
print("Error: %s" % exc, file=sys.stderr)
sys.exit(2)
if args.json:
print(json.dumps(result, indent=2))
else:
r = result["results"]
print("Richardson Extrapolation")
print(" assumed order: %.2f" % args.order)
print(" extrapolated value: %.6g" % r["extrapolated_value"])
print(" error estimate: %.6g" % r["error_estimate"])
print(" relative error estimate: %.6g" % r["relative_error_estimate"])
if "observed_order" in r and r["observed_order"] is not None:
print(" observed order: %.4f" % r["observed_order"])
print(" order consistent: %s" % r["order_consistent"])
if __name__ == "__main__":
main()
Related skills
FAQ
How many refinement levels are needed?
At least 3 levels for h/dt refinement; GCI requires exactly 3 (fine/medium/coarse), and 2 levels can only use Richardson extrapolation with an assumed order.
What do the scripts output?
Structured JSON with fields like observed_orders, mean_order, richardson_extrapolated_value, gci_fine, gci_coarse, and asymptotic_ratio.