feat: integrate checked thermal shell operators

This commit is contained in:
KOKO\Mimi
2026-09-29 19:43:56 +09:00
11 changed files with 2009 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
#include "fesa/analysis/thermal_time_grid.h"
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
namespace fesa {
namespace {
// Fixed four base-2^32 digits. Contract bounds all exact values to 122 bits.
struct UInt128 {
std::array<std::uint32_t, 4> digit{};
};
/// @brief Orders unsigned integers from their most significant digit.
int Compare(const UInt128& a, const UInt128& b) {
for (int i = 3; i >= 0; --i) {
if (a.digit[i] != b.digit[i]) return a.digit[i] < b.digit[i] ? -1 : 1;
}
return 0;
}
/// @brief Adds with explicit carry; false means the fixed storage overflowed.
bool Add(const UInt128& a, const UInt128& b, UInt128& result) {
std::uint64_t carry = 0;
for (std::size_t i = 0; i < 4; ++i) {
const std::uint64_t sum = std::uint64_t{a.digit[i]} + b.digit[i] + carry;
result.digit[i] = static_cast<std::uint32_t>(sum);
carry = sum >> 32;
}
return carry == 0;
}
/// @brief Subtracts nonnegative magnitudes with explicit borrow.
bool Subtract(const UInt128& a, const UInt128& b, UInt128& result) {
std::uint64_t borrow = 0;
for (std::size_t i = 0; i < 4; ++i) {
const std::uint64_t sub = std::uint64_t{b.digit[i]} + borrow;
result.digit[i] =
static_cast<std::uint32_t>(std::uint64_t{a.digit[i]} - sub);
borrow = std::uint64_t{a.digit[i]} < sub ? 1U : 0U;
}
return borrow == 0;
}
/// @brief Places a significand on the exact 2^-88 scaled lattice.
bool Shift(std::uint64_t value, int shift, UInt128& result) {
result = {};
if (shift < 0) return false;
for (int bit = 0; bit < 64; ++bit) {
if ((value & (std::uint64_t{1} << bit)) == 0) continue;
const int target = bit + shift;
if (target >= 128) return false;
result.digit[target / 32] |= std::uint32_t{1} << (target % 32);
}
return true;
}
/// @brief Multiplies by at most 2^32+1 using bounded schoolbook products.
bool Multiply(const UInt128& a, std::uint64_t multiplier, UInt128& result) {
result = {};
for (std::size_t j = 0; j < 2; ++j) {
const auto factor = static_cast<std::uint32_t>(multiplier >> (32 * j));
std::uint64_t carry = 0;
for (std::size_t i = 0; i < 4; ++i) {
if (i + j >= 4) {
if (a.digit[i] != 0 && factor != 0) return false;
continue;
}
const std::uint64_t product =
std::uint64_t{a.digit[i]} * factor + result.digit[i + j] + carry;
result.digit[i + j] = static_cast<std::uint32_t>(product);
carry = product >> 32;
}
if (carry != 0) return false;
}
return true;
}
struct Cell {
UInt128 center;
UInt128 lower;
UInt128 upper;
bool closed;
};
/// @brief Constructs ORIGINAL binary64 cells, including subnormal/maxfinite
/// gaps.
bool MakeCell(double value, int period_exponent, Cell& cell) {
std::uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
const int exponent_bits = static_cast<int>((bits >> 52) & 0x7ffU);
const std::uint64_t fraction = bits & ((std::uint64_t{1} << 52) - 1);
const std::uint64_t significand =
exponent_bits == 0 ? fraction : fraction + (std::uint64_t{1} << 52);
const int exponent = exponent_bits == 0 ? -1074 : exponent_bits - 1075;
const int lattice_shift = exponent - period_exponent + 88;
UInt128 lower_gap;
UInt128 upper_gap;
// At a normal binade boundary only the predecessor gap is halved. The
// smallest normal has subnormal neighbors with the same original spacing.
const int boundary = fraction == 0 && exponent_bits > 1 ? 1 : 0;
cell.closed = (bits & 1) == 0;
return Shift(significand, lattice_shift, cell.center) &&
Shift(1, lattice_shift - 1 - boundary, lower_gap) &&
Shift(1, lattice_shift - 1, upper_gap) &&
Subtract(cell.center, lower_gap, cell.lower) &&
Add(cell.center, upper_gap, cell.upper);
}
/// @brief Returns source-neutral utility failure for parser/procedure
/// decoration.
Status GridFailure(const char* code, const char* message) {
return Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, {}, "HEAT TRANSFER", "", message}});
}
} // namespace
Result<ThermalTimeGrid> ThermalTimeGrid::Create(double period, double increment,
std::uint32_t maximum) {
if (!std::isfinite(period) || !std::isfinite(increment) || period <= 0 ||
increment <= 0 || maximum == 0) {
return Result<ThermalTimeGrid>::Failure(
GridFailure("invalid-thermal-time-grid",
"Time controls must be finite and positive."));
}
if (increment >= period) {
return Result<ThermalTimeGrid>::Success(
ThermalTimeGrid{period, increment, 1U, period});
}
const double quotient = period / increment;
const std::uint64_t limit = std::uint64_t{maximum} + 1U;
if (!std::isfinite(quotient) || quotient > static_cast<double>(limit)) {
return Result<ThermalTimeGrid>::Failure(GridFailure(
"thermal-increment-limit", "Thermal increment count exceeds INC."));
}
// No floating-to-integer cast precedes the finite, positive count bound.
std::uint64_t floor = static_cast<std::uint64_t>(std::floor(quotient));
const double fraction = quotient - static_cast<double>(floor);
const std::uint64_t nearest =
floor + ((fraction > .5 || (fraction == .5 && (floor & 1) != 0)) ? 1 : 0);
int exponent = 0;
std::frexp(period, &exponent);
Cell period_cell{};
Cell increment_cell{};
UInt128 lower;
UInt128 upper;
bool exact_ok = MakeCell(period, exponent, period_cell) &&
MakeCell(increment, exponent, increment_cell) &&
Multiply(increment_cell.lower, nearest, lower) &&
Multiply(increment_cell.upper, nearest, upper);
const int first = Compare(period_cell.upper, lower);
const int second = Compare(upper, period_cell.lower);
const bool closed = period_cell.closed && increment_cell.closed;
const bool close = nearest != 0 && (first > 0 || (first == 0 && closed)) &&
(second > 0 || (second == 0 && closed));
std::uint64_t count = nearest;
if (!close && exact_ok) {
UInt128 product;
UInt128 next;
exact_ok = Multiply(increment_cell.center, floor, product) &&
Multiply(increment_cell.center, floor + 1, next);
if (Compare(product, period_cell.center) > 0) {
--floor;
} else if (Compare(next, period_cell.center) <= 0) {
++floor;
}
exact_ok = exact_ok && Multiply(increment_cell.center, floor, product) &&
Multiply(increment_cell.center, floor + 1, next) &&
Compare(product, period_cell.center) <= 0 &&
Compare(next, period_cell.center) > 0;
count = floor + (Compare(product, period_cell.center) == 0 ? 0U : 1U);
}
if (!exact_ok) {
return Result<ThermalTimeGrid>::Failure(GridFailure(
"invalid-thermal-time-grid", "Exact time lattice bounds failed."));
}
if (count == 0 || count > maximum) {
return Result<ThermalTimeGrid>::Failure(GridFailure(
"thermal-increment-limit", "Thermal increment count exceeds INC."));
}
const double last =
close ? increment
: std::fma(-static_cast<double>(count - 1), increment, period);
ThermalTimeGrid grid{period, increment, static_cast<std::uint32_t>(count),
last};
const auto frame = grid.Frame(grid.Count());
if (!frame.HasValue())
return Result<ThermalTimeGrid>::Failure(frame.GetStatus());
// For j <= 2^32, adjacent exact j*d are separated by more than 2^19 ULPs
// (subnormal products are exact). Thus the terminal check also suffices for
// O(1) preflight; every requested frame still checks its own stored interval.
return Result<ThermalTimeGrid>::Success(grid);
}
ThermalTimeGrid::ThermalTimeGrid(double period, double increment,
std::uint32_t count, double final_delta)
: period_{period},
increment_{increment},
count_{count},
final_delta_{final_delta} {}
std::uint32_t ThermalTimeGrid::Count() const noexcept { return count_; }
double ThermalTimeGrid::Time(std::uint32_t index) const noexcept {
return index == count_ ? period_ : static_cast<double>(index) * increment_;
}
double ThermalTimeGrid::Delta(std::uint32_t index) const noexcept {
return index == count_ ? final_delta_ : increment_;
}
Result<ThermalTimeFrame> ThermalTimeGrid::Frame(std::uint32_t index) const {
if (index == 0 || index > count_) {
return Result<ThermalTimeFrame>::Failure(
GridFailure("invalid-thermal-time-frame",
"Completed frame index is outside the grid."));
}
const double time = Time(index);
const double delta = Delta(index);
const double previous = Time(index - 1);
if (!std::isfinite(time) || !std::isfinite(previous) ||
!std::isfinite(delta) || !(time > previous) || !(delta > 0)) {
return Result<ThermalTimeFrame>::Failure(
GridFailure("invalid-thermal-time-frame",
"Stored times must be finite and strictly increasing."));
}
return Result<ThermalTimeFrame>::Success({time, delta});
}
} // namespace fesa
+108
View File
@@ -0,0 +1,108 @@
#ifndef FESA_ELEMENTS_THERMAL_INTERVAL_H_
#define FESA_ELEMENTS_THERMAL_INTERVAL_H_
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
namespace fesa::thermal_internal {
/// @brief Encloses binary64 operations with one outward nextafter per endpoint.
struct Interval {
double lower = 0;
double upper = 0;
Interval() = default;
explicit Interval(double value) : lower{value}, upper{value} {}
Interval(double low, double high) : lower{low}, upper{high} {}
bool IsFinite() const {
return std::isfinite(lower) && std::isfinite(upper) && lower <= upper;
}
bool ContainsZero() const { return lower <= 0 && upper >= 0; }
};
/// @brief Rounds an operation endpoint toward negative infinity.
inline double Down(double value) {
return std::nextafter(value, -std::numeric_limits<double>::infinity());
}
/// @brief Rounds an operation endpoint toward positive infinity.
inline double Up(double value) {
return std::nextafter(value, std::numeric_limits<double>::infinity());
}
inline Interval operator+(Interval a, Interval b) {
return {Down(a.lower + b.lower), Up(a.upper + b.upper)};
}
inline Interval operator-(Interval a, Interval b) {
return {Down(a.lower - b.upper), Up(a.upper - b.lower)};
}
inline Interval operator*(Interval a, Interval b) {
const std::array<double, 4> products{{a.lower * b.lower, a.lower * b.upper,
a.upper * b.lower, a.upper * b.upper}};
return {Down(*std::min_element(products.begin(), products.end())),
Up(*std::max_element(products.begin(), products.end()))};
}
inline Interval operator/(Interval a, Interval b) {
if (b.ContainsZero())
return {-std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::infinity()};
const std::array<double, 4> quotients{{a.lower / b.lower, a.lower / b.upper,
a.upper / b.lower, a.upper / b.upper}};
return {Down(*std::min_element(quotients.begin(), quotients.end())),
Up(*std::max_element(quotients.begin(), quotients.end()))};
}
/// @brief Encloses the square without introducing negative cancellation bounds.
inline Interval Square(Interval value) {
const double a = value.lower * value.lower;
const double b = value.upper * value.upper;
return {value.ContainsZero() ? 0 : Down(std::min(a, b)), Up(std::max(a, b))};
}
/// @brief Encloses a real square root; negative upper endpoints remain invalid.
inline Interval Sqrt(Interval value) {
return {value.lower <= 0 ? 0 : Down(std::sqrt(value.lower)),
Up(std::sqrt(value.upper))};
}
/// @brief Scales exactly by powers of two, enclosing subnormal loss if present.
inline Interval Scale(Interval value, int exponent) {
const double low = std::ldexp(value.lower, exponent);
const double high = std::ldexp(value.upper, exponent);
const bool low_exact =
std::isfinite(low) && std::ldexp(low, -exponent) == value.lower;
const bool high_exact =
std::isfinite(high) && std::ldexp(high, -exponent) == value.upper;
return {low_exact ? low : Down(low), high_exact ? high : Up(high)};
}
using IntervalVector = std::array<Interval, 3>;
/// @brief Performs componentwise vector addition in fixed Cartesian order.
inline IntervalVector operator+(const IntervalVector& a,
const IntervalVector& b) {
return {{a[0] + b[0], a[1] + b[1], a[2] + b[2]}};
}
/// @brief Performs componentwise vector subtraction in fixed Cartesian order.
inline IntervalVector operator-(const IntervalVector& a,
const IntervalVector& b) {
return {{a[0] - b[0], a[1] - b[1], a[2] - b[2]}};
}
/// @brief Scales a vector by an interval scalar.
inline IntervalVector operator*(Interval s, const IntervalVector& a) {
return {{s * a[0], s * a[1], s * a[2]}};
}
/// @brief Accumulates a three-component inner product in fixed order.
inline Interval Dot(const IntervalVector& a, const IntervalVector& b) {
return (a[0] * b[0] + a[1] * b[1]) + a[2] * b[2];
}
/// @brief Encloses a right-handed Cartesian cross product.
inline IntervalVector Cross(const IntervalVector& a, const IntervalVector& b) {
return {{a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]}};
}
/// @brief Encloses a squared norm as a sum of nonnegative squares.
inline Interval NormSquared(const IntervalVector& a) {
return (Square(a[0]) + Square(a[1])) + Square(a[2]);
}
/// @brief Checks all Cartesian components for finite ordered endpoints.
inline bool IsFinite(const IntervalVector& a) {
return a[0].IsFinite() && a[1].IsFinite() && a[2].IsFinite();
}
} // namespace fesa::thermal_internal
#endif // FESA_ELEMENTS_THERMAL_INTERVAL_H_
+236
View File
@@ -0,0 +1,236 @@
#include "fesa/elements/thermal_shell.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <utility>
#include "thermal_shell_geometry.h"
namespace fesa {
namespace {
using thermal_internal::Interval;
/// @brief Measures the full endpoint deviation from a computed coefficient.
double ErrorBound(Interval interval, double value) {
return thermal_internal::Up(std::max(std::abs(interval.lower - value),
std::abs(interval.upper - value)));
}
} // namespace
ThermalShell::ThermalShell(SourceEntityId source,
std::array<EntityIndex, 4> node_indices,
SourceLocation location, double conductivity)
: source_{std::move(source)},
node_indices_{node_indices},
location_{std::move(location)},
conductivity_value_{conductivity} {}
Result<ThermalShell> ThermalShell::Create(
SourceEntityId source, std::array<EntityIndex, 4> node_indices,
const std::array<Vector3, 4>& coordinates, double conductivity,
double density, double specific_heat, double thickness,
SourceLocation location) {
using namespace thermal_internal;
for (const double property :
{conductivity, density, specific_heat, thickness}) {
if (!(property > 0) || !std::isfinite(property)) {
return Result<ThermalShell>::Failure(
GeometryFailure(source, location, "invalid-thermal-shell-material",
"Thermal properties must be finite and positive."));
}
}
auto sorted = node_indices;
std::sort(sorted.begin(), sorted.end());
if (std::adjacent_find(sorted.begin(), sorted.end()) != sorted.end()) {
return Result<ThermalShell>::Failure(
GeometryFailure(source, location, "invalid-thermal-shell-geometry",
"Patch contains repeated source nodes."));
}
const auto geometry = BuildGeometry(coordinates, source, location);
if (!geometry.HasValue())
return Result<ThermalShell>::Failure(geometry.GetStatus());
ThermalShell kernel{source, node_indices, location, conductivity};
Matrix mass{4, 4};
Matrix surface{4, 4};
std::array<std::array<Interval, 4>, 4> mass_interval{};
std::array<std::array<Interval, 4>, 4> surface_interval{};
const double gauss = std::sqrt(3. / 5.);
const Interval gauss_interval = Sqrt(Interval{3} / Interval{5});
const std::array<double, 3> points{{-gauss, 0., gauss}};
const std::array<Interval, 3> point_intervals{
{Interval{0} - gauss_interval, Interval{0}, gauss_interval}};
const std::array<double, 3> weights{{5. / 9., 8. / 9., 5. / 9.}};
const std::array<Interval, 3> weight_intervals{{Interval{5} / Interval{9},
Interval{8} / Interval{9},
Interval{5} / Interval{9}}};
for (std::size_t x = 0; x < 3; ++x) {
for (std::size_t y = 0; y < 3; ++y) {
const auto result = EvaluateSurface(geometry.Value(), points[x],
points[y], point_intervals[x],
point_intervals[y], source, location);
if (!result.HasValue())
return Result<ThermalShell>::Failure(result.GetStatus());
const auto& point = result.Value();
const double weight = weights[x] * weights[y] * point.jacobian;
const Interval weight_interval =
weight_intervals[x] * weight_intervals[y] * point.jacobian_interval;
for (std::size_t i = 0; i < 4; ++i) {
for (std::size_t j = 0; j < 4; ++j) {
mass(i, j) += point.shape[i] * point.shape[j] * weight;
surface(i, j) += point.gradient[i].Dot(point.gradient[j]) * weight;
mass_interval[i][j] = mass_interval[i][j] +
point.shape_interval[i] *
point.shape_interval[j] * weight_interval;
surface_interval[i][j] =
surface_interval[i][j] +
Dot(point.gradient_interval[i], point.gradient_interval[j]) *
weight_interval;
}
}
}
}
Matrix z0{5, 5};
Matrix z1{5, 5};
std::array<std::array<Interval, 5>, 5> z0_interval{};
std::array<std::array<Interval, 5>, 5> z1_interval{};
constexpr double kZ0[3][3] = {{4, 2, -1}, {2, 16, 2}, {-1, 2, 4}};
constexpr double kZ1[3][3] = {{7, -8, 1}, {-8, 16, -8}, {1, -8, 7}};
for (const std::size_t first : {0U, 2U}) {
for (std::size_t a = 0; a < 3; ++a) {
for (std::size_t b = 0; b < 3; ++b) {
z0(first + a, first + b) += kZ0[a][b] / 30.;
z1(first + a, first + b) += kZ1[a][b] / 3.;
z0_interval[first + a][first + b] = z0_interval[first + a][first + b] +
Interval{kZ0[a][b]} / Interval{30};
z1_interval[first + a][first + b] = z1_interval[first + a][first + b] +
Interval{kZ1[a][b]} / Interval{3};
}
}
}
for (std::size_t i = 0; i < 4; ++i) {
for (std::size_t j = 0; j < 4; ++j) {
for (std::size_t a = 0; a < 5; ++a) {
for (std::size_t b = 0; b < 5; ++b) {
const std::size_t p = 5 * i + a;
const std::size_t q = 5 * j + b;
kernel.capacity_(p, q) =
density * specific_heat * (thickness / 2) * mass(i, j) * z0(a, b);
kernel.conductivity_(p, q) =
conductivity * ((thickness / 2) * surface(i, j) * z0(a, b) +
(2 / thickness) * mass(i, j) * z1(a, b));
const Interval capacity = Interval{density} *
Interval{specific_heat} *
(Interval{thickness} / Interval{2}) *
mass_interval[i][j] * z0_interval[a][b];
const Interval conduction =
Interval{conductivity} *
((Interval{thickness} / Interval{2}) * surface_interval[i][j] *
z0_interval[a][b] +
(Interval{2} / Interval{thickness}) * mass_interval[i][j] *
z1_interval[a][b]);
kernel.capacity_error_(p, q) =
ErrorBound(capacity, kernel.capacity_(p, q));
kernel.conductivity_error_(p, q) =
ErrorBound(conduction, kernel.conductivity_(p, q));
if (!capacity.IsFinite() || !conduction.IsFinite() ||
!std::isfinite(kernel.capacity_(p, q)) ||
!std::isfinite(kernel.conductivity_(p, q)) ||
!std::isfinite(kernel.capacity_error_(p, q)) ||
!std::isfinite(kernel.conductivity_error_(p, q)) ||
(p == q && (!(kernel.capacity_(p, q) > 0) ||
!(kernel.conductivity_(p, q) > 0)))) {
return Result<ThermalShell>::Failure(GeometryFailure(
source, location, "nonfinite-thermal-shell-operator",
"Local matrix/enclosure is not finite or a diagonal lost "
"positivity."));
}
}
}
}
}
const double recovery = 1 / std::sqrt(3.);
const Interval recovery_interval = Interval{1} / Sqrt(Interval{3});
constexpr std::array<double, 4> kXi{{-1, 1, 1, -1}};
constexpr std::array<double, 4> kEta{{-1, -1, 1, 1}};
constexpr std::array<double, 6> kLocal{{-1, 0, 1, -1, 0, 1}};
for (std::size_t ip = 0; ip < 4; ++ip) {
const auto result = EvaluateSurface(
geometry.Value(), kXi[ip] * recovery, kEta[ip] * recovery,
Interval{kXi[ip]} * recovery_interval,
Interval{kEta[ip]} * recovery_interval, source, location);
if (!result.HasValue())
return Result<ThermalShell>::Failure(result.GetStatus());
const auto& point = result.Value();
kernel.frames_[ip] = point.axes;
for (std::size_t trace = 0; trace < 6; ++trace) {
const double s = kLocal[trace];
const std::array<double, 3> shape{
{.5 * s * (s - 1), 1 - s * s, .5 * s * (s + 1)}};
const std::array<double, 3> derivative{{2 * s - 1, -4 * s, 2 * s + 1}};
const std::size_t first = trace < 3 ? 0 : 2;
for (std::size_t i = 0; i < 4; ++i) {
for (std::size_t a = 0; a < 3; ++a) {
const Vector3 gradient =
shape[a] * point.gradient[i] +
(point.shape[i] * (2 / thickness) * derivative[a]) *
point.axes[2];
if (!gradient.IsFinite()) {
return Result<ThermalShell>::Failure(GeometryFailure(
source, location, "nonfinite-thermal-shell-recovery",
"Flux gradient is not finite."));
}
kernel.gradients_[6 * ip + trace][5 * i + first + a] = gradient;
}
}
}
}
return Result<ThermalShell>::Success(std::move(kernel));
}
const SourceEntityId& ThermalShell::SourceId() const noexcept {
return source_;
}
const std::array<EntityIndex, 4>& ThermalShell::NodeIndices() const noexcept {
return node_indices_;
}
const Matrix& ThermalShell::Conductivity() const noexcept {
return conductivity_;
}
const Matrix& ThermalShell::Capacity() const noexcept { return capacity_; }
const Matrix& ThermalShell::ConductivityError() const noexcept {
return conductivity_error_;
}
const Matrix& ThermalShell::CapacityError() const noexcept {
return capacity_error_;
}
Result<ThermalShellRecovery> ThermalShell::Recover(
const std::array<double, 20>& temperature) const {
for (const double value : temperature) {
if (!std::isfinite(value)) {
return Result<ThermalShellRecovery>::Failure(
thermal_internal::GeometryFailure(
source_, location_, "nonfinite-thermal-shell-temperature",
"Recovery temperature is not finite."));
}
}
ThermalShellRecovery recovery{};
recovery.frames = frames_;
for (std::size_t row = 0; row < 24; ++row) {
Vector3 gradient;
for (std::size_t p = 0; p < 20; ++p)
gradient = gradient + temperature[p] * gradients_[row][p];
recovery.global_flux[row] = -conductivity_value_ * gradient;
if (!recovery.global_flux[row].IsFinite()) {
return Result<ThermalShellRecovery>::Failure(
thermal_internal::GeometryFailure(
source_, location_, "nonfinite-thermal-shell-flux",
"Recovered heat flux is not finite."));
}
}
return Result<ThermalShellRecovery>::Success(recovery);
}
} // namespace fesa
@@ -0,0 +1,431 @@
#include "thermal_shell_geometry.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "fesa/model/model_types.h"
namespace fesa {
namespace thermal_internal {
namespace {
/// @brief Represents an exact input vector by three singleton intervals.
IntervalVector Singleton(const Vector3& point) {
return {{Interval{point.X()}, Interval{point.Y()}, Interval{point.Z()}}};
}
/// @brief Checks a candidate direction against the ORIGINAL interval geometry.
bool Certify(const Geometry& geometry, Vector3 direction, double& lower) {
const double magnitude = direction.Norm();
if (!(magnitude > 0) || !std::isfinite(magnitude)) return false;
direction = direction / magnitude;
const auto s = Singleton(direction);
lower = std::numeric_limits<double>::infinity();
for (double xi : {-1., 1.}) {
for (double eta : {-1., 1.}) {
const auto w = Cross(geometry.bi + Interval{eta} * geometry.di,
geometry.ci + Interval{xi} * geometry.di);
const Interval projection = Dot(s, w);
if (!projection.IsFinite() || !(projection.lower > 0)) return false;
lower = std::min(lower, projection.lower);
}
}
const Interval bound = Interval{lower} / Sqrt(NormSquared(s));
lower = bound.lower;
return bound.IsFinite() && lower > 0;
}
} // namespace
Status GeometryFailure(const SourceEntityId& source,
const SourceLocation& location, const char* code,
const char* message) {
return Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, "DS4",
source.instance_name + ":" + source.source_label_text, message}});
}
Result<Geometry> BuildGeometry(const std::array<Vector3, 4>& coordinates,
const SourceEntityId& source,
const SourceLocation& location) {
double largest = 0;
for (std::size_t i = 0; i < 4; ++i) {
if (!coordinates[i].IsFinite()) {
return Result<Geometry>::Failure(
GeometryFailure(source, location, "invalid-thermal-shell-geometry",
"Coordinates must be finite."));
}
for (std::size_t j = 0; j < i; ++j) {
if (coordinates[i] == coordinates[j]) {
return Result<Geometry>::Failure(
GeometryFailure(source, location, "invalid-thermal-shell-geometry",
"Patch has repeated coordinates."));
}
}
for (double component : coordinates[i].Components()) {
largest = std::max(largest, std::abs(component));
}
}
int coordinate_exponent = 0;
std::frexp(largest, &coordinate_exponent);
std::array<Vector3, 4> offsets{};
std::array<IntervalVector, 4> intervals{};
double length = 0;
// Scaling before subtraction prevents overflow even for opposite huge inputs.
for (std::size_t i = 1; i < 4; ++i) {
std::array<double, 3> offset{};
for (std::size_t j = 0; j < 3; ++j) {
offset[j] = std::ldexp(coordinates[i][j], -coordinate_exponent) -
std::ldexp(coordinates[0][j], -coordinate_exponent);
intervals[i][j] =
Scale(Interval{coordinates[i][j]}, -coordinate_exponent) -
Scale(Interval{coordinates[0][j]}, -coordinate_exponent);
length = std::max(length, std::abs(offset[j]));
}
offsets[i] = Vector3{offset};
}
int offset_exponent = 0;
std::frexp(length, &offset_exponent);
for (std::size_t i = 1; i < 4; ++i) {
std::array<double, 3> offset{};
for (std::size_t j = 0; j < 3; ++j) {
offset[j] = std::ldexp(offsets[i][j], -offset_exponent);
intervals[i][j] = Scale(intervals[i][j], -offset_exponent);
}
offsets[i] = Vector3{offset};
}
Geometry geometry{};
geometry.length_exponent = coordinate_exponent + offset_exponent;
geometry.b = .25 * (offsets[1] + offsets[2] - offsets[3]);
geometry.c = .25 * (offsets[2] + offsets[3] - offsets[1]);
geometry.d = .25 * (offsets[2] - offsets[1] - offsets[3]);
geometry.bi = Interval{.25} * (intervals[1] + intervals[2] - intervals[3]);
geometry.ci = Interval{.25} * (intervals[2] + intervals[3] - intervals[1]);
geometry.di = Interval{.25} * (intervals[2] - intervals[1] - intervals[3]);
const Vector3 w0 = geometry.b.Cross(geometry.c);
const Vector3 u = geometry.b.Cross(geometry.d);
const Vector3 v = geometry.d.Cross(geometry.c);
std::vector<Vector3> candidates;
candidates.reserve(10);
for (double xi : {-1., 1.}) {
for (double eta : {-1., 1.}) candidates.push_back(w0 + xi * u + eta * v);
}
// Projection here is the exact constrained-edge minimizer, not a geometry
// clamp.
for (double edge : {-1., 1.}) {
if (v.Dot(v) > 0) {
const double eta = std::clamp(-v.Dot(w0 + edge * u) / v.Dot(v), -1., 1.);
candidates.push_back(w0 + edge * u + eta * v);
}
if (u.Dot(u) > 0) {
const double xi = std::clamp(-u.Dot(w0 + edge * v) / u.Dot(u), -1., 1.);
candidates.push_back(w0 + xi * u + edge * v);
}
}
const double uu = u.Dot(u);
const double vv = v.Dot(v);
const double uv = u.Dot(v);
const double determinant = uu * vv - uv * uv;
if (determinant > 0 && std::isfinite(determinant)) {
const double xi = (uv * v.Dot(w0) - vv * u.Dot(w0)) / determinant;
const double eta = (uv * u.Dot(w0) - uu * v.Dot(w0)) / determinant;
if (std::abs(xi) <= 1 && std::abs(eta) <= 1) {
candidates.push_back(w0 + xi * u + eta * v);
}
}
// The center is also useful for affine or rank-one normal images.
candidates.push_back(w0);
for (const auto& candidate : candidates) {
if (Certify(geometry, candidate, geometry.scaled_jacobian_lower)) {
return Result<Geometry>::Success(geometry);
}
}
return Result<Geometry>::Failure(
GeometryFailure(source, location, "uncertain-thermal-shell-geometry",
"No strictly positive whole-patch interval bound was "
"certified (lower <= 0)."));
}
Result<SurfacePoint> EvaluateSurface(const Geometry& geometry, double xi,
double eta, Interval xi_interval,
Interval eta_interval,
const SourceEntityId& source,
const SourceLocation& location) {
const Vector3 a1 = geometry.b + eta * geometry.d;
const Vector3 a2 = geometry.c + xi * geometry.d;
const auto a1i = geometry.bi + eta_interval * geometry.di;
const auto a2i = geometry.ci + xi_interval * geometry.di;
const Vector3 w = a1.Cross(a2);
const auto wi = Cross(a1i, a2i);
const double jacobian = w.Norm();
const Interval determinant_interval = NormSquared(wi);
const double determinant = w.Dot(w);
// |a1 x a2|^2 is the Gram determinant identity, avoiding subtraction of
// nearly equal g11*g22 and g12*g12. This is still the actual-point metric.
const Interval inverse11 = Dot(a2i, a2i) / determinant_interval;
const Interval inverse12 =
(Interval{0} - Dot(a1i, a2i)) / determinant_interval;
const Interval inverse22 = Dot(a1i, a1i) / determinant_interval;
if (!(determinant_interval.lower > 0) || !inverse11.IsFinite() ||
!inverse12.IsFinite() || !inverse22.IsFinite() || !(jacobian > 0) ||
!std::isfinite(determinant) || determinant <= 0 ||
!Scale(inverse11, -2 * geometry.length_exponent).IsFinite() ||
!Scale(inverse12, -2 * geometry.length_exponent).IsFinite() ||
!Scale(inverse22, -2 * geometry.length_exponent).IsFinite() ||
!Scale(Dot(a1i, a1i), 2 * geometry.length_exponent).IsFinite() ||
!Scale(Dot(a2i, a2i), 2 * geometry.length_exponent).IsFinite()) {
return Result<SurfacePoint>::Failure(GeometryFailure(
source, location, "uncertain-thermal-shell-metric",
"Metric inverse enclosure is not finite or excludes no zero divisor."));
}
const Vector3 reciprocal1 = a2.Cross(w) / determinant;
const Vector3 reciprocal2 = w.Cross(a1) / determinant;
const auto reciprocal1i =
(Interval{1} / determinant_interval) * Cross(a2i, wi);
const auto reciprocal2i =
(Interval{1} / determinant_interval) * Cross(wi, a1i);
SurfacePoint point{};
point.jacobian = std::ldexp(jacobian, 2 * geometry.length_exponent);
point.jacobian_interval =
Scale(Sqrt(determinant_interval), 2 * geometry.length_exponent);
point.axes[0] = a1 / a1.Norm();
point.axes[2] = w / jacobian;
point.axes[1] = point.axes[2].Cross(point.axes[0]);
constexpr std::array<double, 4> kXi{{-1., 1., 1., -1.}};
constexpr std::array<double, 4> kEta{{-1., -1., 1., 1.}};
for (std::size_t i = 0; i < 4; ++i) {
point.shape[i] = .25 * (1 + kXi[i] * xi) * (1 + kEta[i] * eta);
point.shape_interval[i] = Interval{.25} *
(Interval{1} + Interval{kXi[i]} * xi_interval) *
(Interval{1} + Interval{kEta[i]} * eta_interval);
const double dx = .25 * kXi[i] * (1 + kEta[i] * eta);
const double dy = .25 * kEta[i] * (1 + kXi[i] * xi);
const Interval dxi = Interval{.25 * kXi[i]} *
(Interval{1} + Interval{kEta[i]} * eta_interval);
const Interval dyi = Interval{.25 * kEta[i]} *
(Interval{1} + Interval{kXi[i]} * xi_interval);
const Vector3 gradient = dx * reciprocal1 + dy * reciprocal2;
const auto gradient_interval = dxi * reciprocal1i + dyi * reciprocal2i;
std::array<double, 3> components{};
for (std::size_t j = 0; j < 3; ++j) {
components[j] = std::ldexp(gradient[j], -geometry.length_exponent);
point.gradient_interval[i][j] =
Scale(gradient_interval[j], -geometry.length_exponent);
}
point.gradient[i] = Vector3{components};
if (!point.gradient[i].IsFinite() ||
!IsFinite(point.gradient_interval[i])) {
return Result<SurfacePoint>::Failure(
GeometryFailure(source, location, "uncertain-thermal-shell-metric",
"Surface gradient enclosure is not finite."));
}
}
if (!(point.jacobian > 0) || !std::isfinite(point.jacobian) ||
!point.jacobian_interval.IsFinite() ||
!(point.jacobian_interval.lower > 0) || !point.axes[0].IsFinite() ||
!point.axes[1].IsFinite() || !point.axes[2].IsFinite()) {
return Result<SurfacePoint>::Failure(GeometryFailure(
source, location, "uncertain-thermal-shell-metric",
"Surface measure or frame is not finite and positive."));
}
return Result<SurfacePoint>::Success(point);
}
} // namespace thermal_internal
namespace {
using Edge = std::pair<EntityIndex, EntityIndex>;
/// @brief Forms a deterministic undirected source edge key.
Edge EdgeKey(EntityIndex first, EntityIndex second) {
return std::minmax(first, second);
}
/// @brief Rejects possible open-edge contact only for an actual source
/// candidate.
Status CheckPartialEdge(const ModelDefinition& definition, EntityIndex p,
EntityIndex q, EntityIndex v,
const Ds4ThermalElementDefinition& owner) {
using namespace thermal_internal;
const Vector3 x{definition.nodes[p].coordinates};
const Vector3 y{definition.nodes[q].coordinates};
const Vector3 z{definition.nodes[v].coordinates};
double largest = 0;
for (std::size_t j = 0; j < 3; ++j) {
largest =
std::max({largest, std::abs(x[j]), std::abs(y[j]), std::abs(z[j])});
}
int exponent = 0;
std::frexp(largest, &exponent);
IntervalVector a;
IntervalVector b;
std::size_t axis = 0;
double best = 0;
for (std::size_t j = 0; j < 3; ++j) {
a[j] = Scale(Interval{y[j]}, -exponent) - Scale(Interval{x[j]}, -exponent);
b[j] = Scale(Interval{z[j]}, -exponent) - Scale(Interval{x[j]}, -exponent);
const double lower = a[j].ContainsZero() ? 0
: std::min(std::abs(a[j].lower),
std::abs(a[j].upper));
if (lower > best) {
best = lower;
axis = j;
}
}
if (best > 0) {
const Interval lambda = b[axis] / a[axis];
if (lambda.upper < 0 || lambda.lower > 1) return Status::Ok();
for (std::size_t j = 0; j < 3; ++j) {
if (!(b[j] - lambda * a[j]).ContainsZero()) return Status::Ok();
}
}
return GeometryFailure(owner.source_id, owner.location,
"uncertain-thermal-shell-connection",
"Source-sharing coarse/partial edges have unresolved "
"or nonconforming open-edge contact.");
}
} // namespace
Status ValidateThermalShellMesh(const ModelDefinition& definition) {
using namespace thermal_internal;
struct Incidence {
EntityIndex first;
EntityIndex second;
std::size_t element;
};
std::map<Edge, std::vector<Incidence>> edges;
std::map<EntityIndex, std::map<EntityIndex, std::vector<EntityIndex>>> links;
std::set<std::array<EntityIndex, 4>> patches;
std::map<std::pair<std::string, std::int64_t>, EntityIndex> source_nodes;
for (std::size_t index = 0; index < definition.thermal_elements.size();
++index) {
const auto& element = definition.thermal_elements[index];
for (const EntityIndex node : element.node_indices) {
const auto& source = definition.nodes[node].source_id;
const auto inserted = source_nodes.emplace(
std::make_pair(source.instance_name, source.source_label), node);
if (!inserted.second && inserted.first->second != node) {
return GeometryFailure(
element.source_id, element.location,
"invalid-thermal-shell-topology",
"One source node identity has multiple indices.");
}
}
auto sorted = element.node_indices;
std::sort(sorted.begin(), sorted.end());
if (std::adjacent_find(sorted.begin(), sorted.end()) != sorted.end() ||
!patches.insert(sorted).second) {
return GeometryFailure(element.source_id, element.location,
"invalid-thermal-shell-topology",
"Repeated node or duplicate element patch.");
}
std::array<Vector3, 4> coordinates{};
for (std::size_t i = 0; i < 4; ++i)
coordinates[i] =
Vector3{definition.nodes[element.node_indices[i]].coordinates};
const auto geometry =
BuildGeometry(coordinates, element.source_id, element.location);
if (!geometry.HasValue()) return geometry.GetStatus();
// Domain never publishes geometry that fails at any kernel/recovery point.
const double gauss = std::sqrt(3. / 5.);
for (double xi : {-gauss, 0., gauss}) {
for (double eta : {-gauss, 0., gauss}) {
const auto point = EvaluateSurface(
geometry.Value(), xi, eta, Interval{Down(xi), Up(xi)},
Interval{Down(eta), Up(eta)}, element.source_id, element.location);
if (!point.HasValue()) return point.GetStatus();
}
}
const double recovery = 1 / std::sqrt(3.);
for (double xi : {-recovery, recovery}) {
for (double eta : {-recovery, recovery}) {
const auto point = EvaluateSurface(
geometry.Value(), xi, eta, Interval{Down(xi), Up(xi)},
Interval{Down(eta), Up(eta)}, element.source_id, element.location);
if (!point.HasValue()) return point.GetStatus();
}
}
for (std::size_t i = 0; i < 4; ++i) {
const EntityIndex node = element.node_indices[i];
const EntityIndex next = element.node_indices[(i + 1) % 4];
const EntityIndex previous = element.node_indices[(i + 3) % 4];
edges[EdgeKey(node, next)].push_back({node, next, index});
links[node][previous].push_back(next);
links[node][next].push_back(previous);
}
}
for (const auto& item : edges) {
const auto& incidence = item.second;
if (incidence.size() > 2 ||
(incidence.size() == 2 && incidence[0].first == incidence[1].first)) {
const auto& element =
definition.thermal_elements[incidence.back().element];
return GeometryFailure(
element.source_id, element.location, "invalid-thermal-shell-topology",
"Shared edge must have at most two opposite incidences.");
}
}
for (const auto& vertex : links) {
const auto& graph = vertex.second;
std::set<EntityIndex> visited;
std::vector<EntityIndex> pending{graph.begin()->first};
std::size_t ends = 0;
bool invalid = false;
for (const auto& item : graph) {
const auto& adjacent = item.second;
if (adjacent.size() == 1) ++ends;
if (adjacent.size() > 2 ||
(adjacent.size() == 2 && adjacent[0] == adjacent[1]))
invalid = true;
}
while (!pending.empty()) {
const auto node = pending.back();
pending.pop_back();
if (!visited.insert(node).second) continue;
for (const auto next : graph.at(node)) pending.push_back(next);
}
if (invalid || visited.size() != graph.size() || (ends != 0 && ends != 2)) {
const auto& incidence =
edges.at(EdgeKey(vertex.first, graph.begin()->first))[0];
const auto& element = definition.thermal_elements[incidence.element];
return GeometryFailure(element.source_id, element.location,
"invalid-thermal-shell-topology",
"Vertex link is not one path or cycle.");
}
}
// Enumerate only incident SOURCE edges, never all mesh coordinates vs edges.
std::map<EntityIndex, std::set<EntityIndex>> neighbors;
for (const auto& item : edges) {
neighbors[item.first.first].insert(item.first.second);
neighbors[item.first.second].insert(item.first.first);
}
for (const auto& item : edges) {
for (const auto endpoint : {item.first.first, item.first.second}) {
const EntityIndex other =
endpoint == item.first.first ? item.first.second : item.first.first;
const auto& owner = definition.thermal_elements[item.second[0].element];
for (const auto partial : neighbors[endpoint]) {
if (partial == other) continue;
const auto& instance =
definition.nodes[endpoint].source_id.instance_name;
if (definition.nodes[other].source_id.instance_name != instance ||
definition.nodes[partial].source_id.instance_name != instance)
continue;
const Status status =
CheckPartialEdge(definition, endpoint, other, partial, owner);
if (!status.IsOk()) return status;
}
}
}
return Status::Ok();
}
} // namespace fesa
@@ -0,0 +1,59 @@
#ifndef FESA_ELEMENTS_THERMAL_SHELL_GEOMETRY_H_
#define FESA_ELEMENTS_THERMAL_SHELL_GEOMETRY_H_
#include <array>
#include "fesa/core/status.h"
#include "fesa/math/vector3.h"
#include "thermal_interval.h"
namespace fesa {
struct ModelDefinition;
namespace thermal_internal {
/// @brief Holds translated, power-of-two scaled tangents and their enclosures.
struct Geometry {
Vector3 b;
Vector3 c;
Vector3 d;
IntervalVector bi;
IntervalVector ci;
IntervalVector di;
int length_exponent = 0;
double scaled_jacobian_lower = 0;
};
/// @brief Holds actual-point product metric, gradients and orthonormal columns.
struct SurfacePoint {
std::array<double, 4> shape{};
std::array<Interval, 4> shape_interval{};
std::array<Vector3, 4> gradient{};
std::array<IntervalVector, 4> gradient_interval{};
std::array<Vector3, 3> axes{};
double jacobian = 0;
Interval jacobian_interval;
};
/// @brief Certifies whole-patch rank/injectivity from fixed candidate
/// directions.
Result<Geometry> BuildGeometry(const std::array<Vector3, 4>& coordinates,
const SourceEntityId& source,
const SourceLocation& location);
/// @brief Checks finite metric inverse and gradients at one used surface point.
Result<SurfacePoint> EvaluateSurface(const Geometry& geometry, double xi,
double eta, Interval xi_interval,
Interval eta_interval,
const SourceEntityId& source,
const SourceLocation& location);
/// @brief Builds a geometry failure retaining original source element identity.
Status GeometryFailure(const SourceEntityId& source,
const SourceLocation& location, const char* code,
const char* message);
} // namespace thermal_internal
/// @brief Validates DS4 whole-patch geometry and source-only manifold topology.
/// @pre Node indices and section assignments have been checked by Domain.
Status ValidateThermalShellMesh(const ModelDefinition& definition);
} // namespace fesa
#endif // FESA_ELEMENTS_THERMAL_SHELL_GEOMETRY_H_
+4
View File
@@ -8,6 +8,8 @@
#include <string>
#include <utility>
#include "../elements/thermal_shell_geometry.h"
namespace fesa {
namespace {
@@ -112,6 +114,8 @@ Status ValidateThermalDefinition(const ModelDefinition& definition) {
active_nodes[node] = true;
}
}
const Status geometry_status = ValidateThermalShellMesh(definition);
if (!geometry_status.IsOk()) return geometry_status;
std::map<EntityIndex, std::array<double, 5>> initial_values;
for (const auto& initial : definition.thermal_initial_conditions) {
if (initial.node_index >= active_nodes.size() ||