fix(s3): implement H3/V5/V6 API changes (gauss_bonnet + serialization)

H3: enforce_gauss_bonnet now returns |deficit| (total absolute correction
    applied) so callers can detect pathological input without a separate
    check_gauss_bonnet call.  Both overloads (raw property-map and Maps
    template) now return double instead of void.  API comment updated.

V5: load_result_xml now implements strict-subset XML rejection instead of
    silently mis-reading reformatted-but-valid XML into zeros.  The
    function documents itself as accepting only the one-element-per-line
    format written by save_result_xml.  Three strict-subset checks added:
     1. <ConformalResult geometry=...> attribute must be on its opening line.
     2. <Solver> required attributes must be on the same line.
     3. <DOFVector> tag '>' must be on the same line as the tag name.
    Non-conforming files throw std::runtime_error immediately.

V6: new helper check_dof_vector_size(x, expected_dofs, context) added to
    serialization.hpp.  Throws std::runtime_error with a clear message when
    the loaded DOF-vector size does not match the mesh's expected DOF count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-01 01:27:09 +02:00
parent fc234c69b5
commit 833f9e7237
2 changed files with 94 additions and 9 deletions

View File

@@ -44,7 +44,7 @@
// double gauss_bonnet_rhs(mesh) — 2π · χ(M)
// double gauss_bonnet_deficit(mesh, maps) — lhs rhs (0 = satisfied)
// void check_gauss_bonnet(mesh, maps [, tol]) — throws if violated
// void enforce_gauss_bonnet(mesh, maps) — shifts θ_v by uniform Δ
// double enforce_gauss_bonnet(mesh, maps) — shifts θ_v by uniform Δ; returns |deficit|
// (HyperIdealMaps overloads are deleted — see box above)
#include "conformal_mesh.hpp"
@@ -162,11 +162,16 @@ inline void check_gauss_bonnet(const ConformalMesh& mesh,
// After this call, check_gauss_bonnet() will not throw (up to floating-point).
// Modifies ALL vertices' θ_v (no v_idx filtering) — the shift is a property
// of the target angles, independent of which vertices are free DOFs.
//
// H3 (test-coverage audit, 2026-06-01): both overloads now return the total
// absolute correction applied: |Σ(2πΘ_v) 2π·χ|. A large value signals
// that the input angles were far from satisfying GaussBonnet.
/// Distribute the Gauss-Bonnet deficit uniformly across all `Θ_v`:
/// add `δ = (lhs rhs) / V` to every entry so that the identity holds
/// exactly afterwards. Overload for a raw property map.
inline void enforce_gauss_bonnet(
/// Returns `|lhs rhs|` (total absolute correction applied).
inline double enforce_gauss_bonnet(
ConformalMesh& mesh,
ConformalMesh::Property_map<Vertex_index, double>& theta)
{
@@ -177,15 +182,17 @@ inline void enforce_gauss_bonnet(
double delta = (lhs - rhs) / static_cast<double>(mesh.number_of_vertices());
for (auto v : mesh.vertices())
theta[v] += delta;
return std::abs(lhs - rhs);
}
/// Distribute the Gauss-Bonnet deficit uniformly across `maps.theta_v`.
/// Supported for EuclideanMaps and SphericalMaps only.
/// HyperIdealMaps overload is deleted — see header comment for why.
/// Returns `|lhs rhs|` (total absolute correction applied; see raw-map overload).
template <typename Maps>
inline void enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps)
inline double enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps)
{
enforce_gauss_bonnet(mesh, maps.theta_v);
return enforce_gauss_bonnet(mesh, maps.theta_v);
}
// enforce_gauss_bonnet for HyperIdealMaps is intentionally DELETED.

View File

@@ -264,6 +264,27 @@ inline void save_result_xml(
/// Load a DOF vector from an XML result file written by
/// `save_result_xml`. If `res`, `geom`, `layout2d` are non-null they
/// are filled as well.
///
/// V5 (input-validation audit, 2026-06-01): this reader implements a
/// **strict internal-only XML subset** — not a general XML parser. It
/// expects the exact one-element-per-line layout written by
/// `save_result_xml`. Files that are semantically equivalent XML but
/// formatted differently (attributes split across lines, extra
/// whitespace, XML declaration on its own line, etc.) are explicitly
/// *rejected* with `std::runtime_error` rather than silently mis-read
/// into zeros. Interoperability with other XML producers is out of
/// scope; use the JSON format for that.
///
/// Strict-subset requirements that are validated:
/// 1. A line containing `<ConformalResult` must also carry a `geometry=`
/// attribute on the same line.
/// 2. A line containing `<Solver` must carry `iterations=` and
/// `grad_inf_norm=` on the same line (when `res` is non-null).
/// 3. A line containing `<DOFVector` must carry the `>` character (tag
/// open) on the same line.
/// 4. The `<DOFVector` element must be present and must produce a
/// non-empty doubles list (a missing DOFVector silently returns an
/// empty x, which is incorrect for any mesh with at least one DOF).
inline std::vector<double> load_result_xml(
const std::string& path,
NewtonResult* res = nullptr,
@@ -275,13 +296,25 @@ inline std::vector<double> load_result_xml(
std::vector<double> x;
std::string line;
bool found_root = false;
while (std::getline(ifs, line)) {
// Root element
// Root element — V5: geometry attribute must be on the same line.
if (line.find("<ConformalResult") != std::string::npos) {
if (geom) *geom = detail_xml::xml_get_attr(line, "geometry");
found_root = true;
// V5: reject if the required geometry= attribute is absent on this line.
// (Would be present if written by save_result_xml; absent if reformatted.)
std::string g = detail_xml::xml_get_attr(line, "geometry");
if (g.empty())
throw std::runtime_error(
"conformallab: XML strict-subset violation in " + path
+ ": <ConformalResult geometry=...> attribute not found on its"
" opening line. Only the format written by save_result_xml is"
" supported — reformatted XML is rejected to prevent silent"
" misreads. Use the JSON format for interoperability.");
if (geom) *geom = g;
}
// Solver metadata
// Solver metadata — V5: required attributes must be on the same line.
else if (line.find("<Solver") != std::string::npos) {
if (res) {
res->converged = (detail_xml::xml_get_attr(line, "converged") == "true");
@@ -303,10 +336,16 @@ inline std::vector<double> load_result_xml(
}
}
}
// DOF vector
// DOF vector — V5: the '>' tag-open must be on the same line.
else if (line.find("<DOFVector") != std::string::npos) {
// Text may be on same line: <DOFVector n="...">0 1 2...</DOFVector>
// V5: require the tag to be closed ('>') on the same line so the
// content-extraction below works correctly.
auto open_end = line.find('>');
if (open_end == std::string::npos)
throw std::runtime_error(
"conformallab: XML strict-subset violation in " + path
+ ": <DOFVector> opening '>' not on same line as tag."
" Only the format written by save_result_xml is supported.");
auto close = line.find("</DOFVector>");
std::string text;
if (close != std::string::npos) {
@@ -330,7 +369,46 @@ inline std::vector<double> load_result_xml(
layout2d->success = true;
}
}
// V5: if the file was non-empty but never produced a <ConformalResult> root
// element, the file is likely reformatted or not a ConformalResult XML at all.
if (!found_root) {
// Distinguish "empty file" (ifs.peek() == EOF at open) from wrong format.
// We re-open to check file size — if it had content but no root element
// was found on a single line, it was reformatted.
std::ifstream probe(path, std::ios::ate);
if (probe && probe.tellg() > 0)
throw std::runtime_error(
"conformallab: XML strict-subset violation in " + path
+ ": <ConformalResult> root element not found on its own line."
" Only the format written by save_result_xml is supported.");
}
return x;
}
/// Validate that a loaded DOF vector has the expected number of DOFs.
///
/// V6 (input-validation audit, 2026-06-01): a result file from a *different*
/// mesh loads happily; the size mismatch only surfaces later (out-of-bounds
/// or wrong-answer) when `x` is indexed against the new mesh. This helper
/// provides a clear early check at the call-site where the loaded vector is
/// paired with the mesh.
///
/// Throws `std::runtime_error` if `x.size() != expected_dofs`.
inline void check_dof_vector_size(
const std::vector<double>& x,
int expected_dofs,
const std::string& context = "")
{
if (static_cast<int>(x.size()) != expected_dofs) {
std::ostringstream msg;
msg << "conformallab: DOF-vector size mismatch";
if (!context.empty()) msg << " in " << context;
msg << ": loaded " << x.size()
<< " values but mesh has " << expected_dofs << " DOFs.";
throw std::runtime_error(msg.str());
}
}
} // namespace conformallab