Implement private HHT field convergence policy and oracle tests
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
#include "analysis/hht_field_convergence.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace fesa::hht_field_convergence {
|
||||
namespace {
|
||||
constexpr double kRn = .005, kCn = .01, kRp = .02, kEpsilon = 1e-5;
|
||||
constexpr double kZeroCorrection = .001, kLinear = 1e-8;
|
||||
constexpr double kInactive = 1e-5, kSmallTranslation = 1e-8;
|
||||
bool Nonnegative(double x) { return std::isfinite(x) && x >= 0; }
|
||||
bool ValidOptional(const std::optional<double>& x) {
|
||||
return !x || Nonnegative(*x);
|
||||
}
|
||||
bool ValidHistory(const History& h) {
|
||||
return Nonnegative(h.nonzero_sum) && Nonnegative(h.maximum_sum) &&
|
||||
h.nonzero_count <= h.maximum_count &&
|
||||
(h.nonzero_count != 0 || h.nonzero_sum == 0) &&
|
||||
(h.maximum_count != 0 || h.maximum_sum == 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Evaluation Evaluate(const std::array<FieldInput, 2>& fields,
|
||||
const std::array<History, 2>& committed,
|
||||
std::size_t iteration, double reference_length) {
|
||||
Evaluation result;
|
||||
result.status = Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError, "invalid-state", {}, {}, {}, "Invalid field convergence arithmetic or input."}});
|
||||
result.pending = committed;
|
||||
if (!std::isfinite(reference_length) || reference_length <= 0) return result;
|
||||
for (std::size_t a = 0; a < 2; ++a) {
|
||||
const auto& f = fields[a];
|
||||
const auto& h = committed[a];
|
||||
auto& out = result.fields[a];
|
||||
if (!ValidHistory(h) || !Nonnegative(f.residual) || !Nonnegative(f.increment) ||
|
||||
!ValidOptional(f.correction) || !ValidOptional(f.previous_residual) ||
|
||||
!ValidOptional(f.older_residual)) return result;
|
||||
if (f.flux.empty()) {
|
||||
if (f.active) { result.status = Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError, "invalid-flux-population", {}, {}, {}, "Empty active flux population."}}); return result; }
|
||||
continue;
|
||||
}
|
||||
const double historic_maximum = h.maximum_count == 0 ? 0 :
|
||||
h.maximum_sum / static_cast<double>(h.maximum_count);
|
||||
const double cutoff = kInactive * historic_maximum;
|
||||
double maximum = 0;
|
||||
for (const auto& x : f.flux) {
|
||||
if (!std::isfinite(x.internal) || !std::isfinite(x.inertia)) return result;
|
||||
const double total = x.internal + x.inertia;
|
||||
if (!std::isfinite(total)) return result;
|
||||
maximum = std::max(maximum, std::abs(total));
|
||||
}
|
||||
out.maximum_flux = maximum;
|
||||
const bool filter = maximum >= .1 * historic_maximum;
|
||||
double sum = 0;
|
||||
std::size_t count = 0;
|
||||
for (const auto& x : f.flux) {
|
||||
const double b = std::abs(x.internal + x.inertia);
|
||||
if (!filter || b >= cutoff) {
|
||||
sum += b;
|
||||
if (!std::isfinite(sum)) return result;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
if (count == 0) { result.status = Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError, "invalid-flux-population", {}, {}, {}, "Empty active flux population."}}); return result; }
|
||||
const double spatial = sum / static_cast<double>(count);
|
||||
out.spatial_flux = spatial;
|
||||
if (h.nonzero_count == std::numeric_limits<std::uint64_t>::max() ||
|
||||
h.maximum_count == std::numeric_limits<std::uint64_t>::max()) return result;
|
||||
const double base = h.nonzero_count == 0 ? .01 :
|
||||
h.nonzero_sum / static_cast<double>(h.nonzero_count);
|
||||
const double next_sum = h.nonzero_sum + spatial;
|
||||
const double next_maximum = h.maximum_sum + maximum;
|
||||
if (!Nonnegative(next_sum) || !Nonnegative(next_maximum)) return result;
|
||||
const double included = next_sum / static_cast<double>(h.nonzero_count + 1);
|
||||
const bool eligible = spatial > kEpsilon * (h.nonzero_count == 0 ? base : included);
|
||||
const double raw = eligible ? included : base;
|
||||
if (!std::isfinite(raw) || raw <= 0) return result;
|
||||
out.time_average_flux = raw;
|
||||
out.residual_scale = raw;
|
||||
auto& pending = result.pending[a];
|
||||
if (eligible) { pending.nonzero_sum = next_sum; ++pending.nonzero_count; }
|
||||
pending.maximum_sum = next_maximum;
|
||||
++pending.maximum_count;
|
||||
}
|
||||
// Both conversions read raw means, never a converted peer scale.
|
||||
if (fields[0].active && fields[1].active) {
|
||||
const double force = *result.fields[0].time_average_flux;
|
||||
const double moment = *result.fields[1].time_average_flux;
|
||||
const std::array<double, 2> converted{moment / reference_length, reference_length * force};
|
||||
for (std::size_t a = 0; a < 2; ++a) {
|
||||
if (!Nonnegative(converted[a])) return result;
|
||||
auto& out = result.fields[a];
|
||||
if (*out.spatial_flux <= *out.time_average_flux &&
|
||||
*out.time_average_flux < converted[a]) out.residual_scale = converted[a];
|
||||
}
|
||||
}
|
||||
bool all_linear = iteration >= 1;
|
||||
for (std::size_t a = 0; a < 2; ++a) {
|
||||
if (!fields[a].active) continue;
|
||||
const double scale = *result.fields[a].residual_scale;
|
||||
if (!std::isfinite(scale) || scale <= 0) return result;
|
||||
const double ratio = fields[a].residual / scale;
|
||||
if (!Nonnegative(ratio)) return result;
|
||||
all_linear = all_linear && ratio <= kLinear;
|
||||
}
|
||||
bool all_pass = true;
|
||||
for (std::size_t a = 0; a < 2; ++a) {
|
||||
const auto& f = fields[a];
|
||||
if (!f.active) continue;
|
||||
auto& out = result.fields[a];
|
||||
const double scale = *out.residual_scale;
|
||||
const double ratio = f.residual / scale;
|
||||
if (all_linear) {
|
||||
out.branch = Branch::kLinear;
|
||||
out.residual_threshold = kLinear;
|
||||
out.residual_pass = true; out.field_pass = true;
|
||||
continue;
|
||||
}
|
||||
const bool zero = *out.spatial_flux <= kEpsilon * scale;
|
||||
bool nonquadratic = false;
|
||||
if (!zero && iteration > 9 && f.previous_residual && f.older_residual &&
|
||||
*f.older_residual > *f.previous_residual && *f.previous_residual > f.residual && f.residual > 0) {
|
||||
const double recent = f.residual / *f.previous_residual;
|
||||
const double older = *f.previous_residual / *f.older_residual;
|
||||
if (!Nonnegative(recent) || !Nonnegative(older)) return result;
|
||||
nonquadratic = recent >= older;
|
||||
}
|
||||
out.branch = zero ? Branch::kZeroFlux :
|
||||
(nonquadratic ? Branch::kNonquadratic : Branch::kNormal);
|
||||
const double residual_threshold = zero ? kEpsilon : (nonquadratic ? kRp : kRn);
|
||||
out.residual_threshold = residual_threshold;
|
||||
out.residual_pass = ratio <= residual_threshold;
|
||||
if (zero && *out.residual_pass) { out.field_pass = true; continue; }
|
||||
if (!zero && iteration >= 1 && a == 0 && f.increment < kSmallTranslation * reference_length) {
|
||||
out.field_pass = out.residual_pass;
|
||||
} else {
|
||||
const double correction_threshold = zero ? kZeroCorrection : kCn;
|
||||
out.correction_threshold = correction_threshold;
|
||||
const double bound = correction_threshold * f.increment;
|
||||
if (!Nonnegative(bound)) return result;
|
||||
if (iteration >= 1 && f.correction) out.correction_pass = *f.correction <= bound;
|
||||
if (!zero && !nonquadratic && iteration >= 2 && f.correction &&
|
||||
f.previous_residual && f.older_residual) {
|
||||
const double denominator = std::min(*f.previous_residual, *f.older_residual);
|
||||
if (denominator > 0) {
|
||||
const double quotient = f.residual / denominator;
|
||||
if (!Nonnegative(quotient)) return result;
|
||||
const double estimate = quotient * *f.correction;
|
||||
if (!Nonnegative(estimate)) return result;
|
||||
out.estimated_correction = estimate;
|
||||
out.correction_pass = out.correction_pass.value_or(false) || estimate <= bound;
|
||||
}
|
||||
}
|
||||
out.field_pass = zero ? out.correction_pass.value_or(false) :
|
||||
(*out.residual_pass && out.correction_pass.value_or(false));
|
||||
}
|
||||
all_pass = all_pass && *out.field_pass;
|
||||
}
|
||||
result.field_pass = all_pass;
|
||||
result.status = Status::Ok();
|
||||
return result;
|
||||
}
|
||||
} // namespace fesa::hht_field_convergence
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
|
||||
#define FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
|
||||
namespace fesa::hht_field_convergence {
|
||||
inline constexpr const char* kPolicyId = "abaqus2025-field-on-fesa-v1";
|
||||
// Ordered element rows first, then external source entries (including explicit zero).
|
||||
// External entries use internal=value, inertia=0. No assembled cancellation here.
|
||||
struct FluxOccurrence { double internal = 0.0; double inertia = 0.0; };
|
||||
struct History {
|
||||
double nonzero_sum = 0.0;
|
||||
std::uint64_t nonzero_count = 0;
|
||||
double maximum_sum = 0.0;
|
||||
std::uint64_t maximum_count = 0;
|
||||
};
|
||||
struct FieldInput {
|
||||
bool active = false; // At least one free full DOF of this field.
|
||||
std::vector<FluxOccurrence> flux;
|
||||
double residual = 0.0; // max |I+fint-F| on full free endpoint DOFs, fi=1.
|
||||
double increment = 0.0; // max |q_trial-q_last_accepted| in fixed full u/phi chart.
|
||||
std::optional<double> correction; // max |q_trial-q_previous_selected|, i>=1.
|
||||
std::optional<double> previous_residual;
|
||||
std::optional<double> older_residual;
|
||||
};
|
||||
enum class Branch { kNotEvaluated, kLinear, kZeroFlux, kNonquadratic, kNormal };
|
||||
struct FieldResult {
|
||||
std::optional<double> spatial_flux, maximum_flux, time_average_flux, residual_scale;
|
||||
std::optional<double> estimated_correction, residual_threshold, correction_threshold;
|
||||
std::optional<bool> residual_pass, correction_pass, field_pass;
|
||||
Branch branch = Branch::kNotEvaluated;
|
||||
};
|
||||
struct Evaluation {
|
||||
Status status = Status::Failure(FailureCategory::kSolver, {});
|
||||
bool field_pass = false;
|
||||
std::array<FieldResult, 2> fields;
|
||||
// Pure pending snapshot: procedure owns exactly-once commit after every gate/output.
|
||||
std::array<History, 2> pending;
|
||||
};
|
||||
// Index 0: force/translation; index 1: generalized moment/total rotation phi.
|
||||
Evaluation Evaluate(const std::array<FieldInput, 2>& fields,
|
||||
const std::array<History, 2>& committed,
|
||||
std::size_t iteration, double reference_length);
|
||||
} // namespace fesa::hht_field_convergence
|
||||
|
||||
#endif // FESA_ANALYSIS_HHT_FIELD_CONVERGENCE_H_
|
||||
Reference in New Issue
Block a user