test: integrate shell thermal physics verification

This commit is contained in:
KOKO\Mimi
2026-10-03 23:44:38 +09:00
8 changed files with 2684 additions and 0 deletions
@@ -0,0 +1,70 @@
// Generated by thermal_oracle_generator.py; exact root/weight certificates.
#ifndef FESA_TESTS_THERMAL_GAUSS_ORACLE_H_
#define FESA_TESTS_THERMAL_GAUSS_ORACLE_H_
#include <array>
namespace thermal_oracle {
struct GaussPoint {
double x_lo, x_hi, w_lo, w_hi;
};
inline constexpr std::array<GaussPoint, 12> kGauss12{{
{-0x1.f68f1d8e42e82p-1, -0x1.f68f1d8e42e80p-1, 0x1.8275d9de72d45p-5,
0x1.8275d9dedaddep-5},
{-0x1.cee874ffb88b5p-1, -0x1.cee874ffb88b3p-1, 0x1.b60602bcd3d56p-4,
0x1.b60602bcf85a4p-4},
{-0x1.8a30aeed88f37p-1, -0x1.8a30aeed88f35p-1, 0x1.47d7258f1fdcdp-3,
0x1.47d7258f25d51p-3},
{-0x1.2cb4f05c077fap-1, -0x1.2cb4f05c077f8p-1, 0x1.a0163e6b1a279p-3,
0x1.a0163e6b1b46ap-3},
{-0x1.78a8d20a8b19ep-2, -0x1.78a8d20a8b19cp-2, 0x1.de3155c256a18p-3,
0x1.de3155c256b52p-3},
{-0x1.007a5f8f630e5p-3, -0x1.007a5f8f630e3p-3, 0x1.fe40ce6d4f01bp-3,
0x1.fe40ce6d4f030p-3},
{0x1.007a5f8f630e3p-3, 0x1.007a5f8f630e5p-3, 0x1.fe40ce6d4f01bp-3,
0x1.fe40ce6d4f030p-3},
{0x1.78a8d20a8b19cp-2, 0x1.78a8d20a8b19ep-2, 0x1.de3155c256a18p-3,
0x1.de3155c256b52p-3},
{0x1.2cb4f05c077f8p-1, 0x1.2cb4f05c077fap-1, 0x1.a0163e6b1a279p-3,
0x1.a0163e6b1b46ap-3},
{0x1.8a30aeed88f35p-1, 0x1.8a30aeed88f37p-1, 0x1.47d7258f1fdcdp-3,
0x1.47d7258f25d51p-3},
{0x1.cee874ffb88b3p-1, 0x1.cee874ffb88b5p-1, 0x1.b60602bcd3d56p-4,
0x1.b60602bcf85a4p-4},
{0x1.f68f1d8e42e80p-1, 0x1.f68f1d8e42e82p-1, 0x1.8275d9de72d45p-5,
0x1.8275d9dedaddep-5},
}};
inline constexpr std::array<GaussPoint, 16> kGauss16{{
{-0x1.fa92c264d787fp-1, -0x1.fa92c264d787dp-1, 0x1.bcddab41dec5ap-6,
0x1.bcddab55197bep-6},
{-0x1.e39f56616f9b1p-1, -0x1.e39f56616f9afp-1, 0x1.fdfb1a27efbd3p-5,
0x1.fdfb1a30350e0p-5},
{-0x1.bb3403514e484p-1, -0x1.bb3403514e482p-1, 0x1.85c4ee78dfd1dp-4,
0x1.85c4ee7ab8792p-4},
{-0x1.82c45dda4726cp-1, -0x1.82c45dda4726ap-1, 0x1.fe7af2ba91fc7p-4,
0x1.fe7af2bb1510fp-4},
{-0x1.3c5a466d5e8b9p-1, -0x1.3c5a466d5e8b7p-1, 0x1.325f61bc9dd8dp-3,
0x1.325f61bca9bf1p-3},
{-0x1.d50259a43a773p-2, -0x1.d50259a43a771p-2, 0x1.5a6ebbb5a6fa2p-3,
0x1.5a6ebbb5a7c56p-3},
{-0x1.205cae642337dp-2, -0x1.205cae642337bp-2, 0x1.75f8c77e0bf66p-3,
0x1.75f8c77e0c0b8p-3},
{-0x1.852bd6676a9fap-4, -0x1.852bd6676a9f8p-4, 0x1.83feae80e4df6p-3,
0x1.83feae80e4e01p-3},
{0x1.852bd6676a9f8p-4, 0x1.852bd6676a9fap-4, 0x1.83feae80e4df6p-3,
0x1.83feae80e4e01p-3},
{0x1.205cae642337bp-2, 0x1.205cae642337dp-2, 0x1.75f8c77e0bf66p-3,
0x1.75f8c77e0c0b8p-3},
{0x1.d50259a43a771p-2, 0x1.d50259a43a773p-2, 0x1.5a6ebbb5a6fa2p-3,
0x1.5a6ebbb5a7c56p-3},
{0x1.3c5a466d5e8b7p-1, 0x1.3c5a466d5e8b9p-1, 0x1.325f61bc9dd8dp-3,
0x1.325f61bca9bf1p-3},
{0x1.82c45dda4726ap-1, 0x1.82c45dda4726cp-1, 0x1.fe7af2ba91fc7p-4,
0x1.fe7af2bb1510fp-4},
{0x1.bb3403514e482p-1, 0x1.bb3403514e484p-1, 0x1.85c4ee78dfd1dp-4,
0x1.85c4ee7ab8792p-4},
{0x1.e39f56616f9afp-1, 0x1.e39f56616f9b1p-1, 0x1.fdfb1a27efbd3p-5,
0x1.fdfb1a30350e0p-5},
{0x1.fa92c264d787dp-1, 0x1.fa92c264d787fp-1, 0x1.bcddab41dec5ap-6,
0x1.bcddab55197bep-6},
}};
} // namespace thermal_oracle
#endif
@@ -0,0 +1,244 @@
"""Generate test-only certified Gauss brackets from exact rational arithmetic.
Newton/Decimal produces candidates only. Exact Fraction signs prove n distinct
Legendre roots; rational interval recurrence encloses the associated weights.
No runtime solver dependency, reference modification, or libm ULP premise.
"""
from decimal import Decimal, localcontext
from fractions import Fraction as F
from pathlib import Path
import math
import re
import json
def add(a, b):
return a[0] + b[0], a[1] + b[1]
def neg(a):
return -a[1], -a[0]
def mul(a, b):
v = [x * y for x in a for y in b]
return min(v), max(v)
def div(a, b):
assert b[0] > 0 or b[1] < 0
return mul(a, (1 / b[1], 1 / b[0]))
def scale(a, b):
return mul(a, (F(b), F(b)))
def legendre(n, x):
p, q = (F(1), F(1)), x
for k in range(2, n + 1):
p, q = q, scale(add(scale(mul(x, q), 2 * k - 1), neg(scale(p, k - 1))), F(1, k))
derivative = div(scale(add(mul(x, q), neg(p)), n), add(mul(x, x), (-F(1), -F(1))))
return q, derivative
def bracket(value):
return math.nextafter(float(value[0]), -math.inf), math.nextafter(float(value[1]), math.inf)
def rule(n):
result = []
for i in range(n):
with localcontext() as context:
context.prec = 90
z = Decimal(str(math.cos(math.pi * (i + .75) / (n + .5))))
for _ in range(30):
p, q = Decimal(1), z
for k in range(2, n + 1):
p, q = q, ((2 * k - 1) * z * q - (k - 1) * p) / k
d = n * (z * q - p) / (z * z - 1)
z -= q / d
lo, hi = bracket((F(z), F(z)))
pl = legendre(n, (F(lo), F(lo)))[0][0]
ph = legendre(n, (F(hi), F(hi)))[0][0]
assert pl * ph < 0
x = F(lo), F(hi)
derivative = legendre(n, x)[1]
w = div((F(2), F(2)), mul(add((F(1), F(1)), neg(mul(x, x))), mul(derivative, derivative)))
result.append((lo, hi, *bracket(w)))
result.sort()
assert all(a[1] < b[0] for a, b in zip(result, result[1:]))
assert sum(F(r[2]) for r in result) < 2 < sum(F(r[3]) for r in result)
return result
def slab_fixtures():
# Approved Appendix A supplies the independently implemented Cholesky /
# Jacobi spectral candidates; Appendix E supplies certified transcendentals.
document = Path(__file__).resolve().parents[3] / "docs/shell-transient-heat-transfer/numerical-oracle-scratch.md"
blocks = re.findall(r"```python\n(.*?)```", document.read_text(encoding="utf-8-sig"), re.S)
spectral = {}
exec(blocks[0].split('out={"lambda"')[0], spectral)
interval_scope = {}
exec(blocks[-1].split("\nfor t in [.001")[0], interval_scope)
iv, ia, im, isc = (interval_scope[k] for k in ("interval", "add", "mul", "scale"))
ex = interval_scope["exneg"]
n = 4
def exact_inverse(matrix):
a = [list(row) + [F(i == j) for j in range(n)] for i, row in enumerate(matrix)]
for k in range(n):
pivot = next(i for i in range(k, n) if a[i][k])
a[k], a[pivot] = a[pivot], a[k]
divisor = a[k][k]
a[k] = [x / divisor for x in a[k]]
for i in range(n):
if i != k:
factor = a[i][k]
a[i] = [x - factor * y for x, y in zip(a[i], a[k])]
return [row[n:] for row in a]
def exact_mm(a, b):
return [[sum((x*y for x, y in zip(row, col)), F(0)) for col in zip(*b)] for row in a]
def matrix_multiply(a, b):
return [[sum_intervals([im(x, y) for x, y in zip(row, col)]) for col in zip(*b)] for row in a]
def sum_intervals(values):
total = iv(0)
for v in values:
total = ia(total, v)
return total
def apply(a, v):
return [sum_intervals([im(x, y) for x, y in zip(row, v)]) for row in a]
def power(a, count):
result = [[iv(i == j) for j in range(n)] for i in range(n)]
while count:
if count & 1:
result = matrix_multiply(result, a)
count >>= 1
if count:
a = matrix_multiply(a, a)
return result
c = [[F(0) for _ in range(5)] for _ in range(5)]
k = [[F(0) for _ in range(5)] for _ in range(5)]
for offset in (0, 2):
for i in range(3):
for j in range(3):
c[offset+i][offset+j] += F([[4, 2, -1], [2, 16, 2], [-1, 2, 4]][i][j], 60)
k[offset+i][offset+j] += F(2, 3)*[[7, -8, 1], [-8, 16, -8], [1, -8, 7]][i][j]
cf, kf = [r[1:] for r in c[1:]], [r[1:] for r in k[1:]]
inverse = exact_inverse(cf)
d = exact_mm(inverse, kf)
y = [-1-sum(inverse[i][j]*c[j+1][0] for j in range(n)) for i in range(n)]
def matrix_exponential(t):
# An independent enclosure of exp(-Cff^-1 Kff t) certifies all spectral
# eigenpair/coefficient errors together, rather than assuming libm ULPs.
# For ||B||inf<=1/2, the 64-term exponential remainder is bounded by
# 2*(1/2)^65/65!. Entrywise intervals preserve subsequent squarings.
magnitude = max(sum(abs(x*t) for x in row) for row in d)
shift = 0
while magnitude > F(1, 2):
magnitude /= 2
shift += 1
a = [[iv(-x*t/F(2)**shift) for x in row] for row in d]
term = [[iv(i == j) for j in range(n)] for i in range(n)]
result = [row[:] for row in term]
for order in range(1, 65):
term = [[isc(x, F(1, order)) for x in row] for row in matrix_multiply(term, a)]
result = [[ia(x, z) for x, z in zip(row, terms)] for row, terms in zip(result, term)]
remainder = 2*F(1, 2)**65/math.factorial(65)
result = [[ia(x, iv(-remainder, remainder)) for x in row] for row in result]
for _ in range(shift):
result = matrix_multiply(result, result)
return result
def outputs(error):
temperature = [iv(1)] + [ia(iv(1), z) for z in error]
derivative = [iv(0)] + apply([[iv(-x) for x in row] for row in d], error)
flux = []
for offset, s in ((0, -1), (0, 0), (0, 1), (2, -1), (2, 0), (2, 1)):
deriv = [F(4)*F(s-F(1, 2)), F(-8*s), F(4)*F(s+F(1, 2))]
flux.append(sum_intervals([isc(temperature[offset+i], -deriv[i]) for i in range(3)]))
energy = sum_intervals([isc(temperature[j], sum(c[i][j] for i in range(5))) for j in range(5)])
reaction = sum_intervals([isc(temperature[j], k[0][j]) for j in range(5)] +
[isc(derivative[j], c[0][j]) for j in range(5)])
return temperature + flux + [energy, reaction]
def spectral_candidate(t, count):
amplitudes = []
for eigenvalue, coefficient in zip(spectral["lam"], spectral["coef"]):
decay = ex(iv(F(eigenvalue)*t)) if count is None else power_scalar(
interval_scope["div"](iv(1), iv(1+F(eigenvalue)*t/count)), count)
amplitudes.append(isc(decay, F(coefficient)))
return outputs(apply([[iv(F(x)) for x in row] for row in spectral["W"]], amplitudes))
def power_scalar(x, count):
result = iv(1)
while count:
if count & 1:
result = im(result, x)
count >>= 1
if count:
x = im(x, x)
return result
table = [[.141, 10.7, .0304, 10.9], [.0153, 1.28, .00468, .824],
[.00598, .253, .00130, .00632], [.00139, .136, .000666, .00953],
[.000142, .0284, .0000945, .000526], [.0000414, .00816, .0000159, .0000479]]
fixtures, metrics = [], []
for time, space in zip((.001, .01, .05, .1, .5, 1.), table):
t = F(time)
temperatures, flux, energy, _, _ = interval_scope["series_interval"](time)
continuum = temperatures + [flux[i] for i in (0, 1, 2, 2, 3, 4)] + [energy, flux[0]]
solutions = []
spectral_enclosure = 0.
for count in (None, 4096, 8192):
if count is None:
operator = matrix_exponential(t)
else:
a = [[cf[i][j]+t/count*kf[i][j] for j in range(n)] for i in range(n)]
operator = power([[iv(x) for x in row] for row in exact_mm(exact_inverse(a), cf)], count)
exact = outputs(apply(operator, [iv(x) for x in y]))
candidate = spectral_candidate(t, count)
enclosed = [(min(a[0], b[0]), max(a[1], b[1])) for a, b in zip(exact, candidate)]
spectral_enclosure = max(spectral_enclosure, max(float(hi-lo) for lo, hi in enclosed))
solutions.append(enclosed)
fixtures.append((time, space, continuum, *solutions))
metrics.append({"time": time, "spectral_and_matrix_function_enclosure": spectral_enclosure})
print(json.dumps({"eigenvalues": spectral["lam"], "slab_oracle_metrics": metrics}, indent=2))
return fixtures
if __name__ == "__main__":
output = ["// Generated by thermal_oracle_generator.py; exact root/weight certificates.",
"#ifndef FESA_TESTS_THERMAL_GAUSS_ORACLE_H_", "#define FESA_TESTS_THERMAL_GAUSS_ORACLE_H_",
"#include <array>", "namespace thermal_oracle {",
"struct GaussPoint { double x_lo,x_hi,w_lo,w_hi; };"]
for n in (12, 16):
rows = rule(n)
output.append(f"inline constexpr std::array<GaussPoint,{n}> kGauss{n}{{{{")
output.extend(" {" + ",".join(x.hex() for x in row) + "}," for row in rows)
output.append("}};")
output.extend(["}", "#endif", ""])
destination = Path(__file__).with_name("thermal_gauss_oracle.h")
destination.write_text("\n".join(output), encoding="utf-8")
print("Certified Legendre roots/weights: 12 + 16 distinct roots;", destination.name)
fixtures = slab_fixtures()
output = ["// Generated by thermal_oracle_generator.py. Outward binary64 brackets.",
"#ifndef FESA_TESTS_THERMAL_SLAB_ORACLE_H_", "#define FESA_TESTS_THERMAL_SLAB_ORACLE_H_",
"#include <array>", "namespace thermal_oracle {",
"struct Bracket { double lo,hi; };",
"struct SlabCase { double time; std::array<double,4> space; std::array<Bracket,13> fourier,continuous,be4096,be8192; };",
"inline constexpr std::array<SlabCase,6> kSlab{{"]
for time, space, *values in fixtures:
output.append("{" + time.hex() + ",{{" + ",".join(x.hex() for x in space) + "}},")
for rows in values:
output.append("{{" + ",".join("{" + ",".join(x.hex() for x in bracket(row)) + "}" for row in rows) + "}},")
output.append("},")
output.extend(["}};", "}", "#endif", ""])
Path(__file__).with_name("thermal_slab_oracle.h").write_text("\n".join(output), encoding="utf-8")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,359 @@
// Generated by thermal_oracle_generator.py. Outward binary64 brackets.
#ifndef FESA_TESTS_THERMAL_SLAB_ORACLE_H_
#define FESA_TESTS_THERMAL_SLAB_ORACLE_H_
#include <array>
namespace thermal_oracle {
struct Bracket {
double lo, hi;
};
struct SlabCase {
double time;
std::array<double, 4> space;
std::array<Bracket, 13> fourier, continuous, be4096, be8192;
};
inline constexpr std::array<SlabCase, 6> kSlab{{
{
0x1.0624dd2f1a9fcp-10,
{{0x1.20c49ba5e353fp-3, 0x1.5666666666666p+3, 0x1.f212d77318fc5p-6,
0x1.5cccccccccccdp+3}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.85b89453c09acp-26, 0x1.85b89453c09aep-26},
{0x1.fe4543738e03ap-95, 0x1.0ff4c2f026ce0p-94},
{-0x1.64845561fdc38p-101, 0x1.c12311737a124p-99},
{-0x1.68eca14e83537p-101, 0x1.c008fe7858ae5p-99},
{0x1.1d75b94accf81p+4, 0x1.1d75b94accf83p+4},
{0x1.88168e094c2d6p-19, 0x1.88168e094c2d8p-19},
{0x1.e433884766565p-87, 0x1.02b7b8a2bf31ap-86},
{0x1.e433884766565p-87, 0x1.02b7b8a2bf31ap-86},
{-0x1.3a94ff682132fp-91, 0x1.b25320f2bf337p-92},
{-0x1.09df47f0c0666p-91, 0x1.09df47f0c0666p-91},
{0x1.244f96d3c38d6p-5, 0x1.244f96d3c38d8p-5},
{0x1.1d75b94accf81p+4, 0x1.1d75b94accf83p+4}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{-0x1.d37c7f414f866p-4, -0x1.d37c7f414f859p-4},
{0x1.1eb81bc756156p-3, 0x1.1eb81bc75615ap-3},
{-0x1.17e5fa341d20bp-6, -0x1.17e5fa341d110p-6},
{0x1.2d98feedf9757p-5, 0x1.2d98feedf9788p-5},
{0x1.cc5b11a49f520p+2, 0x1.cc5b11a49f523p+2},
{0x1.b851f90e2a7a9p+0, 0x1.b851f90e2a7abp+0},
{-0x1.e0642a3b1429cp+1, -0x1.e0642a3b14296p+1},
{0x1.0ce0640ae3c9bp+0, 0x1.0ce0640ae3cbbp+0},
{0x1.a6a3b817af6eap-3, 0x1.a6a3b817af708p-3},
{-0x1.466eec09efdf1p-1, -0x1.466eec09efdc2p-1},
{0x1.0e51bf4794d1ep-4, 0x1.0e51bf4794d3ap-4},
{0x1.be34bece7dbcbp+2, 0x1.be34bece7dbd0p+2}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{-0x1.d37cd7bcda5f0p-4, -0x1.d37cd7bcda5e4p-4},
{0x1.1eb8818ac9073p-3, 0x1.1eb8818ac9077p-3},
{-0x1.17e77bedcde83p-6, -0x1.17e77bedcdd88p-6},
{0x1.2d9a6a607059ep-5, 0x1.2d9a6a60705d0p-5},
{0x1.cc5b231047dc3p+2, 0x1.cc5b231047dc6p+2},
{0x1.b851df9d4dbe2p+0, 0x1.b851df9d4dbe4p+0},
{-0x1.e064668341fa9p+1, -0x1.e064668341fa4p+1},
{0x1.0ce0f74bd7864p+0, 0x1.0ce0f74bd7884p+0},
{0x1.a6a3cde559e00p-3, 0x1.a6a3cde559e1dp-3},
{-0x1.467007a5021f8p-1, -0x1.467007a5021c9p-1},
{0x1.0e51b2b4d8688p-4, 0x1.0e51b2b4d86a3p-4},
{0x1.be34d80c5223ap+2, 0x1.be34d80c5223fp+2}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{-0x1.d37cab7f333f2p-4, -0x1.d37cab7f333e5p-4},
{0x1.1eb84ea93c6aep-3, 0x1.1eb84ea93c6b2p-3},
{-0x1.17e6bb11c7d2ep-6, -0x1.17e6bb11c7c33p-6},
{0x1.2d99b4a80421ep-5, 0x1.2d99b4a80424fp-5},
{0x1.cc5b1a5a7a2e7p+2, 0x1.cc5b1a5a7a2eap+2},
{0x1.b851ec55b0e53p+0, 0x1.b851ec55b0e55p+0},
{-0x1.e064485f43780p+1, -0x1.e064485f4377ap+1},
{0x1.0ce0adaba68aep+0, 0x1.0ce0adaba68cdp+0},
{0x1.a6a3c2fe76c36p-3, 0x1.a6a3c2fe76c53p-3},
{-0x1.466f79d811b70p-1, -0x1.466f79d811b41p-1},
{0x1.0e51b8fe3293cp-4, 0x1.0e51b8fe32958p-4},
{0x1.be34cb6d72726p+2, 0x1.be34cb6d7272bp+2}}},
},
{
0x1.47ae147ae147bp-7,
{{0x1.f559b3d07c84bp-7, 0x1.47ae147ae147bp+0, 0x1.32b55ef1fddecp-8,
0x1.a5e353f7ced91p-1}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.3bcd133aa0ffbp-4, 0x1.3bcd133aa0ffdp-4},
{0x1.aab859b20ac9ep-12, 0x1.aab859b20aca0p-12},
{0x1.e87470e505fc1p-24, 0x1.e87470e505fc3p-24},
{0x1.b0c1a759f773cp-39, 0x1.b0c1a759f773ep-39},
{0x1.6914d24534247p+2, 0x1.6914d24534249p+2},
{0x1.2ebf3dcc9f22ep+0, 0x1.2ebf3dcc9f230p+0},
{0x1.64e3dcd3af4bap-7, 0x1.64e3dcd3af4bcp-7},
{0x1.64e3dcd3af4bap-7, 0x1.64e3dcd3af4bcp-7},
{0x1.27c272dde935ep-18, 0x1.27c272dde9360p-18},
{-0x1.64c3500000001p-300, 0x1.64c3500000001p-300},
{0x1.ce2f21a042be1p-4, 0x1.ce2f21a042be3p-4},
{0x1.6914d24534247p+2, 0x1.6914d24534249p+2}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.7a435a2984e32p-4, 0x1.7a435a2984e3ap-4},
{0x1.9dc400057caf6p-12, 0x1.9dc400057d765p-12},
{0x1.ad9feeec8c48dp-7, 0x1.ad9feeec8c649p-7},
{-0x1.986d06319bd56p-7, -0x1.986d06319bcd8p-7},
{0x1.50c482dacf8f7p+2, 0x1.50c482dacf8f9p+2},
{0x1.ffcc477fff504p+0, 0x1.ffcc477fff508p+0},
{-0x1.43797c6b3f9d5p+0, -0x1.43797c6b3f9d2p+0},
{-0x1.05044c3c6933ap-3, -0x1.05044c3c69290p-3},
{0x1.a55b2631c7b93p-6, 0x1.a55b2631c7baep-6},
{0x1.6e5b15c8db17bp-3, 0x1.6e5b15c8db21fp-3},
{0x1.e15645f1d96e9p-4, 0x1.e15645f1d9703p-4},
{0x1.345d88b145b6ap+2, 0x1.345d88b145b6ep+2}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.7a374829b1404p-4, 0x1.7a374829b140cp-4},
{0x1.af3900931af0ap-12, 0x1.af3900931bb78p-12},
{0x1.ad81aefddf88dp-7, 0x1.ad81aefddfa49p-7},
{-0x1.983e6ad119e83p-7, -0x1.983e6ad119e05p-7},
{0x1.50c690c2ce70cp+2, 0x1.50c690c2ce70ep+2},
{0x1.ffca18dfed9c8p+0, 0x1.ffca18dfed9cbp+0},
{-0x1.4386114b5e8a2p+0, -0x1.4386114b5e89ep+0},
{-0x1.04baf9d759bb3p-3, -0x1.04baf9d759b09p-3},
{0x1.a5b832d5b2be1p-6, 0x1.a5b832d5b2bfcp-6},
{0x1.6e29068cc6608p-3, 0x1.6e29068cc66acp-3},
{0x1.e154626664a52p-4, 0x1.e154626664a6cp-4},
{0x1.34600de90e320p+2, 0x1.34600de90e323p+2}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.7a3d511a6a009p-4, 0x1.7a3d511a6a012p-4},
{0x1.a67e9a3b3c161p-12, 0x1.a67e9a3b3cdd0p-12},
{0x1.ad90cec9c0f6fp-7, 0x1.ad90cec9c112bp-7},
{-0x1.9855b85b10049p-7, -0x1.9855b85b0ffcbp-7},
{0x1.50c589d18499bp+2, 0x1.50c589d18499ep+2},
{0x1.ffcb302cb8985p+0, 0x1.ffcb302cb8989p+0},
{-0x1.437fc6eca1366p+0, -0x1.437fc6eca1362p+0},
{-0x1.04dfa2a190d26p-3, -0x1.04dfa2a190c7cp-3},
{0x1.a589ad2ce9e39p-6, 0x1.a589ad2ce9e54p-6},
{0x1.6e420deccb411p-3, 0x1.6e420deccb4b5p-3},
{0x1.e1555429f7bf1p-4, 0x1.e1555429f7c0bp-4},
{0x1.345ecb5088dc7p+2, 0x1.345ecb5088dcbp+2}}},
},
{
0x1.999999999999ap-5,
{{0x1.87e7c06e19b91p-8, 0x1.03126e978d4fep-2, 0x1.54c985f06f694p-10,
0x1.9e30014f8b589p-8}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.b77efb25b8dd2p-2, 0x1.b77efb25b8dd4p-2},
{0x1.d252b304ed681p-4, 0x1.d252b304ed683p-4},
{0x1.235c8909400c5p-6, 0x1.235c8909400c7p-6},
{0x1.9a5c5e03484dep-9, 0x1.9a5c5e03484e0p-9},
{0x1.42f6019270650p+1, 0x1.42f6019270652p+1},
{0x1.d89101a46e842p+0, 0x1.d89101a46e844p+0},
{0x1.721a47b716779p-1, 0x1.721a47b71677bp-1},
{0x1.721a47b716779p-1, 0x1.721a47b71677bp-1},
{0x1.343beb6102cc6p-3, 0x1.343beb6102cc8p-3},
{-0x1.3f24600000001p-301, 0x1.3f24600000001p-301},
{0x1.025e67b9cdad7p-2, 0x1.025e67b9cdad9p-2},
{0x1.42f6019270650p+1, 0x1.42f6019270652p+1}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.b8566cd59dd36p-2, 0x1.b8566cd59dd3ap-2},
{0x1.b9d5c5e3870abp-4, 0x1.b9d5c5e3870c1p-4},
{0x1.1587ab02cfe81p-6, 0x1.1587ab02cff38p-6},
{0x1.1ad4cd8131fbap-7, 0x1.1ad4cd8131fd0p-7},
{0x1.6346ef889a9d2p+1, 0x1.6346ef889a9d4p+1},
{0x1.c8c547438f1e7p+0, 0x1.c8c547438f1ecp+0},
{0x1.95f95eebd2052p-1, 0x1.95f95eebd205ep-1},
{0x1.0ed51015fadbfp-1, 0x1.0ed51015fade0p-1},
{0x1.967b2c3360cb1p-3, 0x1.967b2c3360cc9p-3},
{-0x1.0e5de7f129e18p-3, -0x1.0e5de7f129d6ep-3},
{0x1.010a6df46a07bp-2, 0x1.010a6df46a082p-2},
{0x1.43c4ee9a8ce2bp+1, 0x1.43c4ee9a8ce2fp+1}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.b84e9bd4911f6p-2, 0x1.b84e9bd4911fap-2},
{0x1.b9ccf55bd5095p-4, 0x1.b9ccf55bd50abp-4},
{0x1.15b1a7159d30ap-6, 0x1.15b1a7159d3c1p-6},
{0x1.1b178ee6bb5d6p-7, 0x1.1b178ee6bb5ecp-7},
{0x1.634e33812c311p+1, 0x1.634e33812c313p+1},
{0x1.c8c66154855eap+0, 0x1.c8c66154855eep+0},
{0x1.95e0b74d64b63p-1, 0x1.95e0b74d64b6ep-1},
{0x1.0ec60ab6ae53dp-1, 0x1.0ec60ab6ae55ep-1},
{0x1.966a037efd9d7p-3, 0x1.966a037efd9f0p-3},
{-0x1.0e4423dcbe1c4p-3, -0x1.0e4423dcbe11ap-3},
{0x1.0108815a6658cp-2, 0x1.0108815a66593p-2},
{0x1.43cb51041b9e3p+1, 0x1.43cb51041b9e7p+1}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.b85284545e3fep-2, 0x1.b85284545e402p-2},
{0x1.b9d15d4ee5b37p-4, 0x1.b9d15d4ee5b4dp-4},
{0x1.159ca92438c35p-6, 0x1.159ca92438cebp-6},
{0x1.1af6311dbcc58p-7, 0x1.1af6311dbcc6ep-7},
{0x1.634a9180901b3p+1, 0x1.634a9180901b5p+1},
{0x1.c8c5d45623495p+0, 0x1.c8c5d4562349ap+0},
{0x1.95ed0b564cb88p-1, 0x1.95ed0b564cb94p-1},
{0x1.0ecd8d3b0bfa0p-1, 0x1.0ecd8d3b0bfc1p-1},
{0x1.9672972b2e1aap-3, 0x1.9672972b2e1c2p-3},
{-0x1.0e510695d3bacp-3, -0x1.0e510695d3b02p-3},
{0x1.010977a63dfebp-2, 0x1.010977a63dff1p-2},
{0x1.43c81fc9071c0p+1, 0x1.43c81fc9071c4p+1}}},
},
{
0x1.999999999999ap-4,
{{0x1.6c61522a6f3f5p-10, 0x1.16872b020c49cp-3, 0x1.5d2d01c0ca601p-11,
0x1.38476f2a5a46ap-7}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.270906ee5a6cfp-1, 0x1.270906ee5a6d1p-1},
{0x1.0eb16bef11388p-2, 0x1.0eb16bef1138ap-2},
{0x1.945c98b83c928p-4, 0x1.945c98b83c92ap-4},
{0x1.9f4a5c2b49fb7p-5, 0x1.9f4a5c2b49fb9p-5},
{0x1.c8b1bdc9d3f27p+0, 0x1.c8b1bdc9d3f29p+0},
{0x1.8673042a10047p+0, 0x1.8673042a10049p+0},
{0x1.e5a6c4f8749dfp-1, 0x1.e5a6c4f8749e1p-1},
{0x1.e5a6c4f8749dfp-1, 0x1.e5a6c4f8749e1p-1},
{0x1.9af613adf14c0p-2, 0x1.9af613adf14c2p-2},
{-0x1.c357400000001p-302, 0x1.c357400000001p-302},
{0x1.6d631d0d902e4p-2, 0x1.6d631d0d902e6p-2},
{0x1.c8b1bdc9d3f27p+0, 0x1.c8b1bdc9d3f29p+0}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.26f65a0fd4a73p-1, 0x1.26f65a0fd4a75p-1},
{0x1.0e6d0af83d0acp-2, 0x1.0e6d0af83d0b2p-2},
{0x1.916bd5fe070d9p-4, 0x1.916bd5fe07101p-4},
{0x1.93ecedc1f0d7fp-5, 0x1.93ecedc1f0d89p-5},
{0x1.eb5d1d3ccbe86p+0, 0x1.eb5d1d3ccbe89p+0},
{0x1.78c97a83e17a7p+0, 0x1.78c97a83e17aap+0},
{0x1.0635d7caf70c6p+0, 0x1.0635d7caf70cdp+0},
{0x1.cc58e8a2ee2c2p-1, 0x1.cc58e8a2ee2dfp-1},
{0x1.b7deda7ffddf7p-2, 0x1.b7deda7ffde03p-2},
{-0x1.47a0e22f04e5ep-5, -0x1.47a0e22f04c0dp-5},
{0x1.6cb487831db4ap-2, 0x1.6cb487831db50p-2},
{0x1.cb21e9c5f325fp+0, 0x1.cb21e9c5f3263p+0}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.26f24a61a564bp-1, 0x1.26f24a61a564dp-1},
{0x1.0e667df8508a0p-2, 0x1.0e667df8508a6p-2},
{0x1.9170e359fb821p-4, 0x1.9170e359fb849p-4},
{0x1.9417f976522f8p-5, 0x1.9417f97652303p-5},
{0x1.eb6a157592b20p+0, 0x1.eb6a157592b23p+0},
{0x1.78ccc103d7badp+0, 0x1.78ccc103d7bb1p+0},
{0x1.062f6c921cc39p+0, 0x1.062f6c921cc3fp+0},
{0x1.cc4595bdc0604p-1, 0x1.cc4595bdc0621p-1},
{0x1.b7c6fd930c880p-2, 0x1.b7c6fd930c88cp-2},
{-0x1.47e982ab3d9f7p-5, -0x1.47e982ab3d7a5p-5},
{0x1.6cb19975e1802p-2, 0x1.6cb19975e1808p-2},
{0x1.cb2cb028081f8p+0, 0x1.cb2cb028081fcp+0}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.26f45238988fdp-1, 0x1.26f45238988ffp-1},
{0x1.0e69c46af3672p-2, 0x1.0e69c46af3678p-2},
{0x1.916e5ca776061p-4, 0x1.916e5ca776088p-4},
{0x1.940274204c9cdp-5, 0x1.940274204c9d8p-5},
{0x1.eb63995317741p+0, 0x1.eb63995317743p+0},
{0x1.78cb1dca864c4p+0, 0x1.78cb1dca864c8p+0},
{0x1.0632a241f5246p+0, 0x1.0632a241f524cp+0},
{0x1.cc4f3f1d6dc15p-1, 0x1.cc4f3f1d6dc32p-1},
{0x1.b7d2ebcdd3a6fp-2, 0x1.b7d2ebcdd3a7bp-2},
{-0x1.47c534f9a1c17p-5, -0x1.47c534f9a19c6p-5},
{0x1.6cb3107b2e3dep-2, 0x1.6cb3107b2e3e4p-2},
{0x1.cb274cef49caep+0, 0x1.cb274cef49cb1p+0}}},
},
{
0x1.0000000000000p-1,
{{0x1.29cbab649d389p-13, 0x1.d14e3bcd35a86p-6, 0x1.8c5c9a34ca0c3p-14,
0x1.13c68661ae70cp-11}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.b7590ca5d56abp-1, 0x1.b7590ca5d56adp-1},
{0x1.79c27556059fcp-1, 0x1.79c27556059fep-1},
{0x1.509c59ccb1964p-1, 0x1.509c59ccb1966p-1},
{0x1.422975f1d50c3p-1, 0x1.422975f1d50c5p-1},
{0x1.2a37abf469800p-1, 0x1.2a37abf469802p-1},
{0x1.1382380277aa0p-1, 0x1.1382380277aa2p-1},
{0x1.a5b316788e428p-2, 0x1.a5b316788e42ap-2},
{0x1.a5b316788e428p-2, 0x1.a5b316788e42ap-2},
{0x1.c86932802133fp-3, 0x1.c869328021341p-3},
{-0x1.26ee800000001p-303, 0x1.26ee800000001p-303},
{0x1.87247f6ca05d8p-1, 0x1.87247f6ca05dap-1},
{0x1.2a37abf469800p-1, 0x1.2a37abf469802p-1}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.b7553624e5316p-1, 0x1.b7553624e5318p-1},
{0x1.79b5b3c71720bp-1, 0x1.79b5b3c71720dp-1},
{0x1.509288faf814fp-1, 0x1.509288faf8152p-1},
{0x1.4216f7b692db3p-1, 0x1.4216f7b692db6p-1},
{0x1.38c1b66704b5dp-1, 0x1.38c1b66704b63p-1},
{0x1.0c949871d1be7p-1, 0x1.0c949871d1be9p-1},
{0x1.c0cef4f93d8ddp-2, 0x1.c0cef4f93d8e5p-2},
{0x1.b3b7bc7fdfa53p-2, 0x1.b3b7bc7fdfa69p-2},
{0x1.bcf5e084222b4p-3, 0x1.bcf5e084222bdp-3},
{0x1.27c480885099dp-7, 0x1.27c4808850cf9p-7},
{0x1.87181cfb04848p-1, 0x1.87181cfb0484ap-1},
{0x1.2a7c94e9a3a3dp-1, 0x1.2a7c94e9a3a41p-1}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.b751be282259dp-1, 0x1.b751be28225a0p-1},
{0x1.79af4d5fe0284p-1, 0x1.79af4d5fe0286p-1},
{0x1.508a2ff978465p-1, 0x1.508a2ff978468p-1},
{0x1.420defb8a1c5ap-1, 0x1.420defb8a1c5dp-1},
{0x1.38d0a97ead811p-1, 0x1.38d0a97ead816p-1},
{0x1.0ca165403faf5p-1, 0x1.0ca165403faf8p-1},
{0x1.c0e44203a3bafp-2, 0x1.c0e44203a3bb7p-2},
{0x1.b3cc5fc984930p-2, 0x1.b3cc5fc984946p-2},
{0x1.bd0aed39f3147p-3, 0x1.bd0aed39f3150p-3},
{0x1.27d1ae0dd005dp-7, 0x1.27d1ae0dd03b9p-7},
{0x1.87125aea910c9p-1, 0x1.87125aea910cbp-1},
{0x1.2a8ad7437b2aep-1, 0x1.2a8ad7437b2b1p-1}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.b7537a2158170p-1, 0x1.b7537a2158173p-1},
{0x1.79b28088e2c65p-1, 0x1.79b28088e2c67p-1},
{0x1.508e5c6b105ecp-1, 0x1.508e5c6b105efp-1},
{0x1.421273a69f88ep-1, 0x1.421273a69f891p-1},
{0x1.38c9300704d3bp-1, 0x1.38c9300704d40p-1},
{0x1.0c9afeee3a733p-1, 0x1.0c9afeee3a736p-1},
{0x1.c0d99baae0253p-2, 0x1.c0d99baae025bp-2},
{0x1.b3c20e5419820p-2, 0x1.b3c20e5419836p-2},
{0x1.bd00671219eacp-3, 0x1.bd00671219eb4p-3},
{0x1.27cb17c00cee7p-7, 0x1.27cb17c00d243p-7},
{0x1.87153be8d5e91p-1, 0x1.87153be8d5e93p-1},
{0x1.2a83b62aa8393p-1, 0x1.2a83b62aa8396p-1}}},
},
{
0x1.0000000000000p+0,
{{0x1.5b49d2b1e91bbp-15, 0x1.0b630a91537a0p-7, 0x1.0ac1fae1b30dep-16,
0x1.91d0777c385fcp-15}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.ead7f87070237p-1, 0x1.ead7f87070239p-1},
{0x1.d8e87b7ae7a62p-1, 0x1.d8e87b7ae7a64p-1},
{0x1.ccec8c4ec5ef1p-1, 0x1.ccec8c4ec5ef3p-1},
{0x1.c8b73b9ac9a1dp-1, 0x1.c8b73b9ac9a1fp-1},
{0x1.5b5c75846aaf9p-3, 0x1.5b5c75846aafbp-3},
{0x1.40eb7cceb0482p-3, 0x1.40eb7cceb0484p-3},
{0x1.eb3e2ce365fd5p-4, 0x1.eb3e2ce365fd7p-4},
{0x1.eb3e2ce365fd5p-4, 0x1.eb3e2ce365fd7p-4},
{0x1.09dbd573f51e4p-4, 0x1.09dbd573f51e6p-4},
{-0x1.586e000000001p-305, 0x1.586e000000001p-305},
{0x1.dcce118e2abfap-1, 0x1.dcce118e2abfcp-1},
{0x1.5b5c75846aaf9p-3, 0x1.5b5c75846aafbp-3}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.eada37c43dbcdp-1, 0x1.eada37c43dbd0p-1},
{0x1.d8eb0b5ab5aebp-1, 0x1.d8eb0b5ab5aeep-1},
{0x1.ccf1f94448ecfp-1, 0x1.ccf1f94448ed1p-1},
{0x1.c8badb25ce1b3p-1, 0x1.c8badb25ce1b6p-1},
{0x1.6c11624df5d8dp-3, 0x1.6c11624df5d96p-3},
{0x1.38a7a52a52899p-3, 0x1.38a7a52a5289dp-3},
{0x1.053de806af3a2p-3, 0x1.053de806af3a6p-3},
{0x1.fb41824cb7384p-4, 0x1.fb41824cb739ap-4},
{0x1.0303034e7937cp-4, 0x1.0303034e79384p-4},
{0x1.58908a0766c2ap-9, 0x1.58908a0767016p-9},
{0x1.dcd0247fc702ap-1, 0x1.dcd0247fc702dp-1},
{0x1.5b75854f419a5p-3, 0x1.5b75854f419aap-3}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.ead630c226440p-1, 0x1.ead630c226443p-1},
{0x1.d8e399ff30e90p-1, 0x1.d8e399ff30e93p-1},
{0x1.cce84031702f6p-1, 0x1.cce84031702f8p-1},
{0x1.c8b05490c3638p-1, 0x1.c8b05490c363bp-1},
{0x1.6c56b7b4bec58p-3, 0x1.6c56b7b4bec60p-3},
{0x1.38e3300678b71p-3, 0x1.38e3300678b76p-3},
{0x1.056fa85832a89p-3, 0x1.056fa85832a8dp-3},
{0x1.fba21c895611fp-4, 0x1.fba21c8956135p-4},
{0x1.033456e6d857dp-4, 0x1.033456e6d8585p-4},
{0x1.58d2288b53912p-9, 0x1.58d2288b53cfep-9},
{0x1.dcc97107ca959p-1, 0x1.dcc97107ca95cp-1},
{0x1.5bb7b0fadf43fp-3, 0x1.5bb7b0fadf445p-3}}},
{{{0x1.fffffffffffffp-1, 0x1.0000000000001p+0},
{0x1.ead8344139715p-1, 0x1.ead8344139717p-1},
{0x1.d8e752a94ecadp-1, 0x1.d8e752a94ecafp-1},
{0x1.cced1cb61a4edp-1, 0x1.cced1cb61a4f0p-1},
{0x1.c8b597d621e34p-1, 0x1.c8b597d621e37p-1},
{0x1.6c340d23482b2p-3, 0x1.6c340d23482bap-3},
{0x1.38c56ab589a8cp-3, 0x1.38c56ab589a90p-3},
{0x1.0556c847cb264p-3, 0x1.0556c847cb268p-3},
{0x1.fb71cf9a50858p-4, 0x1.fb71cf9a5086ep-4},
{0x1.031bad32ce78bp-4, 0x1.031bad32ce792p-4},
{0x1.58b159698d54ep-9, 0x1.58b159698d939p-9},
{0x1.dccccac08134cp-1, 0x1.dccccac08134fp-1},
{0x1.5b969b4572366p-3, 0x1.5b969b457236cp-3}}},
},
}};
} // namespace thermal_oracle
#endif
@@ -0,0 +1,68 @@
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include "fesa/app/fesa_application.h"
#include "thermal_reference_comparison.h"
namespace {
std::string Bytes(const std::filesystem::path& path) {
std::ifstream in{path, std::ios::binary};
return {std::istreambuf_iterator<char>{in}, std::istreambuf_iterator<char>{}};
}
TEST(ThermalReference, OriginalAbaqusNt11RemainsAnExplicitFailingDiagnostic) {
const auto folder =
std::filesystem::path{FESA_TEST_SOURCE_DIR} / "reference/shell-thermal";
const auto input = folder / "shell-thermal.inp";
const auto csv = folder / "shell-thermal NodalTemperature.csv";
const auto before_input = Bytes(input), before_csv = Bytes(csv);
ASSERT_FALSE(before_input.empty());
ASSERT_FALSE(before_csv.empty());
const int exit = fesa::FesaApplication{}.Run({input.string()});
EXPECT_EQ(Bytes(input), before_input);
EXPECT_EQ(Bytes(csv), before_csv);
ASSERT_EQ(exit, 0);
const auto result = fesa::test::CompareThermalReferenceFiles(
input, csv, folder / "shell-thermal_results.h5");
ASSERT_TRUE(result.HasValue())
<< (result.HasValue() ? "" : result.GetStatus().Diagnostics()[0].message);
const auto& r = result.Value();
ASSERT_EQ(r.rows.size(), 729U);
const auto output = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"thermal-reference-metrics.csv";
std::ofstream rows{output};
rows
<< "instance,label,dof,actual,reference,absolute_error,threshold,passed\n"
<< std::setprecision(17);
std::size_t failed = 0;
for (const auto& row : r.rows) {
failed += !row.tolerance.passed;
rows << row.instance << ',' << row.label << ',' << row.source_dof << ','
<< row.actual << ',' << row.reference << ','
<< row.tolerance.absolute_error << ',' << row.tolerance.threshold
<< ',' << row.tolerance.passed << '\n';
}
ASSERT_TRUE(rows.good());
std::cout << std::setprecision(17)
<< "THERMAL_REFERENCE rows=" << r.rows.size()
<< " S=" << r.family.reference_scale
<< " B=" << r.family.near_zero_band
<< " RMS/S=" << r.family.relative_rms
<< " max_abs=" << r.family.maximum_absolute_error
<< " failed_rows=" << failed << " numerical_passed=" << r.passed
<< " classification=diagnostic blocking=" << r.family.blocking
<< '\n';
EXPECT_EQ(r.family.reference_scale, 1000);
EXPECT_EQ(r.family.near_zero_band, 10);
EXPECT_FALSE(r.family.blocking);
EXPECT_FALSE(r.passed);
EXPECT_FALSE(r.family.rms_passed);
EXPECT_EQ(failed, 87U);
EXPECT_GT(r.family.relative_rms,
fesa::test::ReferenceTolerancePolicy::kRelativeRmsTolerance);
EXPECT_GT(r.family.maximum_absolute_error, r.family.near_zero_band);
}
} // namespace
@@ -0,0 +1,417 @@
#include "thermal_reference_comparison.h"
#include <hdf5.h>
#include <algorithm>
#include <array>
#include <charconv>
#include <cmath>
#include <fstream>
#include <map>
#include <regex>
#include <set>
#include <sstream>
#include <stdexcept>
#include "fesa/analysis/prepared_analysis.h"
namespace fesa::test {
namespace {
Status Failure(const std::string& message) {
return Status::Failure(FailureCategory::kModel,
{{Severity::kError,
"invalid-thermal-reference",
{},
"HEAT TRANSFER",
"shell-thermal/Step-1/t=37/NT11",
message}});
}
void Require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error{message};
}
class H {
public:
H(hid_t id, herr_t (*close)(hid_t)) : id_(id), close_(close) {
Require(id >= 0, "Missing or invalid HDF5 object.");
}
~H() { close_(id_); }
H(const H&) = delete;
H& operator=(const H&) = delete;
hid_t Id() const { return id_; }
private:
hid_t id_;
herr_t (*close_)(hid_t);
};
std::vector<hsize_t> Shape(hid_t dataset) {
H space{H5Dget_space(dataset), H5Sclose};
const int n = H5Sget_simple_extent_ndims(space.Id());
Require(n >= 0, "Invalid dataset dataspace.");
std::vector<hsize_t> dims(static_cast<std::size_t>(n));
Require(H5Sget_simple_extent_dims(space.Id(), dims.data(), nullptr) >= 0,
"Invalid dimensions.");
return dims;
}
std::string Attr(hid_t object, const char* name) {
H a{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
H t{H5Aget_type(a.Id()), H5Tclose};
H s{H5Aget_space(a.Id()), H5Sclose};
Require(H5Sget_simple_extent_type(s.Id()) == H5S_SCALAR &&
H5Tget_class(t.Id()) == H5T_STRING &&
H5Tget_cset(t.Id()) == H5T_CSET_UTF8,
"Invalid UTF8 scalar attribute.");
if (H5Tis_variable_str(t.Id()) > 0) {
char* p = nullptr;
Require(H5Aread(a.Id(), t.Id(), &p) >= 0 && p != nullptr,
"Cannot read string.");
std::string result{p};
H5free_memory(p);
return result;
}
std::vector<char> p(H5Tget_size(t.Id()) + 1, 0);
Require(H5Aread(a.Id(), t.Id(), p.data()) >= 0, "Cannot read fixed string.");
return std::string{p.data()};
}
template <class T>
T Scalar(hid_t object, const char* name, hid_t disk, hid_t memory) {
H a{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
H t{H5Aget_type(a.Id()), H5Tclose};
H s{H5Aget_space(a.Id()), H5Sclose};
Require(H5Tequal(t.Id(), disk) > 0 &&
H5Sget_simple_extent_type(s.Id()) == H5S_SCALAR,
"Invalid scalar type/shape.");
T value{};
Require(H5Aread(a.Id(), memory, &value) >= 0, "Cannot read scalar.");
return value;
}
template <class T>
std::vector<T> Primitive(hid_t file, const char* path,
const std::vector<hsize_t>& dims, hid_t disk,
hid_t memory) {
H d{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
H t{H5Dget_type(d.Id()), H5Tclose};
Require(Shape(d.Id()) == dims && H5Tequal(t.Id(), disk) > 0,
"Wrong dataset primitive/extent.");
std::size_t n = 1;
for (auto extent : dims) n *= static_cast<std::size_t>(extent);
std::vector<T> values(n);
Require(H5Dread(d.Id(), memory, H5S_ALL, H5S_ALL, H5P_DEFAULT,
values.data()) >= 0,
"Cannot read primitive.");
return values;
}
void Field(hid_t compound, const char* name, hid_t expected) {
Require(H5Tget_class(compound) == H5T_COMPOUND, "Expected compound.");
const int index = H5Tget_member_index(compound, name);
Require(index >= 0, "Missing compound field.");
H type{H5Tget_member_type(compound, static_cast<unsigned>(index)), H5Tclose};
Require(H5Tequal(type.Id(), expected) > 0,
"Invalid compound field primitive.");
}
std::uint64_t Label(const std::string& text) {
std::uint64_t value = 0;
const auto parsed =
std::from_chars(text.data(), text.data() + text.size(), value);
Require(parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() &&
value > 0,
"Invalid node label.");
return value;
}
std::string Trim(const std::string& text) {
const auto begin = text.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) return {};
return text.substr(begin, text.find_last_not_of(" \t\r\n") - begin + 1);
}
} // namespace
Result<ThermalReferenceReport> CompareThermalSnapshots(
const ThermalReferenceSnapshot& actual,
const ThermalReferenceSnapshot& reference,
const std::vector<std::uint64_t>& expected_labels,
const std::string& instance) {
try {
const std::set<std::uint64_t> labels(expected_labels.begin(),
expected_labels.end());
Require(!labels.empty() && labels.size() == expected_labels.size() &&
!instance.empty(),
"Invalid input node inventory.");
auto mapped = [&](const ThermalReferenceSnapshot& s) {
Require(s.step == "Step-1" && s.time == 37 && s.increment == 37 &&
s.source_dof == 11,
"Wrong source step/time/increment/station.");
std::map<std::uint64_t, double> result;
for (const auto& row : s.rows) {
Require(row.instance == instance && labels.count(row.label) == 1 &&
std::isfinite(row.value),
"Wrong instance/extra label/nonfinite value.");
Require(result.emplace(row.label, row.value).second,
"Duplicate node identity.");
}
Require(result.size() == labels.size(), "Missing node identity.");
return result;
};
const auto a = mapped(actual), r = mapped(reference);
ReferenceToleranceFamily family{
"shell-thermal/Step-1/time=37/NT11/temperature/user-consistent",
{"NT11"},
false,
{}};
for (auto label : labels) family.rows.push_back({a.at(label), r.at(label)});
const auto evaluation = ReferenceTolerancePolicy::Evaluate({family});
Require(evaluation.valid && evaluation.families.size() == 1,
"Invalid tolerance evaluation.");
ThermalReferenceReport report{
evaluation.families[0].passed, {}, evaluation.families[0]};
std::size_t index = 0;
for (auto label : labels)
report.rows.push_back({instance, label, 11, a.at(label), r.at(label),
report.family.rows[index++]});
return Result<ThermalReferenceReport>::Success(std::move(report));
} catch (const std::exception& error) {
return Result<ThermalReferenceReport>::Failure(Failure(error.what()));
}
}
Result<ThermalReferenceSnapshot> ReadThermalReferenceCsv(
const std::filesystem::path& path, const std::string& sole_instance) {
try {
Require(!sole_instance.empty(), "CSV needs a proven sole input instance.");
std::ifstream input{path};
Require(input.good(), "Cannot read original CSV.");
std::string line;
Require(static_cast<bool>(std::getline(input, line)),
"Missing CSV header.");
auto split = [](const std::string& row) {
std::vector<std::string> values;
std::istringstream in{row};
std::string value;
while (std::getline(in, value, ',')) values.push_back(Trim(value));
return values;
};
Require(split(line) ==
std::vector<std::string>{"Step", "Frame", "Node Label", "NT11"},
"Wrong CSV columns/station.");
ThermalReferenceSnapshot result{"Step-1", 37, 37, 11, {}};
const std::regex frame{R"(^Increment\s+37:\s+Step Time\s*=\s*37\.00$)"};
while (std::getline(input, line)) {
const auto row = split(line);
Require(row.size() == 4 && row[0] == "Step-1" &&
std::regex_match(row[1], frame),
"Wrong CSV source frame.");
std::size_t consumed = 0;
const double value = std::stod(row[3], &consumed);
Require(consumed == row[3].size() && std::isfinite(value),
"Invalid CSV value.");
result.rows.push_back({sole_instance, Label(row[2]), value});
}
Require(input.eof(), "CSV read failed.");
return Result<ThermalReferenceSnapshot>::Success(std::move(result));
} catch (const std::exception& error) {
return Result<ThermalReferenceSnapshot>::Failure(Failure(error.what()));
}
}
Result<ThermalReferenceSnapshot> ReadThermalReferenceHdf5(
const std::filesystem::path& path) {
try {
H file{H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
H meta{H5Gopen2(file.Id(), "/metadata", H5P_DEFAULT), H5Gclose};
Require(
Scalar<std::uint64_t>(meta.Id(), "schema_version", H5T_STD_U64LE,
H5T_NATIVE_UINT64) == 1 &&
Attr(meta.Id(), "feature_id") == "shell-transient-heat-transfer" &&
Attr(meta.Id(), "analysis_procedure") ==
"uncoupled-transient-heat-transfer" &&
Attr(meta.Id(), "source_step_name") == "Step-1" &&
Attr(meta.Id(), "unit_system_label") ==
"user-consistent-unspecified",
"Wrong thermal metadata.");
Require(Primitive<std::uint64_t>(file.Id(), "/model/temperature_dofs", {5},
H5T_STD_U64LE, H5T_NATIVE_UINT64) ==
std::vector<std::uint64_t>{11, 12, 13, 14, 15},
"Wrong station DOFs.");
Require(Primitive<double>(file.Id(), "/model/temperature_zeta", {5},
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE) ==
std::vector<double>{-1, -.5, 0, .5, 1},
"Wrong station coordinates.");
H nodes{H5Dopen2(file.Id(), "/model/nodes", H5P_DEFAULT), H5Dclose};
H nt{H5Dget_type(nodes.Id()), H5Tclose};
const auto shape = Shape(nodes.Id());
Require(shape.size() == 1 && shape[0] > 0, "Invalid nodes shape.");
Field(nt.Id(), "internal_node_id", H5T_STD_U64LE);
struct NodeRow {
std::uint64_t id;
char* instance;
char* label;
};
H string{H5Tcopy(H5T_C_S1), H5Tclose};
H5Tset_size(string.Id(), H5T_VARIABLE);
H5Tset_cset(string.Id(), H5T_CSET_UTF8);
for (const auto* name : {"instance_name", "source_label"}) {
const int index = H5Tget_member_index(nt.Id(), name);
Require(index >= 0, "Missing identity field.");
H type{H5Tget_member_type(nt.Id(), static_cast<unsigned>(index)),
H5Tclose};
Require(H5Tget_class(type.Id()) == H5T_STRING &&
H5Tget_cset(type.Id()) == H5T_CSET_UTF8,
"Wrong identity type.");
}
H memory{H5Tcreate(H5T_COMPOUND, sizeof(NodeRow)), H5Tclose};
H5Tinsert(memory.Id(), "internal_node_id", HOFFSET(NodeRow, id),
H5T_NATIVE_UINT64);
H5Tinsert(memory.Id(), "instance_name", HOFFSET(NodeRow, instance),
string.Id());
H5Tinsert(memory.Id(), "source_label", HOFFSET(NodeRow, label),
string.Id());
std::vector<NodeRow> raw(static_cast<std::size_t>(shape[0]));
Require(H5Dread(nodes.Id(), memory.Id(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
raw.data()) >= 0,
"Cannot read node identities.");
std::map<std::uint64_t, ThermalReferenceRow> identities;
bool valid = true;
for (const auto& row : raw) {
if (!row.instance || !row.label)
valid = false;
else {
try {
valid = identities
.emplace(row.id, ThermalReferenceRow{row.instance,
Label(row.label), 0})
.second &&
valid;
} catch (...) {
valid = false;
}
}
}
H ns{H5Dget_space(nodes.Id()), H5Sclose};
Require(
H5Treclaim(memory.Id(), ns.Id(), H5P_DEFAULT, raw.data()) >= 0 && valid,
"Invalid node identity inventory.");
const auto active =
Primitive<std::uint64_t>(file.Id(), "/model/thermal_node_ids", shape,
H5T_STD_U64LE, H5T_NATIVE_UINT64);
Require(std::set<std::uint64_t>(active.begin(), active.end()).size() ==
active.size(),
"Duplicate active IDs.");
struct Frame {
std::uint64_t frame, increment;
double time, delta;
std::uint8_t initial;
};
H fd{H5Dopen2(file.Id(), "/steps/Step-1/accepted_frames", H5P_DEFAULT),
H5Dclose};
H ft{H5Dget_type(fd.Id()), H5Tclose};
H fm{H5Tcreate(H5T_COMPOUND, sizeof(Frame)), H5Tclose};
for (const auto* name : {"frame_index", "increment_index"})
Field(ft.Id(), name, H5T_STD_U64LE);
for (const auto* name : {"time", "time_increment"})
Field(ft.Id(), name, H5T_IEEE_F64LE);
Field(ft.Id(), "is_initial", H5T_STD_U8LE);
H5Tinsert(fm.Id(), "frame_index", HOFFSET(Frame, frame), H5T_NATIVE_UINT64);
H5Tinsert(fm.Id(), "increment_index", HOFFSET(Frame, increment),
H5T_NATIVE_UINT64);
H5Tinsert(fm.Id(), "time", HOFFSET(Frame, time), H5T_NATIVE_DOUBLE);
H5Tinsert(fm.Id(), "time_increment", HOFFSET(Frame, delta),
H5T_NATIVE_DOUBLE);
H5Tinsert(fm.Id(), "is_initial", HOFFSET(Frame, initial), H5T_NATIVE_UINT8);
const auto fs = Shape(fd.Id());
Require(fs == std::vector<hsize_t>{101}, "Missing/extra reference frames.");
std::vector<Frame> frames(static_cast<std::size_t>(fs[0]));
Require(H5Dread(fd.Id(), fm.Id(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
frames.data()) >= 0,
"Cannot read frames.");
std::size_t matches = 0;
for (std::size_t i = 0; i < frames.size(); ++i) {
const auto& f = frames[i];
Require(f.frame == i && f.increment == i && f.initial == (i == 0) &&
std::isfinite(f.time) && std::isfinite(f.delta),
"Invalid/duplicate frame inventory.");
Require(f.time == static_cast<double>(i) && f.delta == (i == 0 ? 0. : 1.),
"Wrong frame schedule.");
if (f.time == 37) {
++matches;
Require(f.increment == 37, "Wrong reference increment.");
}
}
Require(matches == 1 && frames[37].time == 37,
"No unique exact t=37 frame.");
H group{H5Gopen2(file.Id(), "/steps/Step-1/frames/37", H5P_DEFAULT),
H5Gclose};
Require(Scalar<double>(group.Id(), "time", H5T_IEEE_F64LE,
H5T_NATIVE_DOUBLE) == 37 &&
Scalar<std::uint64_t>(group.Id(), "frame_index", H5T_STD_U64LE,
H5T_NATIVE_UINT64) == 37 &&
Scalar<std::uint64_t>(group.Id(), "increment_index",
H5T_STD_U64LE, H5T_NATIVE_UINT64) == 37 &&
Scalar<std::uint8_t>(group.Id(), "is_initial", H5T_STD_U8LE,
H5T_NATIVE_UINT8) == 0,
"Wrong selected frame attributes.");
const auto values = Primitive<double>(
file.Id(), "/steps/Step-1/frames/37/nodal/temperature", {shape[0], 5},
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE);
ThermalReferenceSnapshot result{"Step-1", 37, 37, 11, {}};
result.source_input_identity = Attr(meta.Id(), "source_input_identity");
H temperature{
H5Dopen2(file.Id(), "/steps/Step-1/frames/37/nodal/temperature",
H5P_DEFAULT),
H5Dclose};
Require(
Attr(temperature.Id(), "component_order") ==
"NT11,NT12,NT13,NT14,NT15" &&
Attr(temperature.Id(), "units_label") == "temperature" &&
Attr(temperature.Id(), "coordinate_system") == "scalar" &&
Attr(temperature.Id(), "location") == "nodal-thickness-stations",
"Wrong temperature component/units/location metadata.");
for (double value : values)
Require(std::isfinite(value), "Nonfinite temperature station.");
for (std::size_t i = 0; i < active.size(); ++i) {
Require(identities.count(active[i]) == 1, "Unknown active node ID.");
auto row = identities.at(active[i]);
row.value = values[5 * i];
result.rows.push_back(std::move(row));
}
return Result<ThermalReferenceSnapshot>::Success(std::move(result));
} catch (const std::exception& error) {
return Result<ThermalReferenceSnapshot>::Failure(Failure(error.what()));
}
}
Result<ThermalReferenceReport> CompareThermalReferenceFiles(
const std::filesystem::path& input, const std::filesystem::path& csv,
const std::filesystem::path& hdf5) {
auto prepared = PrepareAnalysisInput(input);
if (!prepared.HasValue())
return Result<ThermalReferenceReport>::Failure(prepared.GetStatus());
const auto& domain = prepared.Value().domain;
if (!domain.ThermalStep() || domain.ThermalStep()->name != "Step-1" ||
domain.Nodes().size() != 729 || domain.ThermalElements().Size() != 676)
return Result<ThermalReferenceReport>::Failure(
Failure("Unexpected reference input inventory."));
std::vector<std::uint64_t> labels;
for (const auto& node : domain.Nodes()) {
if (node.source_id.instance_name != "Part-1-1")
return Result<ThermalReferenceReport>::Failure(
Failure("CSV requires the sole Part-1-1 input instance."));
labels.push_back(static_cast<std::uint64_t>(node.source_id.source_label));
}
const auto actual = ReadThermalReferenceHdf5(hdf5),
reference = ReadThermalReferenceCsv(csv, "Part-1-1");
if (!actual.HasValue())
return Result<ThermalReferenceReport>::Failure(actual.GetStatus());
if (!reference.HasValue())
return Result<ThermalReferenceReport>::Failure(reference.GetStatus());
const std::string expected_identity =
"path=" +
std::filesystem::absolute(domain.SourcePath())
.lexically_normal()
.generic_u8string() +
";content_identity=" + domain.SourceContentIdentity();
if (actual.Value().source_input_identity != expected_identity)
return Result<ThermalReferenceReport>::Failure(
Failure("Wrong source input identity."));
return CompareThermalSnapshots(actual.Value(), reference.Value(), labels,
"Part-1-1");
}
} // namespace fesa::test
@@ -0,0 +1,57 @@
#ifndef FESA_TESTS_REFERENCE_THERMAL_REFERENCE_COMPARISON_H_
#define FESA_TESTS_REFERENCE_THERMAL_REFERENCE_COMPARISON_H_
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
#include "fesa/core/status.h"
#include "reference_tolerance_policy.h"
namespace fesa::test {
struct ThermalReferenceRow {
std::string instance;
std::uint64_t label;
double value;
};
struct ThermalReferenceSnapshot {
std::string step;
double time = 0;
std::uint64_t increment = 0;
int source_dof = 0;
std::vector<ThermalReferenceRow> rows;
std::string source_input_identity;
};
struct ThermalReferenceRowDecision {
std::string instance;
std::uint64_t label;
int source_dof;
double actual;
double reference;
ReferenceToleranceRowDecision tolerance;
};
struct ThermalReferenceReport {
// Numerical family verdict, retained even when the original artifact is
// classified as nonblocking diagnostic by the approved option A decision.
bool passed = false;
std::vector<ThermalReferenceRowDecision> rows;
ReferenceToleranceFamilyDecision family;
};
/// Identity checks precede the unchanged common tolerance policy. Expected
/// labels come from the actual input Domain, never CSV row order.
Result<ThermalReferenceReport> CompareThermalSnapshots(
const ThermalReferenceSnapshot& actual,
const ThermalReferenceSnapshot& reference,
const std::vector<std::uint64_t>& expected_labels,
const std::string& instance);
Result<ThermalReferenceSnapshot> ReadThermalReferenceCsv(
const std::filesystem::path& path, const std::string& sole_instance);
Result<ThermalReferenceSnapshot> ReadThermalReferenceHdf5(
const std::filesystem::path& path);
Result<ThermalReferenceReport> CompareThermalReferenceFiles(
const std::filesystem::path& input, const std::filesystem::path& csv,
const std::filesystem::path& hdf5);
} // namespace fesa::test
#endif
@@ -0,0 +1,330 @@
#include "thermal_reference_comparison.h"
#include <gtest/gtest.h>
#include <hdf5.h>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <limits>
#include <random>
#include <stdexcept>
namespace {
using fesa::test::ThermalReferenceSnapshot;
class TemporaryFolder {
public:
TemporaryFolder() {
static std::atomic<unsigned> next{0};
do {
path =
std::filesystem::temp_directory_path() /
("fesa-thermal-reference-" + std::to_string(std::random_device{}()) +
"-" + std::to_string(next++));
} while (!std::filesystem::create_directory(path));
}
~TemporaryFolder() {
std::error_code error;
std::filesystem::remove_all(path, error);
}
std::filesystem::path path;
};
class Handle {
public:
Handle(hid_t id, herr_t (*closer)(hid_t)) : id(id), closer(closer) {
if (id < 0) throw std::runtime_error{"HDF fixture creation failed."};
}
~Handle() { closer(id); }
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
hid_t id;
private:
herr_t (*closer)(hid_t);
};
void StringAttribute(hid_t object, const char* name, const std::string& value) {
Handle type{H5Tcopy(H5T_C_S1), H5Tclose};
ASSERT_GE(H5Tset_size(type.id, H5T_VARIABLE), 0);
ASSERT_GE(H5Tset_cset(type.id, H5T_CSET_UTF8), 0);
Handle space{H5Screate(H5S_SCALAR), H5Sclose};
Handle attr{
H5Acreate2(object, name, type.id, space.id, H5P_DEFAULT, H5P_DEFAULT),
H5Aclose};
const char* text = value.c_str();
ASSERT_GE(H5Awrite(attr.id, type.id, &text), 0);
}
template <class T>
void ScalarAttribute(hid_t object, const char* name, hid_t type, T value) {
Handle space{H5Screate(H5S_SCALAR), H5Sclose};
Handle attr{
H5Acreate2(object, name, type, space.id, H5P_DEFAULT, H5P_DEFAULT),
H5Aclose};
ASSERT_GE(H5Awrite(attr.id, type, &value), 0);
}
void Dataset(hid_t file, const char* path, hid_t type,
const std::vector<hsize_t>& shape, const void* data) {
Handle space{
H5Screate_simple(static_cast<int>(shape.size()), shape.data(), nullptr),
H5Sclose};
Handle dataset{H5Dcreate2(file, path, type, space.id, H5P_DEFAULT,
H5P_DEFAULT, H5P_DEFAULT),
H5Dclose};
ASSERT_GE(H5Dwrite(dataset.id, type, H5S_ALL, H5S_ALL, H5P_DEFAULT, data), 0);
}
// Minimal independent schema fixture for the comparator (not a solver output
// or a replacement for the immutable Abaqus artifact).
void HdfFixture(const std::filesystem::path& path, int defect) {
Handle file{
H5Fcreate(path.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT),
H5Fclose};
for (const auto* name : {"/metadata", "/model", "/steps", "/steps/Step-1",
"/steps/Step-1/frames", "/steps/Step-1/frames/37",
"/steps/Step-1/frames/37/nodal"}) {
Handle group{
H5Gcreate2(file.id, name, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
H5Gclose};
}
Handle metadata{H5Gopen2(file.id, "/metadata", H5P_DEFAULT), H5Gclose};
ScalarAttribute<std::uint64_t>(metadata.id, "schema_version", H5T_STD_U64LE,
1);
StringAttribute(metadata.id, "feature_id", "shell-transient-heat-transfer");
StringAttribute(metadata.id, "analysis_procedure",
"uncoupled-transient-heat-transfer");
StringAttribute(metadata.id, "source_step_name", "Step-1");
StringAttribute(metadata.id, "source_input_identity",
"synthetic-comparator-fixture");
StringAttribute(metadata.id, "unit_system_label",
"user-consistent-unspecified");
std::uint64_t dofs[5] = {11, 12, 13, 14, 15};
if (defect == 1) dofs[0] = 15;
Dataset(file.id, "/model/temperature_dofs", H5T_STD_U64LE, {5}, dofs);
double zeta[5] = {-1, -.5, 0, .5, 1};
if (defect == 2) zeta[0] = 0;
Dataset(file.id, "/model/temperature_zeta", H5T_IEEE_F64LE, {5}, zeta);
const std::uint64_t active[2] = {1, 0};
Dataset(file.id, "/model/thermal_node_ids", H5T_STD_U64LE, {2}, active);
struct Node {
std::uint64_t id;
const char* instance;
const char* label;
};
Node nodes[2] = {{0, "Part-1-1", "7"}, {1, "Part-1-1", "20"}};
if (defect == 9) nodes[1].id = 0;
if (defect == 10) nodes[1].instance = "Wrong";
if (defect == 11) nodes[1].label = "7";
Handle string{H5Tcopy(H5T_C_S1), H5Tclose};
H5Tset_size(string.id, H5T_VARIABLE);
H5Tset_cset(string.id, H5T_CSET_UTF8);
Handle nt{H5Tcreate(H5T_COMPOUND, sizeof(Node)), H5Tclose};
H5Tinsert(nt.id, "internal_node_id", HOFFSET(Node, id), H5T_STD_U64LE);
H5Tinsert(nt.id, "instance_name", HOFFSET(Node, instance), string.id);
if (defect != 14)
H5Tinsert(nt.id, "source_label", HOFFSET(Node, label), string.id);
Dataset(file.id, "/model/nodes", nt.id, {2}, nodes);
struct Frame {
std::uint64_t frame, increment;
double time, delta;
std::uint8_t initial;
};
std::vector<Frame> frames;
for (std::uint64_t i = 0; i < 101; ++i)
frames.push_back({i, i, static_cast<double>(i), i ? 1. : 0.,
static_cast<std::uint8_t>(i == 0)});
if (defect == 6) frames[37].frame = 36;
if (defect == 7) frames.pop_back();
if (defect == 8) frames.push_back(frames.back());
Handle ft{H5Tcreate(H5T_COMPOUND, sizeof(Frame)), H5Tclose};
H5Tinsert(ft.id, "frame_index", HOFFSET(Frame, frame), H5T_STD_U64LE);
H5Tinsert(ft.id, "increment_index", HOFFSET(Frame, increment), H5T_STD_U64LE);
H5Tinsert(ft.id, "time", HOFFSET(Frame, time), H5T_IEEE_F64LE);
H5Tinsert(ft.id, "time_increment", HOFFSET(Frame, delta), H5T_IEEE_F64LE);
H5Tinsert(ft.id, "is_initial", HOFFSET(Frame, initial), H5T_STD_U8LE);
Dataset(file.id, "/steps/Step-1/accepted_frames", ft.id, {frames.size()},
frames.data());
Handle group{H5Gopen2(file.id, "/steps/Step-1/frames/37", H5P_DEFAULT),
H5Gclose};
ScalarAttribute<double>(group.id, "time", H5T_IEEE_F64LE,
defect == 12 ? 38. : 37.);
ScalarAttribute<std::uint64_t>(group.id, "frame_index", H5T_STD_U64LE, 37);
ScalarAttribute<std::uint64_t>(group.id, "increment_index", H5T_STD_U64LE,
37);
ScalarAttribute<std::uint8_t>(group.id, "is_initial", H5T_STD_U8LE, 0);
double values[10] = {1000, 1, 2, 3, 4, -15.5742216, 5, 6, 7, 8};
if (defect == 5) values[8] = std::numeric_limits<double>::quiet_NaN();
Dataset(file.id, "/steps/Step-1/frames/37/nodal/temperature",
defect == 4 ? H5T_STD_U64LE : H5T_IEEE_F64LE,
{2, defect == 3 ? 4U : 5U}, values);
Handle temperature{
H5Dopen2(file.id, "/steps/Step-1/frames/37/nodal/temperature",
H5P_DEFAULT),
H5Dclose};
StringAttribute(
temperature.id, "component_order",
defect == 13 ? "NT15,NT14,NT13,NT12,NT11" : "NT11,NT12,NT13,NT14,NT15");
StringAttribute(temperature.id, "units_label", "temperature");
StringAttribute(temperature.id, "coordinate_system", "scalar");
StringAttribute(temperature.id, "location", "nodal-thickness-stations");
}
TEST(ThermalReferenceComparison,
HdfSchemaMappingRejectsMalformedStationFrameAndNodeData) {
TemporaryFolder folder;
const auto path = folder.path / "fixture.h5";
for (int defect = 0; defect <= 14; ++defect) {
SCOPED_TRACE(defect);
HdfFixture(path, defect);
const auto read = fesa::test::ReadThermalReferenceHdf5(path);
if (defect == 0 || defect == 10 || defect == 11) {
ASSERT_TRUE(read.HasValue())
<< (read.HasValue() ? "" : read.GetStatus().Diagnostics()[0].message);
ThermalReferenceSnapshot reference{
"Step-1",
37,
37,
11,
{{"Part-1-1", 7, -15.5742216}, {"Part-1-1", 20, 1000}}};
const auto result = fesa::test::CompareThermalSnapshots(
read.Value(), reference, {7, 20}, "Part-1-1");
EXPECT_EQ(result.HasValue(), defect == 0);
if (result.HasValue()) EXPECT_TRUE(result.Value().passed);
} else
EXPECT_FALSE(read.HasValue());
}
}
ThermalReferenceSnapshot Snapshot() {
return {"Step-1",
37,
37,
11,
{{"Part-1-1", 7, -15.5742216},
{"Part-1-1", 20, 1000},
{"Part-1-1", 3, 0}}};
}
TEST(ThermalReferenceComparison,
MapsSourceLabelsWithoutRowOrderAndPreservesSignedValues) {
auto actual = Snapshot(), reference = Snapshot();
std::reverse(actual.rows.begin(), actual.rows.end());
const auto result = fesa::test::CompareThermalSnapshots(
actual, reference, {3, 7, 20}, "Part-1-1");
ASSERT_TRUE(result.HasValue());
EXPECT_TRUE(result.Value().passed);
ASSERT_EQ(result.Value().rows.size(), 3U);
EXPECT_EQ(result.Value().rows[1].label, 7U);
EXPECT_EQ(result.Value().rows[1].reference, -15.5742216);
EXPECT_EQ(result.Value().rows[1].tolerance.branch,
fesa::test::ReferenceToleranceBranch::kRelative);
EXPECT_EQ(result.Value().family.reference_scale, 1000);
EXPECT_EQ(result.Value().family.near_zero_band, 10);
EXPECT_FALSE(result.Value().family.blocking);
}
TEST(ThermalReferenceComparison,
RejectsMissingExtraDuplicateAndNonfiniteBeforeTolerance) {
for (int mode = 0; mode < 5; ++mode) {
for (bool mutate_actual : {false, true}) {
auto actual = Snapshot(), reference = Snapshot();
auto& rows = mutate_actual ? actual.rows : reference.rows;
switch (mode) {
case 0:
rows.pop_back();
break;
case 1:
rows.push_back({"Part-1-1", 99, 0});
break;
case 2:
rows.push_back(rows[0]);
break;
case 3:
rows[0].value = std::numeric_limits<double>::infinity();
break;
default:
rows[0].value = std::numeric_limits<double>::quiet_NaN();
break;
}
SCOPED_TRACE(mode);
SCOPED_TRACE(mutate_actual);
EXPECT_FALSE(fesa::test::CompareThermalSnapshots(actual, reference,
{3, 7, 20}, "Part-1-1")
.HasValue());
}
}
}
TEST(ThermalReferenceComparison,
RejectsWrongStepTimeIncrementInstanceAndStation) {
for (int mode = 0; mode < 5; ++mode) {
for (bool mutate_actual : {false, true}) {
auto actual = Snapshot(), reference = Snapshot();
auto& row = mutate_actual ? actual : reference;
switch (mode) {
case 0:
row.step = "Step-2";
break;
case 1:
row.time = std::nextafter(37., 38.);
break;
case 2:
row.increment = 38;
break;
case 3:
row.rows[0].instance = "Other";
break;
default:
row.source_dof = 15;
break;
}
SCOPED_TRACE(mode);
SCOPED_TRACE(mutate_actual);
EXPECT_FALSE(fesa::test::CompareThermalSnapshots(actual, reference,
{3, 7, 20}, "Part-1-1")
.HasValue());
}
}
}
TEST(ThermalReferenceComparison,
UsesUnmodifiedFivePercentOnePercentAndExactZeroPolicy) {
auto actual = Snapshot(), reference = Snapshot();
actual.rows[0].value = 0;
auto result = fesa::test::CompareThermalSnapshots(actual, reference,
{3, 7, 20}, "Part-1-1");
ASSERT_TRUE(result.HasValue());
EXPECT_FALSE(result.Value().passed);
for (auto& row : reference.rows) row.value = 0;
actual = reference;
actual.rows[0].value = std::numeric_limits<double>::denorm_min();
result = fesa::test::CompareThermalSnapshots(actual, reference, {3, 7, 20},
"Part-1-1");
ASSERT_TRUE(result.HasValue());
EXPECT_FALSE(result.Value().passed);
EXPECT_EQ(result.Value().family.reference_scale, 0);
}
TEST(ThermalReferenceComparison,
CsvRequiresExactHeaderFrameFiniteValuesAndSoleInstance) {
static std::atomic<unsigned> next{0};
std::filesystem::path directory;
do {
directory = std::filesystem::temp_directory_path() /
("fesa-thermal-csv-" + std::to_string(std::random_device{}()) +
"-" + std::to_string(next++));
} while (!std::filesystem::create_directory(directory));
const auto path = directory / "fixture.csv";
const std::string header = "Step,Frame,Node Label,NT11\n";
const std::string valid =
"Step-1,Increment 37: Step Time = 37.00,7,-15.5742216\n";
for (const auto& text :
{header + valid, std::string{"Step,Frame,Node Label,NT15\n"} + valid,
header + "Step-1,Increment 38: Step Time = 37.00,7,0\n",
header + "Step-2,Increment 37: Step Time = 37.00,7,0\n",
header + "Step-1,Increment 37: Step Time = 37.00,7,nan\n",
header + "Step-1,Increment 37: Step Time = 37.00,7,0,extra\n"}) {
{
std::ofstream out{path};
out << text;
}
const auto result = fesa::test::ReadThermalReferenceCsv(path, "Part-1-1");
EXPECT_EQ(result.HasValue(), text == header + valid);
EXPECT_FALSE(fesa::test::ReadThermalReferenceCsv(path, "").HasValue());
}
std::filesystem::remove_all(directory);
}
} // namespace