feat(p1): CLI extensions + quality measures + stereographic layout
Implement Phase-Session P1 quick wins (4 independent additions):
9h.1: Add --tol and --max-iter CLI options to conformallab_core
- Newton solver tolerance [default 1e-8]
- Newton iteration limit [default 200]
- Thread both through run_euclidean / run_spherical / run_hyper_ideal
- Update CLI parameter table in documentation
9h.2: Add -g cp_euclidean and -g inversive_distance geometry routes
- run_cp_euclidean() & run_inversive_distance() pipelines (~60 lines each)
- Face-based DOF assignment for CP-Euclidean
- Vertex-based DOF assignment for Inversive-Distance
- Both integrated into CLI geometry validator (IsMember)
9g.1: Create conformal_quality.hpp with validation measures
- IsothermicityMeasure: metric anisotropy (conformality deviation)
- DiscreteConformalEquivalenceMeasure: length-cross-ratio residuals
- FlippedTriangles: detects inverted/degenerate triangles
- LengthCrossRatio: discrete conformal invariant computation
- ConvergenceUtility: aggregated convergence statistics (max/mean/sum)
- Ported from Java: plugin/visualizer + convergence utilities
- Includes sanity tests validating finite outputs on valid layouts
9d.3: Create stereographic_layout.hpp for S² → ℂ projection
- Stereographic projection from north pole: S² → ℂ ∪ {∞}
- Inverse projection: ℂ → S² for round-trip validation
- Möbius centring: centres the 2-D point cloud at origin
- stereographic_layout(Layout3D) -> Layout2D conversion
- Round-trip tests: south pole, equator, random sphere points
- Tests: projection/inverse consistency, north pole handling
Test results: 336/336 CGAL tests pass (272 pre-existing + 64 new from all phases)
- conformal_quality.cpp: 13 new tests (measures, isothermic, dce, convergence)
- stereographic_layout.cpp: 10 new tests (projection, inverse, round-trip, layout)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,26 @@ inline std::vector<double> load_result_xml(
|
||||
|
||||
std::vector<double> x;
|
||||
std::string line;
|
||||
bool found_root = false;
|
||||
bool found_dofvector = 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 +337,17 @@ 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>
|
||||
found_dofvector = true;
|
||||
// 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 +371,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
|
||||
|
||||
Reference in New Issue
Block a user