233 lines
8.9 KiB
C++
233 lines
8.9 KiB
C++
#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
|