All notable changes to TensorCAD are documented here. The format follows Keep a Changelog and the project uses semantic versioning.
tensorcad.nurbs: piecewise (break-aware) degree elevation (NURBSCurve.split_at_discontinuities,NURBSCurve.elevate_degree_piecewise,docs/nurbs_piecewise_degree_elevation.md,examples/nurbs_piecewise_elevation/, 17 tests). Degree elevation already refused a curve with an interior knot of multiplicitydegree + 1-- a break, not aC0join -- and told the caller to split the curve first, but nothing in the package could split it: a composite profile (polyline plus arc, a duplicated seam, two concatenated patches) could not reach the elevated representation at all.split_at_discontinuitiesreturns the maximalC0-or-better pieces as a pure slice of the control polygon (nothing inserted, nothing re-solved, so the split is bit-exact and keeps every interior knot and rational weight; a smooth curve returns itself), andelevate_degree_piecewise(times)composes it with the exact homogeneous elevation. Evidence on the exact rational quarter circle plus straight segment: radius2 m + 8.9e-16 m, total lengthpi + 2 sqrt 2to8.9e-16 m, point deviation<= 6.3e-16 m, the 45 deg kink and the two jumps of a three-piece fixture preserved, control-point growth minimal.tensorcad.girder: hull-girder elastic and plastic section properties (tensorcad/girder.py,docs/girder_section.md,examples/girder_section/,tools/girder_section_doc.py, 25 tests). Geometry could produce a hull and the hydrostatics could say what it displaces, but nothing could say what the section carries: there was no second moment, no section modulus and no plastic capacity anywhere in the package, which is the whole input of a scantling check.girder_sectiontakes one closed polygon in the(y, z)section plane (or a half-breadth table viamirror_half_section) and returns area, centroid, the two centroidal second moments, the product of inertia with its principal axes, the four elastic section moduli (deck/keel/port/starboard, each with its own extreme-fibre distance), the plastic neutral axis and plastic modulus for both axes, andfirst_moment(z)-- the static moment that is the numerator oftau = V Q / I t.closed_cell_torsion_constant(Bredt) andopen_section_torsion_constantcover the other degree of freedom. Area and second moments are the exact shoelace/Euler sums, so there is no discretisation error: measured against closed forms, a rectangle is exact to0.0relative, a double-walled box to 1.2e-16, and an I-beam polygon matches a sum of rectangles with Steiner terms to 1.4e-16. The plastic neutral axis is the only searched quantity; for three trapezoids it lands on the closed-form equal-area level to 1.4e-15 relative and the plastic modulus matches a Simpson quadrature of the width law to 5.7e-15. Two traps are documented: an annulus entered as a loop with a zero-width bridge must not be clipped-and-reconstructed (the reconstruction paired the void's chain with itself and dropped it, returning 21.33 instead of 20 for the box's plastic modulus -- the boundary-integral form is blind to holes and fixed it), and the moment potential-min(z, z_p)^2/2is non-zero above the cut, so the above-level half of a split edge must still be integrated. On the parametric hull's nine stations the section area agrees withbonjean.section_propertiesto 4.8e-16 relative, the symmetric loft leaves a product of inertia of 4.1e-12 m^4, deck and keel moduli differ (0.1432 vs 0.0940 m^3 at midship) and the plastic-to-elastic ratio is 1.98 against a rectangle's 1.5.NURBSSurface.elevate_degree: tensor-product degree elevation (tensorcad/nurbs.py,docs/nurbs_surface_degree_elevation.md,examples/nurbs_surface_degree_elevation/,tools/nurbs_surface_elevation_figure.py, 17 tests). The curve operation landed without its surface half, so a degree-(2,1)rational cylinder could not be expressed in the degree-(3,3)space of a bicubic neighbour even though both describe the same patch. Each fixed-vcolumn is now elevated inuand each fixed-urow inv, through the same homogeneous collocation core thatNURBSCurve.elevate_degreewas refactored onto. Measured: point deviation 3.56e-15 m worst case, cylinder radius error 1.33e-15 m, area errors 4.66e-15 against2 pi R Hand 1.33e-15 against4 pi R^2, the two directions commuting to 2.22e-16 m, a non-rational bicubic patch staying non-rational, and growth exactlyn + times * spans. Documented trap: elevating the 3-D net without the weights loses 0.3425 m of radius (25.0 % ofR) and 14.1 % of the lateral area; a second one found here is that the solve returns uniform weights as1 +/- 4e-16, which silently flippedNURBSSurface.rationalon, so the homogeneous net is rescaled to the exact constant. Trimming and stitching remain out of scope.NURBSCurve.elevate_degree: standard degree elevation (tensorcad/nurbs.py,docs/nurbs_degree_elevation.md,examples/nurbs_degree_elevation/,tests/test_nurbs_degree_elevation.py). A curve of degreepcan now be represented as degreep + timeswith the same parametrisation and the same continuity: every knot multiplicity grows bytimes, the new control points are the unique coefficients in that space (Greville collocation, homogeneous for rational curves). Measured on the exact rational circleR = 2 m, three 120-degree arcs: degree 2 -> 5 / 7 -> 16 control points, max point deviation 1.11e-15 m, radius deviation 1.55e-15 m, arc-length relative error 2.83e-16 against2 pi R; a cubic C2 join stays C2 (measured derivative jump 0.0) with the minimal 7-point quartic, where a naive per-span Bezier route would return 9 points at C0. This is the curve-level half of the patch-degree alignment that multi-patch assembly needs; surface degree elevation, trimming and stitching remain out of scope.tensorcad.resistance: the 1966/1978 ITTC method (tensorcad/resistance.py,docs/resistance.md,examples/resistance/, 60 tests). Geometry alone could not say what a hull costs to tow; this closes the gap the verification audit listed as medium priority ("no resistance modules -- ITTC-57 + form factor first"). Three flat-plate lines (ittc57,schoenherrsolved by bisection,prandtl-schlichting), the Prohaska form-factor fit with residuals and standard errors, the resistance components (R_F,(1+k)R_F,R_R,R_T,P_E) for one body at one speed, and model-to-ship Froude scaling with the ITTC-78 two-dimensional splitCT_s = (1+k) CF_s + CR + CA. Residual resistance and correlation allowance are caller inputs with no invented defaults. Measured: ITTC-57 within 2.24 % of Schoenherr and 2.55 % of Prandtl-Schlichting forRe >= 1e7(the divergence below1e6-- 16.1 % at1e5-- is asserted, not hidden), mesh wetted area within 0.0016 % of the analytic flank integral at 161x43, Prohaska recovering an injected1 + k = 1.15to round-off, and a 10 m -> 100 m Wigley scaling where the ship is 615.5x the model, not the naivelambda^3 = 1000x. Documented trap: a closed hull's deck patch is 30.9 % of its shell area, so shell area used as wetted surface inflates friction by 44.8 %.
- The pinned public-surface contract test had been red at HEAD for ten names
(
tests/test_public_surface.py):OffsetTable,apply_transformand the eight parametric-hull/cross-section exports were intensorcad.__all__but missing fromEXPECTED_API. The list now matches the package, so the contract test can catch the next drift instead of failing for a pre-existing one. - A waterline plane that contains mesh edges is traced once, not twice
(
tensorcad/stability.py::_plane_segments;docs/freeboard_seam.md). The seam wheregz.with_freeboard's vertical band meets the hull is a ring of edges in the cutting plane, met by both of that edge's faces; every segment was emitted twice, the doubled contour was read as a hole nested in itself, and the Wigley hull reported a waterplane area of-13.33125 m^2(exactly-2 A) instead of6.665625 m^2. initial_stabilitymeasures the immersed body, not the whole closed mesh (same file). A mesh carrying freeboard used to hand back its total volume as displacement (Wigley 81x21:4.108733 m^3for a2.775608 m^3displacement,KB+28 %,GM+28 %) with no warning; the banded branch now usesrighting_arm.heeled_displacementat zero heel. Hulls capped at their waterline keep the original route bit for bit.
- Wigley GZ verification closed (
tests/test_wigley_gz_verification.py:: TestFreeboardSeamWaterplane,examples/freeboard_seam/):GMand the GZ curve are now measured on the same freeboarded mesh --GZ/sin(phi)at 2 deg matchesGMto 0.02 %, andGMconverges to the analyticKB + I_T/nabla - KGat 0.114 % / 0.047 % / 0.012 % on 41x13 / 81x21 / 161x41.
nurbs.sphere_surface+ committed anchor for a closed patch with singular pole rows (docs/nurbs_pole_surface.md,examples/nurbs_sphere/run_example.py,artifacts/nurbs_sphere/,tests/test_nurbs_pole_surface.py). The surface service had anchors for an open tube and for a cylinder capped by hand-built fans, but none for a patch that pinches by itself -- the case every hull form creates at a stem, a transom corner or a bulb tip. One rational biquadratic patch (7 x 5net, degree(2, 2)) is the exact sphere:max | |S| - R | = 5.33e-15 matR = 12 m, parametric area1809.5573684677186 m^2vs4 pi R^2 = 1809.5573684677208 m^2(-1.22e-15), and the tessellation alone is a watertight, outward, single-component shell with volume converging asO(h^2)to4/3 pi R^3(-7.94e-04at192 x 64). 27 tests.
MeshBuilder.add_quadreported two triangles for a quad it collapsed to one. A pole quad has two coincident corners, so only(a, c, d)survives; returning2shifted every laterfaces_per_quadindex by one and madetessellation_reportcompare the wrong facet and finally raiseIndexError: tuple index out of range(383 indices for 336 faces on the48 x 16sphere). It now returns the number of triangles actually appended. The cylinder anchor could not catch this -- it has no degenerate quad.- A singular (pinched) parameter boundary is now welded on its control point
(
NURBSSurface.singular_boundary_point). The per-sample evaluation of a pole ring agrees only to 1 ulp ((a*b)/brounds twice), so exact-position welding left a ragged ring: the48 x 16sphere was not watertight (12 inward faces, spurious 90.4° normal) while24 x 8happened to weld. Keying the row is a fact about the control net, not a tolerance, so the module's no-tolerance policy is intact. tests/test_box_gz_area.pyno longer fails on a clean checkout (docs/analysis/2026-11-08-evidence-artifact-restoration.md). The node's evidence was written toartifacts/m4873/, which the integration gate filters, somaincarried the code, the page and the tests but not the artifact two of those tests open (FileNotFoundError, known and recorded indocs/m5283_geometry_audit.mdas pre-existing debt). The evidence now lives beside the page it certifies,docs/box_gz_areas/(the convention already used bydocs/bonjean/,docs/fairness/), andtools/verify_box_gz_areas.py, the test and the figure link follow. Regenerated frommain's code, not copied: the JSON and the PNG come out byte-identical to the ones produced when the node was authored (sha256of the json9fa5acf0...ffa4e, of the png351a91ff...2038c; 1415 leaf keys, 1141 numeric fields, 0 differing), and the harness' own rewrite of the page's tables differs only in the figure link -- so the committed numbers reproduce from current code. No physics changed. 293 tests over the GZ/statutory evidence chain pass (the two previously red ones included).
tensorcad.damage: deterministic damage stability by lost buoyancy (docs/damage_stability.md). A box-shaped compartment open to the sea is flooded and the ship's new equilibrium -- sinkage, trim and heel -- is solved as three balances on one waterline plane:V_ext - mu V_c = Delta/rho, and the centroid of that net volume at the ship's(x_G, y_G). Heel is an output, so an asymmetric compartment finds its own list.GM = KB + I_T/(Delta/rho) - KGuses the net waterplane (external minusmutimes the flooded part), which is the lost-buoyancy method's answer to the free-surface question -- no separate correction, which would double count. Damped Newton with a backtracking line search; section cuts cached once per station; compartment ends are panel edges and each panel is three-point Gauss-Legendre, so the jump inA(x)at a compartment end is never sampled across. Measured against the closed forms a wall-sided barge has: midship draftL T/(L - l)andmu = 0.6draftL T/(L - mu l)to 1e-12 relative, the aft-compartment and port-half-compartment two-equation solutions to round-off, mirror-symmetric heel,I_Tof the damaged waterplane exact, and a curved hull's intact solve againsttensorcad.hydrostatics(draft 0.5 %, KB/LCB 1 %,I_Twithin 5 %).tests/test_damage.py(24 cases),examples/damage/run_example.py(17 claims, two figures,docs/damage/results.json). Not implemented, and said so on the page: probabilistic subdivision (SOLAS part B-1), progressive flooding, cross-flooding, intermediate stages, and damaged-shipGZ.
tensorcad.geomquality: CAD geometry-quality pipeline from file to gated hydrostatics (docs/geomquality-pipeline.md). The chain is exchange -> topology -> declared geometry -> continuity -> hydrostatics -> grid convergence -> machine gate, withCRITERIAandenforce()so the verdict is computed and never written by hand. Exchange reads three routes into one B-rep: native.brep.json, STEP as a topological model (sharededge_curveidentity is the shared edge), and IGES entity 128, which carries no topology and no orientation at all -- so the reader sews boundaries geometrically and then propagates a consistent orientation over the face graph (orient_shell) before the audit can say anything. The topology audit works on the edge-face adjacency graph: free edges, non-manifold edges, unsewn seams measured in metres and as a fraction ofL, collapsed boundaries recorded as degenerate and exempt, inside-out faces caught by comparing each face's declared sense with the winding of its own loop. Continuity classifies every seam G0/G1/G2 from the normal angle and the relative curvature jump across the seam, with limits calibrated on 60 % of a labelled seam set and scored on the held-out 40 % (F1 = 1.000, separation 0.43 vs 8e-5); designed creases are reported asKNUCKLE. Hydrostatics integrate each face with Gauss-Legendre per knot span at an order that follows the patch degree -- a degree-24 vertical span under-integrates by 0.5 % at the default order, which is measured, not assumed. Two things worth knowing: refining parameter space is not refining physical space on a graded patch (equal-arc sampling recovers the second-order rate: observed order 2.13 instead of 1.55), and a 14-case defect library with repair hints runs through the same detectors (14/14 detected, zero false alarms on the clean hull). Measured: Wigley III displacement 2.2e-13, worst hydrostatic quantity 1.1e-4 across three hulls, geometry deviation 1.8e-6 L, 8.5 s for the slowest hull against a 10 s budget.
tensorcad.geometry_audit: hull geometry quality audit pipeline (docs/m5283_geometry_audit.md). A machine gate between an exchanged hull model and the numbers computed from it, in the order a shipyard checks a lines plan: topology (free edges, seam gaps sized as the shortest distance between pieces, non-manifold edges, duplicate facets, winding conflicts, shell count, signed volume), fairness (discrete Gaussian curvature from the angle deficit over Meyer mixed areas, then the relative jump|dK|/Kacross every non-crease edge against a 50 % criterion, with designed creases excluded by dihedral angle rather than smoothed away), the declaredL/B/T/Cbgate, and a two-route hydrostatic check (Simpson on a station x waterline offsets table and the divergence theorem on the triangles, gated at 1 %). Six injectors ship with it because a detector that has never seen a defect has not been tested: seam offset, hole, normal flip, duplicate shell, waterline-vanishingCboffset, Gaussian bump. Measured on Wigley III and Series 60 Cb=0.70 at 161 x 41: 24/24 seam gaps found with the measured gap equal to the injected one, 9/9 bumps above the 50 % jump marked with no false positive on the clean hull, table versus mesh within 0.55 %, a +5 %Cboffset refused and named, and both standard hulls inside 2 % of their closed forms (artifacts/m5283_geometry_audit/gates_verified.json, 33 pytest cases).tensorcad.exchange: geometry exchange seam for the audit (docs/m5283_geometry_audit.md). An AP203 faceted STEP writer and reader (FACETED_BREPoverPOLY_LOOP, millimetres with the scale recorded, 17 significant digits so the round trip is exact) and a neutral patch-table JSON, so a seam gap survives export and re-import (measured: a 1 % L injected seam is found again at 1.000 % L after the round trip). Analytic B-rep STEP and IGES are refused with the blocking entity names instead of being tessellated by guesswork -- auditing a tessellator's artefacts is worse than declining -- and the module states that the written STEP file has only been validated by an internal round trip, no third-party CAD kernel being available here.
-
tensorcad.series60: the vertical taper was inverted (zeta**minstead ofzeta**(1/m)), and every Series 60 hull integrated ~21 times less volume than its own closed form (-95.3 % at 41 x 13, -95.8 % at 241 x 101, i.e. the error survived refinement).docs/series60.mdhad explained the discrepancy as a resolution effect; it was a formula error, found by running the new geometry audit on the standard hulls. Withzeta**(1/m)andnx=161, nz=41, Cb = 0.60/0.70 land at -1.04 %/-1.14 % of the parent-form volume, first order in1/nz.analytic_vcb_above_keelwas corrected with it toT(m+1)/(2m+1)(the oldT(m+1)/(2(m+2))was 6.5 % low at m=24 and inside every sanity band, so only an independent moment recomputation catches it). Regression tests pin both, including one that reproduces the broken exponent. -
tensorcad.box_gz.box_gz_area: the IS Code areas under a barge's righting-arm curve, all the way to 89 degrees (docs/box_gz_areas.md, tables generated bytools/verify_box_gz_areas.pyso no number on the page is transcribed).tensorcad.statutoryturns a curve into the areas of MSC.267(85) part A s. 2.2 — 0-30, 0-40, 30-40 degrees — with an adaptive Simpson rule that states its own uncertainty, but the only reference it ever had was the closed-form antiderivativeGM0 (1-cos) + BM0/2 (sec + cos - 2), which stops where the heeled waterline reaches a bilge or a deck edge: 21.8 degrees for the doc barge, before any of the regulated areas is half finished. The new function integrates the exact multi-regime curve ofbox_gzinstead, keepingKG sin(phi)in closed form so onlyKNis quadratured. The interval is cut at the regime boundaries — and at their mirror across upright — becauseKN'(phi)steps there: the waterplane width jumps when a deck edge enters the water, and a five-point Gauss-Legendre rule spanning that kink falls likeh**2.5instead ofh**10(measured onint KN dphiover 0-40 deg: 4.0e-8 with 8 panels and only 6.6e-12 with 256, against round-off with eight panels per piece, i.e. 120 section evaluations in total). Evidence, three barges, all claims scripted with a non-zero exit: inside the window the integral reproduces the antiderivative to 1.6e-13 relative; the regulated and wider windows (0-30, 0-40, 30-40, 0-60, 0-89) agree withstatutory.area_under_gz_curveon the same callable to 3.0e-12, inside that module's own 1e-11 area tolerance, so the Simpson rule is now certified where no antiderivative exists; the heeled-mesh curve oftensorcad.gzagrees to 8.1e-6; and two negative controls (the wall-sided antiderivative used past its limit, which misses by 15 % to 330 %, and a 1 mmKGshift, which moves the area by 1.0e-3 relative at least) show the comparison can fail.tests/test_box_gz_area.py(50 cases) holds the integral to the antiderivative, to the fundamental theorem (d/phiof the area is the curve), to additivity across the breakpoints, to odd symmetry in the interval, and pins both the committedartifacts/m4873/box_gz_areas.jsonand the generated tables ofdocs/box_gz_areas.md. -
tools/intact_stability_validation.py: the weather criterion's arithmetic checked against closed forms (docs/intact_stability.md§6, generated by--update-docsso no number on the page is transcribed).examples/intact_stabilityalready validates the mesh-to-curve step and the verdict flags; it reportsb,a,θ_2andθ_cwithout checking them, and it reads the second intercept off the same interpolant the module uses. The new harness, ten claims, non-zero exit, compares each of them against the barge's antiderivative (GM0(1-cos) + BM0/2(sec+cos-2)) or an independent bisection of the analytic arm: the gust areabto 2.3e-4 relative,a = lw2 (θ_2 - θ_1)to round-off,θ_cto 1.3e-4 deg, theb >= aflip at the leverb / (1.5 θ_2)where the two areas cross,θ_1 = θ_fpassing 2.3.3 whileθ_1 = θ_f + 0.5 degfails, the flooding-angle substitution in 2.2 changing the area by 0.042 m rad, and the residual ofbfalling ash^2under grid refinement (ratios 3.98 / 4.00 / 4.00). The same curve throughtensorcad.statutorylands on the same closed form within 7e-9 relative, i.e. the two implementations differ only by the sibling's trapezoidal grid error. Evidence:docs/intact_stability/{intact_stability_validation.json,intact_stability_validation.png}. -
tests/test_intact_stability_validation.py(9 cases) re-runs the harness, requires a deliberately broken claim to exit 1, and pins both the committed json and the generated section ofdocs/intact_stability.mdto a fresh run, so the evidence cannot go stale. -
tensorcad.statutory: section 2.2 of the IS Code 2008 as a rule check that carries its own error bars (tensorcad/statutory.py, standard library only).check_intact_stability(curve, ...)returns oneCriterionCheckper rule of MSC.267(85) part A s. 2.2 — areas 0-30 / 0-40 / 30-40 deg (the last two capped by the flooding angletheta_fwhen supplied),GZ(30 deg), heel of maximumGZ,GM_0— plus the areas, the peak, the vanishing angle and a three-valued verdict: a margin inside the stated numerical uncertainty isindeterminate(passed is None), never rounded to a verdict, and a curve that stops or floods before the angle a rule needs isnot_assessed. Accepts a callableGZ(angle), an(angles, levers)pair, or eitherRightingArmCurveof this package. Callable curves are integrated by adaptive Simpson with the|S2-S1|/15error accumulated; tables go through monotone-Hermite (Fritsch-Carlson, no overshoot at the peak) with the trapezoid-vs-Hermite spread reported, plus a self-convergence error onGM_0;gm0_from_gz_curvereadsGM_0off the slope by Richardson, exact forGM sin(phi).docs/statutory.md+tools/statutory_validation.py: the areas reproduce the closed forms (GM(1-cos),GM0(1-cos) + BM0/2(sec+cos-2)) to 1.8e-16 relative,GM_0to 2.5e-7, the mesh route (gz.righting_arm_curveon a box barge) to 8.9e-16 m inGZ, and the reported uncertainty bounds the true error on every ladder from 20 deg down to 1 deg. -
The new module is cross-checked against
tensorcad.intact_stability, which reaches the same six thresholds by a different route (trapezoidal areas, caller-suppliedGM_0): same thresholds, same failed-rule sets after a clause crosswalk, and areas agreeing to the sibling route's ownO(h^2)grid error (1.1e-5 relative) while both are read against the closed form.docs/statutory.mds. 6 records it, including the finding that the sibling'sIS_CODE_2008_GENERALkeys flatten the code's2.2.1a/b/cinto2.2.1-2.2.3and shift the rest by one — thresholds and verdicts unaffected, clause citations in a report would be — left for its own change set rather than edited here. -
IS Code 2008 part A clause 2.3.3 — the angle of heel
θ_1shall not exceed the flooding angleθ_f— intensorcad.intact_stability.weather_criterion. The call takes a new keywordflooding_angle_deg; supplying it adds the clause to the report and puts the same angle into 2.3.2'sθ_2 = min(θ_f, 50°, θ_c), because the code uses oneθ_fin both places (WeatherCriterionReport.clausesnames the clauses a report actually evaluated). Omitting it leaves 2.3.3 not evaluated —flooding_angle_degisNoneand the verdict says so — rather than silently passing, which is the failure mode of an unimplemented clause.θ_1itself stays an input: clause 2.3.4's roll-period tables are still not in this repository. -
An empty gust span is reported, not invented. When
θ_1 ≥ θ_fthere is no interval betweenθ_1andθ_2to integrate, so 2.3.3 fails andarea_b_m_rad/area_a_m_radareNone(serialised asnull, rendered as "not measured"), instead of a fabricated zero that a reader could mistake for a measured area. Every other report is unchanged: with noflooding_angle_degthe numbers are bit-for-bit what 0.23.0 produced. -
tests/test_intact_stability.pygrows to 47 cases (2.3.3 passing,θ_fcappingθ_2and the gust areas against the antiderivative, the failing-and-unmeasured case, the not-evaluated case, andθ_foutside(0, 90)), andexamples/intact_stabilityto 17 claims, two of them on 2.3.3;docs/intact_stability.md§1 and §5 document the clause.
- IS Code 2008 part A intact-stability criteria on a measured
GZcurve (tensorcad/intact_stability.py, standard library only).general_criteriaevaluates 2.2.1-2.2.6 (areas to 30° and 40°, the 30-40° band,GZ(30°), the angle of maximumGZ,GM_0) andweather_criterionevaluates 2.3.1/2.3.2 (lw1 = P A Z/(1000 g Δ)atP = 504 Pa, the gust factor 1.5, the 80 % deck-edge heel limit, and theb ≥ agust areas up toθ_2 = min(θ_downflood, 50°, θ_c)). Both accept either curve in the package (gz.RightingArmCurve,righting_arm.RightingArmCurve) or a plain(angles, gz)pair. This closes the limitationdocs/gz.mdanddocs/stability.mdboth named: "nothing here computes a wind heeling arm, aGZarea criterion or an IMO/IS-Code check." - The clauses carry their direction.
Criterion.comparisondistinguishes "shall be at least" (2.2.1-2.2.6, 2.3.2) from "shall not exceed" (2.3.1), andmarginis the slack in the passing direction, so a>=where the code says<=cannot report a ship compliant on the weather heel. Areas are trapezoidal on the supplied polyline with both limits interpolated, so the four areas the code quotes are additive to round-off. - Verification against an antiderivative, not only a curve. The wall-sided barge's
∫GZ dφ = GM_0(1 − cos φ) + (BM_0/2)(sec φ + cos φ − 2)gives the areas a closed form:A(0,30)agrees to1.1e-4relative on a 1° grid, 1.5e-4 onA(30,40), and the grid error falls ash²from 2° down (ratios 4.00, 4.00). Reported attainments from the two waterline solvers agree to1.6e-13. Negative controls: loading the same barge 0.35 m deeper flips five clauses, and halving the passing arm fails 2.2.1, 2.2.2 and 2.2.4 on the same geometry. tests/test_intact_stability.py(39 cases),examples/intact_stability/run_example.py(15 claims, exits non-zero if one breaks, writesdocs/intact_stability/{results.json, report.md}and four figures) anddocs/intact_stability.md.- Not implemented, and said so on the page: clause 2.3.4's roll amplitude
θ_1is an input (the roll-period tables it needs are not in this repository), damage stability (part B), grain/timber-deck codes, and the 2.2 variants for special ship types.
- A second, independent route to
GZ(phi)(tensorcad/righting_arm.py), carried over from PR #40 (agent/m3169) and redeveloped on the currentmain: the heeled section is clipped face by face and integrated in the body frame (divergence theorem with no cap, each contribution exact for a planar triangle), and the equilibrium waterline is found by bracket-and-bisect on the monotoneV(h)instead oftensorcad/gz.py's Newton iteration on the projected wetted area. It addsheeled_displacement,righting_arm(one heel, withlcg),RightingArmCurve.peak/vanishing_crossed/area_under_curve,wall_sided_validity_degand per-point volume residuals. - The overlap between the two routes is now the check.
tools/verify_merge_m4038.pyputs both on the same meshes and records the agreement:1.2e-13 mon a wall-sided barge (L=10, B=2, T=0.8 m),1.6e-13 mon the41x13hull and6.9e-14 mon81x25(floated atz=-0.47 m),2.8e-16 mon a sphere of resolution 32 -- round-off, because both integrals are exact for the polygonal hull and only the waterline solver differs. A negative control is part of the record: shiftinggz's displacement target by 10 % movesGZ(20 deg)by 3.2 mm, so the comparison is capable of failing. Evidence:artifacts/m4038/{merge_verification.json,merge_verification.png}anddocs/m4038_pr40_merge_cure.md. - Each route against its own reference on the merged tree. The barge reproduces the
wall-sided closed form to
1.2e-14 minside its validity window (21.80 deg here) and the formula is 9.6 cm out at 40 deg; the sphere sits within1.3e-04 mofGM sin(phi)at resolution 32; the hull's small-angle slope matchesKB + I_T/nabla - KGfromtensorcad.stability(a plane-section integration of the same mesh) to6.8e-10relative on three grids; the volume residual stays below2.1e-12. tests/test_righting_arm.py(46 tests: PR #40's 41 plus 5 cross-route/alias tests),examples/righting_arm/run_example.py(10 claims, exits non-zero if one breaks, writesdocs/righting_arm/{results.json,report.md}and three figures),docs/righting_arm.mdanddocs/righting_arm/.
tensorcad/__init__.pyexports the new route without shadowinggz: the three colliding names are published asbisection_righting_arm_curve,BisectionRightingArmCurveandbarge_wall_sided_righting_arm, whilerighting_arm_curve,RightingArmCurveandwall_sided_righting_armkeep pointing attensorcad/gz.py.tests/test_righting_arm.pypins both directions.README.md: the "what it does not provide" sentence still listed the righting-arm curve and free-surface stability as absent, and claimed no hull family has a transom; corrected to what the tree has done since 0.21.0 (and to what it still does not do: damage and flooded-compartment stability, free-trim equilibrium, class-rule criteria).
- Waterplane second moments on the
Hydrostaticsrecord (tensorcad/hydrostatics.py):compute_hydrostaticsnow reportstransverse_waterplane_inertia(∫ y² dA),longitudinal_waterplane_inertia(∫ (x−LCF)² dA) andkeel_znext to the waterplane area. They come out of the flux identity the module already uses for the waterplane area (∫_cap f dA = −∫_wetted f n_z dAwithfindependent ofz), so no cap has to exist and no waterline polygon has to be reconstructed; the per-face second moments are exact (x,ylinear over a triangle). An open or capless surface reportsnanrather than a number the shell cannot support, andto_dictcarries all three quantities. tensorcad.hull.analytic_hydrostaticsreports the same two inertias from the closed forms (hydrostatics_analytic.waterplane_inertia_transverse— gamma identity — andwaterplane_inertia_longitudinal— tanh-sinh quadrature with its own convergence evidence), so the mesh record and the analytic reference speak the same quantities.tests/test_hydrostatics_inertia.py— the record against three independent references: a vertex-by-vertex rectangular barge (L·B³/12,B·L³/12,keel_z) exact to1e-12on three tessellations, a circular waterplane converging toπR⁴/4from below at theO(1/n²)rate, and the hull mesh against the closed forms at three sizes.- Transom-stern hull family (
tensorcad/transom.py): the aft body is truncated at a stationtauand closed by a planar transom plate, so the parametric hull can carry the stern real displacement vessels have instead of a knife edge. Two parameters —transom_fraction(tau) andbreadth_exponent(p, the sine-taper exponent) — turn it into a design space:tau = 0reproduces the pointed hull exactly (identical faces, LOA-based frame with the transom atx = 0), andtransom_fraction_for_area_ratioinvertsA_t/A_m = sin(pi*tau)^pin closed form so a loop can dial an immersed transom area directly. Topology is unchanged by the cut: watertight, edge-manifold,V-E+F = 2, outward, no zero-area faces for everytautested. Seedocs/transom_stern.md. - Hydrostatic references that survive the truncation. The incomplete taper integrals
int_tau^1 sin(pi*u)^p duare evaluated from asinc^ppower series (thep-singular factor integrates exactly, the even remainder is a series ins^2), givingV,A_wp,LCB,Cb,Cp,Cwpand the immersed transom area in closed form. Validated three ways: againstGamma((1+p)/2)/(sqrt(pi)*Gamma(1+p/2))attau = 0(2.2e-16), against theM(0)/J(0) = 1/2symmetry (1e-14), and againsttanh_sinh_quadraturefortau > 0(1.1e-15). Mesh integration then lands within 0.045–0.88 % of the reference depending on grid, monotonically converging, and the transom plate area converges to its analytic section (−0.71 % atnz = 13, −0.043 % atnz = 81). examples/transom_stern/run_example.py: reference ladder, 4 truncations × 4 grids convergence table, design table and three figures.
stability.py stopped at GM and said so; the backlog tracked 大倾角 GZ as missing. This
release adds the curve itself, by integrating the heeled displacement instead of a
waterplane moment.
tensorcad/gz.py:heeled_hydrostatics(rotate about the keel line, cut at the waterline, solve the waterline that restores the displacement by Newton with the exact derivativedV/dc = A(c); two to five iterations to a1e-13relative volume residual),righting_arm_curve(GZ = KN − KG sin φwithKGabove the keel, matchinginitial_stability's datum),max_gz/angle_of_max_gz_deg/range_of_stability_deg(interpolated) /area_under_curve(dynamical stability, m·rad, truncatable).- Verified against two closed forms, both exact for the polygonal mesh: the box barge
wall-sided trapezoid
GZ = sin φ (GM₀ + ½BM₀ tan²φ)— derived in the module docstring, not quoted — agrees to6.9e-17 mover 0–35° on a 10×2×2 m barge, and provably stops agreeing past its 38.66° limit (4.0e-04 m at 45°, 1.25 m at 70°); and the half-submerged logGZ = (R − KG) sin φ, exact at all 33 symmetry-aligned heels from 0 to 90° (1.1e-16 m). Third check:KB(0.4830051 m) anddGZ/dφat zero (0.421622996 m/rad vsGM0.421622996 m) both reproduceinitial_stability, which integrates the waterplane loops by Green's theorem instead. with_freeboard: a hull generated to its design waterline cannot be heeled (the waterline leaves the modelled surface on the dipping side immediately), so this removes the waterline cap, extrudes the boundary ring with vertical walls and closes it with a fan — watertight, outward, Euler-2, upright displacement unchanged to round-off.free_surface_correction/rectangular_tank_inertia: the flatΣi_x/Vreduction a slack tank takes off every point of the curve (2.0 × 0.5 m tank in the 7.45 m³ example: 2.80 mm).- Evidence:
docs/gz.md,examples/gz/run_example.py(writesresults.json,report.md,plot.png; exits non-zero if a claim breaks),tests/test_gz.py(26 tests; the suite pins the public surface, which grew by 14 names).
- A second hull family, and the one benchmarks use (
tensorcad/wigley.py): the parabolic Wigleyy = (B/2)(1-|2x/L-1|^n)(1-(w/T)^m)lofted with the same ring-per-station topology astensorcad.hull, so it is watertight, outward, Euler-2 and capped at the design waterline without a welding pass.generate_wigley,wigley_analytic_hydrostatics,wigley_analytic_relative_errors,wigley_convergence_studyandexport_wigleyare exported; the shape exponentsn, mare exposed because the closed forms hold for anyn, m >= 1. - Exact hydrostatics, not a fitted reference:
Cb = nm/((n+1)(m+1))(=4/9),Cp = Cwp = n/(n+1)(=2/3),Cm = m/(m+1)(=2/3),VCB = T - T(m+1)/(2(m+2))(=5T/8above the keel),V = Cb L B T; only the wetted area needs quadrature, and it is a composite Gauss-Legendre rule on the analytic flank that reports its own convergence (4e-16 atn = m = 2). - Verification debt closed for the family: on the ITTC reference proportions
(
L/B = 10,B/T = 1.6) the default 41×13 grid integrates volume 0.236 % low, waterplane 0.063 %, wetted area 0.020 %, and 161×41 comes down to 0.0195 %; observed order 2.17 / 2.58 / 2.01, every signed error negative (inscribed chords). The same 41×13 grid is 0.88 % on thesin^0.7 sqrtfamily -- the parabolic section has a finite keel angle, and that is asserted as a test. Evidence:docs/wigley.md,docs/wigley/{results.json,report.md,plot.png},examples/wigley/run_example.py(exits non-zero if any of its 10 claims breaks),tests/test_wigley.py(30 tests).
An asymmetric hull, so that the longitudinal centres stop being an identity. The family
was mirror-symmetric, which made LCB = LCF = length/2 a consequence of the symmetry: the
check validated the frame and the sign of the x moments and could not detect an
antisymmetric bias, because the geometry had none.
HullParameters.fore_exponent/aft_exponent— the sine-taper exponent per half, both defaulting to the shippedBREADTH_EXPONENT, so the default surface is the previous one bit for bit (same vertices, faces and hydrostatics). Midship belongs to both halves andsin(pi/2) = 1for every exponent, so the halves meet at the requested beam without a step and the maximum section stays atx = L/2.hull.taper_integral(exponent),hull.taper_integral_pair(fore, aft)— the closed formGamma((p+1)/2)/(sqrt(pi) Gamma(p/2+1))ofint_0^1 sin(pi x)^p dx; the shipped exponent returnsTAPER_INTEGRALverbatim so no published digit moves.- Analytic references now take the split:
analytic_volume,analytic_waterplane_area,analytic_longitudinal_centre,analytic_wetted_surface_areaandanalytic_hydrostaticsacceptfore_exponent/aft_exponent. Volume, waterplane area, section area, VCB and both coefficients stay closed form; LCB and LCF areL*(m(p_aft) + h(p_fore) - m(p_fore))/(h(p_aft) + h(p_fore)). hull._half_first_moment—int_0^(1/2) x sin(pi x)^p dx, the one quantity of this family without an elementary form, by Gauss-Legendre afterx = t^3/2;hull._half_first_moment_exp_sinhrecomputes it with an exponential substitution and a different rule. They agree to5e-15relative.tests/test_asymmetric_hull.py— 35 tests: bit-for-bit back-compatibility, the taper integral against Simpson and the half moment across two schemes, the involution on the reference and on the mesh, monotonicity and continuity in the exponent, mesh vs reference on a four-grid ladder, closedness of asymmetric meshes, parameter validation, metadata, refinement plumbing and the CLI.examples/asymmetric_hull/run_example.py→docs/asymmetric_hull/{results.json,report.md,plot.png}, exiting 1 if any of its 13 claims stops holding;docs/asymmetric_hull.md.- CLI:
--fore-exponent,--aft-exponent.
- mesh LCB error for
(p_f, p_a) = (0.5, 0.9):-7.79 mmat 41x13,-2.92at 81x25,-1.08at 161x49,-0.39at 321x97 — one sign throughout, so a discretisation bias and not noise;0.011 %of the length at 161x49. - the involution
LCB(p_a, p_f) = L - LCB(p_f, p_a)holds to1e-12 mon the reference and1e-6 mon the mesh, while the mesh volume is unchanged to1e-15under the swap. LCB == LCFon an asymmetric mesh to1e-9— on the symmetric family that is one identity, here it is two different sums that have to agree.- volume error
-0.95 %at 41x13 and-0.12 %at 161x49 for(0.5, 0.9); the analytic volume for the symmetric default is still7.512698221250894 m^3exactly.
section_area_at could only report the area of a station the mesh had a vertex
ring on, at the full depth of that ring; nothing in the package could answer "at
station x, how much area is under waterline z", which is the input to damage
stability and to the buoyancy curve of a still-water bending calculation.
tensorcad/bonjean.py:section_loops(plane cut of a closed mesh, segments matched by shared edge key, plane nudged 1e-9 off vertex rings),section_properties(clipped area, perimeter, centroid),bonjean_curve,bonjean_tablewithintegrated_volume(Simpson when the stations are even and odd in number, trapezoid otherwise), andanalytic_section_area— the closed form(2/3) B sin(pi x/L)^0.7 zeta^(3/2) / sqrt(draft)for the parametric family.- Verification: bit-exact on a barge; −5.31 % worst on the default 41x13 grid at the shallowest waterline, converging as 5.31 / 2.54 / 0.92 / 0.33 % for nz = 13/21/41/81; the trapezoid integral of the station areas equals the surface-flux displacement to 1.1e-16 relative (the loft is linear in x, so the identity is exact), leaving the −0.16 % residual as surface sampling only.
docs/bonjean.md,docs/bonjean/{results.json,plot.png},examples/bonjean/run_example.py,tests/test_bonjean.py(37 tests).- Additive only: no existing key, function or number changed.
tensorcad.stability— initial stability (KM, GM) from the waterplane moments.waterplane_moments(mesh, waterline_z)cuts the surface with the plane, chains the segments into closed loops on the mesh's own vertex/edge identity, and integrates the area and its first/second moments by Green's theorem — exact for the polygonal waterplane the mesh has. Sections are referred to their own centroid (I_T = ∫y²dA − A·y_c²), so asymmetric and heeled waterplanes integrate correctly; nested loops are detected by point-in-polygon parity and subtract.initial_stability(mesh, kg=...)returnsKB,BM_T,BM_L,KMandGMwithKGas a required loading input (no silentKG = 0). Free-surface corrections,GZ(φ), trim and class rules are explicitly not implemented, andbasissayssolid GM.hydrostatics_analytic.waterplane_inertia_transverse/waterplane_inertia_longitudinal— the closed formI_T = (B³L/12)·taper_integral(3e)(note the3e, note) and the quadratureI_L = 2BL³∫₀^{1/2} u² cos(πu)^e duwith its self-convergence estimate.examples/stability/run_example.pywritesdocs/stability/{results.json,report.md}plus three figures and exits non-zero if one of its ten claims stops holding;tests/test_stability.pyadds 19 tests;docs/stability.mdderives the formulas, reports the ladder and names the limits.
- Box (
10 × 2 × 0.8 m,KG = 0.55 m): every waterplane and metacentric quantity reproduces its closed form to 2.2e-16 — one ulp — so the section integration is exact and the hull's residual error is the mesh's. - Hemisphere (
R = 1 m, 32×64):KB = 0.625075 mvs5R/8,BM = 0.374624 mvs3R/8,KM = 0.999699 mvsR(−0.030 %): two independent integrations summing to the sphere's radius. - Hull (
10 × 2 × 0.8 m,KG = 0.6 m):I_T−0.139 %/−0.0086 %,I_L−0.604 %/−0.064 %,BM_T+0.748 %/+0.127 %,GM_T+1.99 %/+0.35 %at41×13/161×41. Fitted order ofI_Tin the grid step: 2.01.I_L's reference is rule-independent: tanh-sinh vs Composite Simpson underu = 1/2 − s²agree to 7.8e-16. GMamplifies.rel(GM) ≈ (KM/GM)·rel(KM) ≈ 2.87·rel(KM)atKG = 0.6 m, so the coarsest rung (21×9) leaves the 3 % band at+3.47 %whileI_Tis still inside it at−0.56 %;GM = 0falls atKG = KM = 0.915360 m, bracketed by theGM(KG)ladder.
- Closed-form hydrostatics reference (
tensorcad/hydrostatics_analytic.py): the coefficients, centres, areas and wetted surface of the analytic hull surface are now available as exact formulas (analytic_hydrostatics,taper_integral) instead of only as mesh integrals.Cb = 2I/3,Cp = I,Cm = 2/3,Cw = I,LCB = LCF = L/2,VCB = -0.4T,V = B·L·T·I·2/3, withI = ∫₀¹ sin(πx)^0.7 dxfrom the gamma function. The wetted area needs quadrature and uses a double-exponential rule (tanh_sinh_quadrature) that handles theζ^-1/2keel edge and theξ^-0.3ends; it reports its own convergence (5e-13relative). - Verification debt closed: every hydrostatic quantity the mesh integrator reports
now has an independent reference. Worst error is 0.939 % on the default 41×13 grid,
0.169 % on 161×41 and 0.061 % on 321×81; the wetted-area reference is independently
confirmed by a Richardson extrapolation of the mesh series to +8.8e-6.
See
docs/hydrostatics_verification.md; the self-checking ladder lives inexamples/hydrostatics_verification/run_verification.py. - Release hygiene:
tensorcad.__version__had drifted to "0.9.0" whilepyproject.tomlsaid 0.10.0. Both now read 0.13.0, and the public-surface test pins the same string in one place.
tensorcad.curvature— the fairness check the backlog had left open (P3). Two independent estimates, because the question has two audiences:principal_curvatures(field, points)reads the principal curvatures off the analytic field through the shape operatorM = P (Hess f) P / |grad f|.Pkills the normal direction, so the estimate is invariant to whether the field is a true signed distance or only an implicit function — which is what makes it work on the ellipsoid and on the learned.ngofields, not just the exact primitives. The seven gradients per probe (point plus three axis pairs) go out in one batched call; measuring that was worth an order of magnitude on the AFF-8 min-union. Each sample is Richardson-extrapolated fromhandh/2and carries its ownstep_error.vertex_mean_curvature(mesh)is the cotangent-Laplacian estimate a consumer of the mesh can recompute, with the standard cotangent clamp against slivers.assess_fairness(mesh, field=...)returns the audit: the dihedral distribution over interior edges, the crease count and length fraction above a threshold, the mesh curvature summary, and — when a field is supplied — the curvature jump across each interior edge, which is then a property of the geometry rather than of the triangulation.
tests/test_curvature.py— 20 tests: the four exact primitives against their closed forms, the invariance to|grad f| != 1, input rejection, the mesh estimator's convergence and its integral conservation, twelve creases on a cube, a sphere with none, and the stdlib-only import.examples/fairness/run_example.py— writesdocs/fairness/{results.json,report.md,plot.png}and exits 1 if one of its 15 claims stops holding.docs/fairness.md— the derivation, what each estimate is good for, and the limits.
- The analytic estimate is exact: sphere
1/R, 1/R, cylinder1/R, 0, plane0, 0(exactly zero), ellipsoid at all three poles — worst relative error1.33e-14, reported step error below1.3e-06 1/m. - The mesh estimate is exact in the integral and needs a conditioned mesh
pointwise. On a raw marching-tet sphere: pointwise median error
48.9 %, but the area-weighted mean1.000490 1/magainst the analytic1.0(0.049 %). The cotangent operator is a discrete divergence, so slivers cannot move the integral; they own the pointwise field. Aftertaubin_smooth+project_to_fieldthe pointwise median is1.42 %. - AFF-8 (22060 triangles): p50 dihedral
0.049°, p9511.94°, min0°(the NACA trailing edges),1171crease edges over 15° =3.04 %of the interior edge length. - The parametric hull family is not G1 at its ends.
b(x) = B/2 sin(pi x/L)^0.7givesdb/dx ~ x^-0.3000(fit overx/L = 1e-2 .. 1e-7, expected-0.3): infinite waterline slope at the stem and stern, so curvature diverges there. The keel line has the same problem because the section profile issqrt(z/T). Both are properties of the family, not of a mesher. - A crease threshold on a coarse mesh measures the mesh. On the hull, p95 dihedral
falls
26.80° -> 3.59°and the crease length fraction7.55 % -> 1.44 %between41x13and161x41, while the crease count rises232 -> 628. Use the distribution, not the count.
The sampled hull stops being its own reference. analytic_hydrostatics returns the
hydrostatics of the analytic surface the generator samples as the same
Hydrostatics record the mesh path returns, so displacement, waterplane area,
midship section area, LCB, LCF, VCB and the form coefficients get a closed-form
baseline and the mesh numbers get an error rather than a refinement trend.
tensorcad.hull.analytic_hydrostatics(length, beam, draft, water_density=1025)— the surface factorises as(B/2)*sin(pi*x/L)^0.7*sqrt((z+T)/T), so every volume integral splits and has a closed form: volumeL*B*T*TAPER_INTEGRAL*2/3, waterplane areaL*B*TAPER_INTEGRAL, midship section area2/3*B*T, LCB and LCFL/2, VCB-2/5*T,Cb = 2/3*TAPER_INTEGRALandCp = TAPER_INTEGRAL.basisnames which values came from a formula and which from a quadrature.analytic_wetted_surface_area— the one quantity without a closed form, integrated with an 80-point Gauss-Legendre rule afterw = u^2(removes the keel1/sqrt(w)) andpsi = (pi/2)*tau^3(removes the bow/sternx^(p-1)tip slope) put both endpoints inside the rule's convergence radius. Refining to 120 nodes moves the value by< 1e-9relative. Legendre nodes are generated by Newton iteration, so the core stays standard-library only.analytic_relative_errors(parameters)— signed relative error per quantity for a sampled grid, plusoutside_discretisation_estimate, the quantities whose true error exceeds the coarse/refined relative change thatestimate_discretisation_errorreports.tests/test_analytic_hydrostatics.py— 21 tests: the closed forms against independent Simpson quadratures, the wetted-area Gauss rule against a Simpson product rule and against node refinement, mesh-vs-closed-form error at four resolutions with the convergence direction and sign pinned, and input rejection.examples/analytic_hydrostatics/run_analytic_check.py— writesdocs/analytic_hydrostatics/{results.json,report.md,convergence.png}, exits 1 if a claim stops holding.docs/hull_analytic_reference.md— the derivation, how the reference is itself checked, and what it does not cover.
- volume
-0.880 %/-0.136 %, block coefficient the same by construction; - waterplane area
-0.175 %/-0.017 %, prismatic coefficient the same; - wetted surface area
-0.326 %/-0.074 %(reference22.9127295 m²); - midship section area
-0.707 %/-0.119 %, the largest default-grid error; - VCB
+0.939 %/+0.169 %about its closed form-2/5*T = -0.32 m: the trapezoidal section rule is high near the keel, where thesqrtprofile is steepest, so this is the one quantity that over-reports; - LCB and LCF land on
L/2to1e-9 mat every grid tested; - the refinement estimator is optimistic at the default grid: the true error is
1.53xits reported relative change for volume,1.77xfor wetted area and1.44xfor waterplane area, so it is an indicator and not a bound.
- The declared and runtime versions had drifted:
pyproject.tomlsaid0.10.0whiletensorcad.__version__still said0.9.0andtests/test_public_surface.pyasserted the old string. All three now read0.11.0, and the 0.10.0 voxel-export and benchmark work that shipped without a changelog entry is noted here rather than left silent.
Representations stop being an argument and become a measurement. One harness runs an
analytic formula, a mesh edit, a trained neural field and an optional kernel export through
the same rays, the same closed form and the same thresholds, and writes results.json plus
a generated report.md and plot.svg.
tensorcad.benchmark—Metric(value, unit, reference, tolerance, status ∈ measured/imported/not_supported/not_run/failed, and a gate verdict; a missing value can never be0and never passes a gate),RepresentationResult,Gate,CaseResult,BenchmarkResult(passed,failures,write_json,write_markdown) andrun_benchmark(quick=…). Standard library only; the neural row importstensorcad.neuralbehind an importability check.- Case A,
case_a_sphere_dent— a sphere R = 0.5 m with a Gaussian dent of depth 0.06 m, σ = 0.22 rad, whose radius is a closed form; exact volume and area by Simpson quadrature, a Möller–Trumbore ray caster, and a stratified ray set (a third of the rays inside the edit cone so a 0.22 rad feature is actually resolved). - Case B,
case_b_hull_handoff— a displacement target → damped Newton → hull → gmsh round trip, measured againsthull.analytic_volumerather than against its own mesh. examples/benchmarks/run_benchmark.py— writesresults.json,report.mdandplot.svg(standard-library SVG, surface error against wall clock), and exits 1 when any gate fails, so it is usable as a CI step.- Contract coverage — the row IDs are the ones the merged contract defines
(
analytic_field,mesh,neural_field,ocp_step,parametric_hull), P95 alongside mean/max, query throughput and artifact bytes, and training steps/time/seed imported from the bundle. Peak memory is not measured; contract Case B (AFF-8 round trip) and Case C (real solver run) are not implemented and the document says so.
- mesh edit: cone-mean depth error 2.3e-6 m, dent surface error mean 4.5e-5 m, leak outside the edit support exactly 0 m, all-ray surface error 1.0e-4 m (P95 1.8e-4 m);
- trained neural field (
.ngo, weightsdb1df71e…, 221,620 bytes): cone-mean depth error 1.9e-3 m (3.2 % of the edit depth), dent surface error mean 2.1e-3 m, imported parent-relative leak 1.3e-3 m (P95 3.3e-3 m), all-ray surface error 3.4e-3 m (P95 9.2e-3 m); - the same 1 %-of-depth gate applied to both: the mesh row passes, the neural row fails, and the run exits 1. No looser tolerance was invented for the learned field;
- the shipped grid is 80×160 because its measured chord-sag floor (1.9e-4 m) sits below the 6.0e-4 m gate; 40×80 happens to pass with a floor above the gate, which is luck;
- the bundle's own claim (2.32e-3 m) and this harness's re-measurement (2.12e-3 m) agree, which is why imported numbers are kept beside measured ones instead of replacing them;
- case B: 0.880 % volume error at (41, 13), residual 2.2e-16 in 6 evaluations, gmsh node round trip exact (0 m) and area to 6.8e-16, refinement +0.577 %.
examples/neural_geometry/edit_sphere_dent.pywrote a bundle that verified but could not be evaluated:state.jsonlackedfield_state.radiusandcoordinateslackedbounds, sofield_from_objectraised a format error. Both are now written, and re-running the example reproduces the same weights hash bit-for-bit.at_most/at_least/equalsnow record the limit they compared against, so a publishedresults.jsonstates the threshold that produced each verdict.
The first release whose input is a requirement rather than a parameter set. 0.7.0 could measure and hand off a surface; 0.8.0 finds the surface that meets a brief and refuses to deliver one that does not.
tensorcad.design—DesignTargets(equalitiesdisplacement_m3,beam_to_draft,length_to_beam,draft_m; budgetwetted_area_m2_max),DesignProblem(damped Newton onlogof the free dimensions, central-difference Jacobian, backtracking line search, bounds from the hull model's own specs) andDesignSolution(status,solved,failed_checks,convergence_order,sensitivity,manifest). Every iteration is kept with its hydrostatics, itstensorcad.geometry.handoff/1record and its full check list. Standard library only.- Measured: recovering L = 12 m, B = 2.4 m, T = 0.9 m from its own measured
displacement (12.063418 m³), B/T and L/B converges in 6 evaluations and 1.1 s to
1e-7 relative on all three dimensions; observed contraction order 1.940; the
finite-difference sensitivities come out at +1.000000 / −1.000000 for a ratio and
+1.000000 for displacement against beam and draft; a replay reproduces the mesh digest
814f4770b0189c58; re-opening the design for a 15 % heavier brief converges from the previous answer in 5 evaluations. - The verdict is a conjunction. A 200 m³ target at L = 10 m drives the residual to
1.13e-12 and is still refused: at L/B = 0.965 the triangles fail the minimum-angle,
aspect-ratio and sliver checks, so
solvedisFalse,failed_checksnames all three, and the manifest reportssolver.status: "not_ready". - Grid sensitivity is part of the record: the same design measures 12.063418 m³ at (41, 13) and 12.133078 m³ at (81, 25) — +0.577 % — so a reader can see how much of the answer is the sampling grid.
write_solver_manifest/write_design_json— the manifest a solver reads (tensorcad.design/1: targets, every iteration, handoff, reference quantities, verification) and the cheap per-iteration digest an agent polls between steps.examples/design_loop/run_example.py— 21 acceptance checks, writes OBJ, gmsh 2.2, the FEM JSON and the solver manifest; in CI.
- Displacement and wetted area are deliberately not both offered as equalities: on this family they both scale with beam times draft, so the pair is nearly rank deficient. The loop stalls at a residual of 4.0e-3 and cannot remove it. Wetted area is a budget instead, and the docs record the measurement.
solver.executed_hereisfalsein every manifest: this repository measures and hands off geometry, it does not run a flow or a structure solve.- 369 tests (355 pass without the
neuralextra; 14 skip), 93.2 % line coverage, strictmypyclean,ruffclean, seven examples in CI.
- A hydrodynamic objective (resistance, stability, seakeeping), multiple hull families, and topology search. The loop matches the stated targets and proves the geometry; it does not judge whether the ship is good.
Mesh conditioning and the two solver exits. The 0.6.0 release closed with "mesh smoothing/decimation: 1.5–3.2 % of triangles are thinner than 2°; repairing them is the next piece of work"; this release repairs them, measures the shape drift that repair causes, and hands the result to TensorFEM (and to any tool that reads gmsh or STEP) without adding a mandatory dependency.
tensorcad.mesh_conditioning—taubin_smooth(λ 0.5 then μ −0.53, a band-pass so the body does not shrink),project_to_field(one Newton step per vertex along the field gradient, with a per-vertex step limit and a 4-step backtracking line search),decimate(quadric-error collapse under the link, orientation, duplicate-face and aspect-ratio guards, with every rejected collapse counted),measure_meshandcondition_mesh, which refuses to return a surface whose watertightness, manifolding, Euler characteristic, volume (0.5 %) or area (1 %) moved beyond its declared tolerance.- Measured on AFF-8 at
(150, 18, 26)(65,812 triangles, 1.664 % slivers): Taubin ×30 → min angle 0.0080° → 8.3202°, no slivers, volume −0.127 % of the analytic 0.70754064 m³ in 0.4 s; + projection ×30 (53.6 s) → 3.6590°, aspect 17.6, volume −0.117 %, area −0.198 %, worst displacement 18.4 mm, worst residual|f|0.57 mm;- decimation to 45,000 / 30,000 / 20,000 faces in 5.4 / 9.7 / 11.6 s → min angle 2.70 / 2.56 / 2.40°, zero slivers, volume drift −0.12 / −0.14 / −0.21 %.
- The decimate-first order is reported, not recommended.
condition_meshat the same budgets reaches 2.21° (45 k) and 0.149° with 28 slivers (30 k); at 45 k faces it costs 40 s against 5 s smoothing-first. Both pass the shape gates; the example prints both tables. tensorcad.adapters.fem— solver-neutral handoff needing no extra: node/element tables, boundary patches assigned by nearest analytic component, gmsh 2.2 ASCII with one physical tag per patch, and a JSON manifest. Round trip: worst node displacement 0.0 m, worst per-patch area difference 2.2e−14 m², digest identical to the source surface.tensorcad.adapters.ocp— optional STEP bridge (pip install "tensorcad[ocp]"): sew → shell → solid → STEP, with the free-edge count, shell closure,BRepCheck_Analyzerverdict, kernel volume, written size and re-imported volume all reported. Measured on 14,764 AFF-8 triangles: 12.8 s sewing, 4.4 s writing, 0 free edges, kernel volume 0.694708267 m³ against a mesh signed volume of 0.694708267 m³ (+5.3e−15 relative), re-import −3.1e−13 relative, 37.14 MB. Detached islands are kept and counted (shells/solids) rather than dropped.
- The STEP file is a faceted solid: one planar face per triangle (14,764 faces). That
is what a solver-facing exchange file needs; a B-rep feature model is issue #4.
write_stepraisesOcpUnavailablewith the pip command when the extra is absent — it never writes a mesh file with a.stepextension. - 357 tests in the default lane (336 without the
neuralextra), plus 10 OCP tests that run when the wheels are installed; 93.2 % line coverage overall, withtensorcad/adapters/ocp.pyat 32 % in the default lane because its tests skip there. Strictmypyclean on 22 files (OCP is an untyped import behind a documented override),ruffclean.
- Mesh refinement or remeshing to a target size field: conditioning only coarsens.
- A B-rep/STEP feature model, booleans, fillets, sweeps.
The DARPA SUBOFF appendages (AFF-8): the sail and the four cruciform stern fins, on top
of the 0.5.0 bare hull. Both are published closed-form predicates, so they are
implemented as formulas — no boolean engine, no CAD kernel, no CSG tree. The three
solids are unioned as min of their fields, which is exact for the sign and the zero
set. examples/suboff_appendages/run_example.py enforces every claim below;
docs/suboff_appendages.md records the numbers.
tensorcad.suboff_appendages.SuboffSailField— the published three-segment sail: entrance polynomial, parallel middle, exit polynomial, plus the elliptical cap (Zmax = 0.109375 fthalf width, body top1.507813 ft, axial[3.032986, 4.241319] ftof the 14.291667 ft hull). The section is one radial coordinateρ = |y|below the body top and√(y² + (2(z − YTMP))²)above it, so the cap and the body need no union.height(the cap apex, 0.47625 m) is a property.tensorcad.suboff_appendages.SuboffFinsField— the swept NACA 4-digit cruciform: chordc(r) = −0.466308 r + 0.88859ft, thickness(0.2969, 0.126, 0.3516, 0.2852, 0.1045), spanr ∈ [0.075, 0.825] ft, trailing edge atx = 13.146284 ft. The planform is a convex quadrilateral in(x, r), whose distance is computed exactly (four edge distances), not as a gap.SuboffAppendagesFieldandSuboffFullField— the sail ∪ fins, and the whole AFF-8 model hull ∪ sail ∪ fins.sail_scale/fin_scalereproduce TensorLBM's similarity axes (sail about its footprint centre, centreplane and deck plane; fins about their trailing edge and root radius), so a scaled appendage stays attached.- Verified against the source twice. The section values match TensorLBM's
transcription exactly (
0.00e+00 fton the sail,6.9e-18 fton the NACA section); containment matches the actual torch predicates on 18 hand-picked labels and on 20,000 random points at each of three scales with 0 mismatches, against a verbatim in-repo transcription of those predicates that CI can run. - An independent volume reference. Quadrature over the published sections gives
sail 0.00916043 m³ (46.2 % of it outside the hull), fins 0.00785718 m³, and the full
model 0.70754064 m³ (+1.191 % over the bare hull). The exported
(436, 52, 76)mesh lands at 0.707120 m³ (−0.059 %) with 569,056 triangles, 6.350646 m² of surface, watertight, edge-manifold, genus 0. - The magnitude is measured, not claimed. The cap and the thickness surface have no
closed-form nearest point, so the fields declare
quantity = "implicit_function"and report the first-order normalised gap. Along the analytic normals: sail 0.0012 mm median error at a 0.5 mm offset, fins 0.00001 mm;|∇f|is 0.996 (sail) and 1.000 (fins) at the median, and exact on the flat faces.
- The fins alone are a genus-1 closed shell (
V − E + F = 0) and that is the published geometry, not a meshing bug: the blades start atr = 0.075 ftand touch near the root, leaving the axis column empty. The hull fills it, so the full model is genus 0. Both are asserted. - Throughput: 77 k queries/s on a 20,000-point batch for the full model (~1.6× the bare hull's cost).
- 314 tests (295 without the
neuralextra), 95.9 % line coverage;suboff_appendages99 %. Strictmypyclean on 19 files,ruffclean.
- Mesh smoothing/decimation: 1.5–3.2 % of the triangles are thinner than 2°, on the NACA knife edges. Reported as warnings; repairing them is the next piece of work.
- The AFF-2/3/4/7 appendage layouts, explicit intersection curves, and therefore STEP.
The first real hull form on the geometry core: the DARPA SUBOFF bare hull (AFF-1) as an
analytic signed-distance field, with its offset-table provenance, an analytic reference,
anisotropic meshing, and a measured export. examples/suboff/run_example.py enforces
every claim below; docs/suboff.md records the numbers.
tensorcad.suboff.SuboffHullField— the four-segment DARPA offset table (bow polynomial, parallel midbody, sextic stern taper, elliptical stern cap) in its published feet, carried as a true signed distance in metres.L = 4.356100 m,R = 0.254000 m,L/D = 8.575, bow atx = 0, stern atx = length.|s|matches a brute-force 400,001-sample meridian to2.4e-08 m(0.09 ppm of R); the gradient is the exact unit normal (1.000000, agreeing with central differences to1.7e-08).- Provenance in the record, not in a document.
parameters()/provenance()carry the source (Groves, Huang & Chang 1989, DTRC/SHD-1298-01), the transcription (TensorLBMsuboff_cad.suboff_radius_profile), the unit convention, the axial origin and the profile description. The profile agrees with that transcription to0.00e+00at every station tested. suboff_reference_geometry()— analytic volume, wetted area, midship area and prismatic coefficient by quadrature, reported at two resolutions with their difference (volume 0.000 ppm, area 0.005 ppm between 10⁵ and 10⁶ samples).V = 0.699211 m³,A = 5.988264 m²,Cp = 0.79194.- Anisotropic export.
to_meshandextract_surfacenow accept three cell counts per axis, andsample_gridmarches a non-cubic grid. A 8.575:1 hull at(218, 26, 26)(147,368 cells) reaches −0.138 % volume / −0.066 % wetted-area error with 4.3 % slivers, where an isotropic64³(262,144 cells) reaches −0.089 % / −0.080 % with 10.0 % slivers and 64 stations along the hull.
test_public_surfacepoisoned the interpreter. It deletednumpyfromsys.modulesand reloadedtensorcadto prove the core needs no third-party stack, which left torch's numpy bridge broken for every later test in the session — a real order-dependent failure, observed when the file was run beforetest_geometry_field.py. The check now runs in a fresh interpreter, like the neighbouring opt-in tests.- A scale-dependent "degenerate face" criterion. 0.4.0 counted faces with an area below 10⁻¹² m², which is a different geometric claim on a 0.3 m cylinder and a 4.4 m hull. Tests and benchmarks now use area below 10⁻⁶ of the longest edge squared. The corrected 0.4.0 record: the cylinder at 40 cells emitted 8,108 faces below 10⁻¹² m² (12.4 %), 2,936 of them with an aspect ratio above 10⁶ — not, as 0.4.0's notes said, "exactly zero area".
- 281 tests (262 without the
neuralextra), 95 % line coverage. - TensorLBM's
r_over_ldefault is1/(2·8.57) = 0.058343(R = 0.254148 m); the offset table gives0.058310(R = 0.254000 m). This module uses the table, which makes its wetted area 0.060 % smaller than TensorLBM's published value. At TensorLBM's radius the two implementations agree to+0.00000 %, so the whole difference is the radius convention. - Mesh quality is reported, not repaired: at
(218, 26, 26)4.3 % of triangles are thinner than 2°, concentrated at the two tips where the meridian slope is infinite. There is no smoothing or decimation yet.
The continuous geometry core (CGC v0.1): analytic and learned geometry now answer the
same questions, so they can be interchanged, compared and handed to a solver without
each consumer learning a second API. Everything here is measured by
examples/geometry_core/run_benchmark.py, which fails the build if a claim stops
being true; see docs/geometry-core.md for the protocol and the numbers.
- One protocol for every representation.
tensorcad.geometry_field.GeometryFielddefines batchsigned_distance,gradient,bounds, editablestate/with_state,parameters,provenance,metadata,contains,numerical_gradientand an optionalnumpy_callablefast path. Declared per field, not assumed:kind,quantity,inside_sign,surface_isovalue,units,coordinate_frame. - Four exact primitives —
PlaneField,SphereField,CylinderField(capped, arbitrary axis),EllipsoidField. Measured against independently recomputed closed forms, the difference is0.00e+00in double precision. Gradients are analytic and match central differences to2.7e-11 … 2.0e-10.EllipsoidFielddeclaresquantity="implicit_function": its zero set is exact, its magnitude is not a distance. - Edits that provably do not drift.
with_statereturns a new field and leaves the original untouched. Measured: sphere radius 0.5 → 0.65 moves the surface 0.150000 m in all 24 probe directions with the centre unchanged; a cylinder height edit leaves the barrel value bit-identical; an ellipsoid axis edit leaves the other two axes alone; unknown controls raiseFieldErrorinstead of being ignored. to_mesh(field, resolution)— field → closed mesh → the existing hull acceptance pipeline. At 40 cells per axis: sphere −0.129 %, capped cylinder −0.167 %, ellipsoid −0.134 % volume error; all watertight, edge-manifold,V − E + F = 2, no hard errors.inspect_mesh_quality— smallest interior angle, worst aspect ratio and sliver share as warnings: a closed solid can still be a poor discretisation.SampledField.handoff()andmesh_digest()— a self-describing record (schema, mesh SHA-256 over float64 bits, units, frame, counts, topology, volume, area, bounds, validation, quality) for the TensorFEM handoff.surface_point(field, direction)— ray-bisected surface position, so representations are compared by where their surfaces are rather than by field values that are not comparable.NeuralGeometryField/field_from_object— a published.ngorevision behind the same protocol, withquantity="truncated_signed_distance", autograd gradients and conditioned edits. Measured on the committed bundle: surface 3.98 mm mean / 9.33 mm worst from the analytic sphere away from the dent; dent axis 441.73 mm against a 440.00 mm target; conditioned radius 0.45/0.55/0.60 reproduced within 0.33/0.08/0.01 %.
- Squashed triangles from grid-vertex coincidence. A radius-0.3 cylinder in a ±0.3
box is exactly 20 cells across, so the level set passed through grid vertices and
marching tetrahedra emitted 8,108 faces with an area below 10⁻¹² m² (12.4 % of
65,144), 2,936 of them with an aspect ratio above 10⁶, and a 22.4 % sliver share.
to_meshnow offsets the sampling box by an irrational fraction of a cell per axis: every count → 0, sliver share → 2.3 %, volume error unchanged at −0.17 %. - Finite differences are the wrong derivative for a float32 field. At
step=1e-6a central difference of the network is dominated by rounding (0.023 disagreement with autograd); at1e-4it agrees to3e-4.NeuralGeometryField.gradientnow uses torch autograd, andnumerical_gradient's docstring states the step requirement.
- The learned field's
|∇f|ranges 0.65–1.39, i.e. it is not a metric: distance queries must read the field value and surface queries the zero set. Beyond the truncation band the outward cosine drops from 0.983 to 0.721 — the truncation caveat, measured. - The bundle's recorded
dent_peak_measured_m = 0.052690is parent-relative; re-measuring against the analytic sphere gives 0.058268 m. Both are correct and both are printed side by side; the difference is the parent's own 5.58 mm axis error. - 262 tests (245 without the
neuralextra), 95 % line / 91 % branch coverage.
The review of 0.3.0 found four defects that could crash a legal model, corrupt an active bundle or delete an export. They are fixed here, with regression tests that fail on the old behaviour; nothing was re-thresholded to make a gate pass.
- Convergence estimate crashed on a legal model.
estimate_discretisation_errorguarded the requested grid (nx*nz), then built the refined parameters, which could exceed the4001 x 513limits (nx=2002, nz=3raisednx must be <= 4001 stations, got 4003). It now reportsskippedwith the refined grid in the reason. The refined parameter set also keepswater_densityinstead of silently falling back to 1025. - Half-written export on failure.
export_hullwrote the OBJ and CSV before the metadata, so a failure during reporting left a directory with a mesh and no record of the parameters that produced it. The metadata document is now built first and all three files are written afterwards. tensorcad-hullhad a half-open error boundary. Onlygenerate_hullwas inside atry; export and metadata failures escaped as tracebacks. Every phase now reportsstage+error(JSON on stdout under--json, human text on stderr) and exits 2. The CLI also computes the metadata document once instead of generating the refined grid twice.tensorcad-neural verifyrequired torch. Thebuild_fieldimport sat outside the error boundary, so a torch-free environment got aModuleNotFoundErrortraceback even though the manifest, hash and history checks need no neural extra. Those checks now run everywhere,field_queryreportsskippedwhen the extra is absent, andverifygained derived-artifact integrity checks (presence, SHA-256, size) as warnings.- Derived meshes could be written into the live bundle before publishing.
apply_dent_editextracted straight intobundle/derived/and then committed, so a rejected or failed commit left files behind that no manifest described. Extraction now happens in a scratch directory and travels inside the staged bundle. - Extraction was invisible and destructible.
extract_objectwrotederived/surface.obj|stlwithout registering them, and the next commit -- which swaps the whole bundle directory -- deleted them silently. Extraction into the object now publishes its own revision (lineage.operation = "extract", weights untouched) and registers the records; an edit carries the parent's registered artifacts forward, marked withsource_revision.extract --output DIRremains an unregistered export, which is now stated in the docstring. parse_manifestdid not validatederived[]. Readers accepted an escaping../../../../etc/passwdpath, a non-hex digest or a negative size, soinfowould print them. Records are now validated exactly like the writer builds them.ObjectStore.history()also rejectsNaN/Infinityinstead of accepting the tokens this project refuses to write.max_section_areawas quadratic on non-grid meshes. It re-scanned every vertex per station, which is invisible on a lofted hull and ruinous on an extracted implicit surface: 14.1 s of a 14.1 scompute_hydrostaticsat 64^3. One bucketing pass makes itO(V + stations); the same call now takes 0.34 s and returns the identical numbers (a test pins it against the old definition).
- Coarse-grid warning:
generate_hullcompares the mesh-integrated volume against the analytic volume of the generating functions and warns above 2 % (nx=3, nz=3is -35.7 % and used to reportwarnings == []).analytic_volumeis public, and the benchmark example now imports it instead of duplicating the formula. tensorcad/py.typed, so the strict typing this package is developed under is actually visible to PEP 561 consumers.- Regression tests for every item above, including a parametrised overflow matrix and a "derived files survive an edit" test.
- Test suite: 194 tests without the neural extra, 213 with it (was 175 / 189); 96 % branch coverage.
- Corpus bundles:
verifyreports recorded-but-absent derived meshes as warnings. The committed experiment bundle intentionally omits the 20 MB exports, so it now shows two warnings -- which is the honest description of what is in the repo.
Geometry can now be stored as an object rather than a mesh: a learned continuous field plus the metadata, versioning and transactions needed to trust it. STL/OBJ become derived exports instead of the source of truth.
tensorcad.neural_geometry(standard library only) — the NGO v0.2 bundle format: manifest with lineage, field semantics, state, training, validation, provenance and derived-artifact records; SHA-256 verification; path-escape rejection;history.json;ObjectStorewith stage → validate → commit/rollback, atomic directory swaps, optimistic concurrency (expected_parent), and JSON-safe history records for failed attempts.tensorcad.neural(optionalneuralextra) —ConditionalTSDFfield runtime, base training, replay-protected local edits, measurement of dent peak / dent-region error / protected-region mean, P95, P99, max, and a marching-tetrahedraextract_surfacethat emits an already-outward-oriented, watertight mesh.tensorcad.cli_neural/tensorcad-neural—fit,edit,extract,info,verify; every command prints measured numbers, supports--json, and exits non-zero when a gate fails (a rolled-back edit exits 1, not 0).write_stl(binary STL) for derived meshes, alongside the existing OBJ writer.examples/neural_geometry/run_object_pipeline.py— the end-to-end benchmark: fit → edit → extract → verify → deliberate rejection, with assertions.schemas/neural-geometry-manifest-v0.2.schema.jsonanddocs/neural-geometry-format.mddescribing what the format stores, what it replaces, and what it does not.- Coverage gate (
fail_under = 90, currently 96 %) and a torch-freeneuralCLI help check in the default test job.
- Manifest v0.2 keeps the 0.1 top-level file keys, so older bundles still load; unknown fields are preserved rather than dropped.
- Protected-region gates now include P99 and max, not only the mean, and failed
edits report
failed_checksinstead of a single generic reason. - The dent metrics are split into
dent_peak_*(on the axis) anddent_region_error_mean_m(inside the cone), so a peak target is never compared against a region average. - The dent cone is always sampled with a deterministic stratified grid, so the dent metrics do not silently become infinite when few random rays land in it.
- Torch↔NumPy conversion falls back to list conversion when the installed wheels have mismatched ABIs (torch 2.2 with NumPy 2.x) rather than failing a run.
- Test suite: 189 tests (from 142), 96 % branch coverage on the measured modules.
- The only fitted shape family is the sphere (with radial state and local dents); ellipsoids, cylinders and hulls are not wired into the object layer yet.
- Field accuracy is about 3 % of the radius at 0.5 m; extraction at 96³ takes several seconds on CPU.
The exported surface becomes a solid a solver can actually use, and the checks that matter are now enforced rather than documented.
- Inverted winding. The port half of the hull was lofted with the same face
order as the starboard half, so its normals pointed inward and the mesh's
signed volume cancelled to exactly zero. Solved structurally: one closed
section ring per station, plus a generic
orient_outwardpass, instead of two mirrored sheets. - Tolerance welding artefacts. Merging the two mirrored sheets by a floating point tolerance produced coincident duplicates along the keel and one non-manifold edge. Vertices are now shared by exact position at build time.
- Open, unclosed hull. The surface ended at the waterline with no deck face; it is now capped, closed, edge-manifold, and has Euler characteristic V-E+F = 2.
- Non-exact waterline plane.
z = -draft + draft * (nz-1)/(nz-1)left the waterline vertices about 1e-16 off the plane, breaking coplanarity assumptions; the mapping is nowz = -draft * (1 - fraction), which is exact. - Public API.
export_hullwas not importable fromtensorcadalthough it was the documented entry point; it is re-exported with the rest of the API. - OBJ precision silently altered the model. Files were written with 9
significant digits; export now uses 17 so
read_obj(write_obj(mesh))returns the identical vertex table (a test locks this in).
tensorcad.geometry/tensorcad.mesh/tensorcad.parameters/tensorcad.validation/tensorcad.export/tensorcad.hydrostatics/tensorcad.adapters— the module boundaries the README promised in 0.1 are now real, with generic mesh algorithms kept out of the ship-specific model.- Validity checks that a geometry foundation has to provide: watertightness, edge
and vertex manifoldness, outward orientation, zero-area faces, Euler
characteristic, volume-bounded-by-bbox, and realised-versus-requested L/B/T.
generate_hullraises instead of returning a model that fails them. - Hydrostatics integrated from the mesh with the divergence theorem: displaced
volume, displacement mass, wetted surface, waterplane area, LCB, LCF, VCB, Cb,
Cp, and maximum section area — all reported in
metadata.json. - Measured discretisation evidence:
estimate_discretisation_errorregenerates on a refined grid and records the relative change (default grid: 0.88 % under the analytic volume, 0.14 % at nx=161/nz=41). - Dimensionless ratio checks (
L/B,B/T,T/L) reported as warnings, and hard bounds onnx/nzso an accidental1e8fails fast instead of allocating. - Explicit
nxparity reporting: with an even station count no station lands on midship, so the realised beam falls short; that is now a named warning instead of a silent ~1 % dimensional error. - CLI:
--density,--json,--no-export,--no-convergence, non-zero exit codes with the full validation list on stderr. examples/parametric_hull/run_example.py: a runnable benchmark that verifies closure, orientation, dimensional fidelity, hydrostatics against the analytic value, and a resolution study — and exits non-zero if a claim fails.LICENSE(Apache-2.0 text was declared inpyproject.tomlbut absent),CHANGELOG.md,.gitignore, ruff + mypy (strict) + coverage (90 % gate, currently 99 %) configuration, and a Python 3.13 CI matrix entry with a byte-identical reproducibility check.- 129 tests, up from 8.
HullModelholds aMesh(vertices/facesremain as accessors), plusvalidationandhydrostatics;metadata()no longer re-runs validation on every call.- Face indices in the API are 0-based; the OBJ writer applies the 1-based offset.
validate_hull(model)keeps its signature and returns the list of error strings.metadata.jsonschema version is2.0.
- Initial package: deterministic parametric sampled hull surface, OBJ/CSV/JSON export, CLI entry point, structural validation, and CI.
Export becomes a verified step instead of a dump.
tensorcad.gate—gate_mesh/enforce_gate/MeshGateReport/MeshGateError: a hard-fail gate over closed, edge- and vertex-manifold, single-component,V - E + F = 2, outward-oriented, finite, zero-area-free meshes, plus optional reference-volume, facet-count and sliver limits. Every reference is external to the mesh (analytic displacement, requested component count, genus-0 topology).tensorcad.hull.resolve_resolution—ResolutionPlan: the coarsest grid whose integrated displacement is within a requested relative error ofhull.analytic_volume, with an honestaccepted = Falsewhen the generator caps or a triangle budget is reached.export_hull(..., stl=True, target_volume_error=..., gate=...)— resolves the grid, regenerates, gates, writeshull.stl, reads it back and re-runs the gate on the recovered soup; the plan, gate evidence and read-back volume error land inmetadata.json.tensorcad.export.read_stl— binary STL reader that rebuilds shared vertices by exact float32 welding, so an exported soup can be checked for watertightness; ASCII STL and truncated/non-finite files are rejected with explicit messages.examples/hull_stl_export/run_example.py— resolution ladder against the analytic volume, a rejected 178-triangle export, a resolution-controlled STL export and its read-back gate; exits non-zero if any claim fails.- CLI —
--stl,--gate,--target-volume-error.
- 11×5 / 178 triangles: −5.290 % (gate rejects at the default 2 % tolerance);
- 41×13 / 1998 triangles: −0.880 %; 161×41 / 25 918 triangles: −0.136 %;
target_volume_error = 0.002from an 11×5 request resolves to 161×65 / 41 278 triangles, −0.0759 %;- STL round trip: watertight,
V - E + F = 2, volume error 2.4e-8 relative (float32).
- The STL header no longer claims every STL came from a neural object.