// Simple numbering for non-book documents #let equation-numbering = "(1)" #let callout-numbering = "1" #let subfloat-numbering(n-super, subfloat-idx) = { numbering("1a", n-super, subfloat-idx) } // Theorem configuration for theorion // Simple numbering for non-book documents (no heading inheritance) #let theorem-inherited-levels = 0 // Theorem numbering format (can be overridden by extensions for appendix support) // This function returns the numbering pattern to use #let theorem-numbering(loc) = "1.1" // Default theorem render function #let theorem-render(prefix: none, title: "", full-title: auto, body) = { if full-title != "" and full-title != auto and full-title != none { strong[#full-title.] h(0.5em) } body } // Some definitions presupposed by pandoc's typst output. #let content-to-string(content) = { if content.has("text") { content.text } else if content.has("children") { content.children.map(content-to-string).join("") } else if content.has("body") { content-to-string(content.body) } else if content == [ ] { " " } } #let horizontalrule = line(start: (25%,0%), end: (75%,0%)) #let endnote(num, contents) = [ #stack(dir: ltr, spacing: 3pt, super[#num], contents) ] #show terms.item: it => block(breakable: false)[ #text(weight: "bold")[#it.term] #block(inset: (left: 1.5em, top: -0.4em))[#it.description] ] // Some quarto-specific definitions. #show raw.where(block: true): set block( fill: luma(230), width: 100%, inset: 8pt, radius: 2pt ) #let block_with_new_content(old_block, new_content) = { let fields = old_block.fields() let _ = fields.remove("body") if fields.at("below", default: none) != none { // TODO: this is a hack because below is a "synthesized element" // according to the experts in the typst discord... fields.below = fields.below.abs } block.with(..fields)(new_content) } #let empty(v) = { if type(v) == str { // two dollar signs here because we're technically inside // a Pandoc template :grimace: v.matches(regex("^\\s*$")).at(0, default: none) != none } else if type(v) == content { if v.at("text", default: none) != none { return empty(v.text) } for child in v.at("children", default: ()) { if not empty(child) { return false } } return true } } // Subfloats // This is a technique that we adapted from https://github.com/tingerrr/subpar/ #let quartosubfloatcounter = counter("quartosubfloatcounter") #let quarto_super( kind: str, caption: none, label: none, supplement: str, position: none, subcapnumbering: "(a)", body, ) = { context { let figcounter = counter(figure.where(kind: kind)) let n-super = figcounter.get().first() + 1 set figure.caption(position: position) [#figure( kind: kind, supplement: supplement, caption: caption, { show figure.where(kind: kind): set figure(numbering: _ => { let subfloat-idx = quartosubfloatcounter.get().first() + 1 subfloat-numbering(n-super, subfloat-idx) }) show figure.where(kind: kind): set figure.caption(position: position) show figure: it => { let num = numbering(subcapnumbering, n-super, quartosubfloatcounter.get().first() + 1) show figure.caption: it => block({ num.slice(2) // I don't understand why the numbering contains output that it really shouldn't, but this fixes it shrug? [ ] it.body }) quartosubfloatcounter.step() it counter(figure.where(kind: it.kind)).update(n => n - 1) } quartosubfloatcounter.update(0) body } )#label] } } // callout rendering // this is a figure show rule because callouts are crossreferenceable #show figure: it => { if type(it.kind) != str { return it } let kind_match = it.kind.matches(regex("^quarto-callout-(.*)")).at(0, default: none) if kind_match == none { return it } let kind = kind_match.captures.at(0, default: "other") kind = upper(kind.first()) + kind.slice(1) // now we pull apart the callout and reassemble it with the crossref name and counter // when we cleanup pandoc's emitted code to avoid spaces this will have to change let old_callout = it.body.children.at(1).body.children.at(1) let old_title_block = old_callout.body.children.at(0) let children = old_title_block.body.body.children let old_title = if children.len() == 1 { children.at(0) // no icon: title at index 0 } else { children.at(1) // with icon: title at index 1 } // TODO use custom separator if available // Use the figure's counter display which handles chapter-based numbering // (when numbering is a function that includes the heading counter) let callout_num = it.counter.display(it.numbering) let new_title = if empty(old_title) { [#kind #callout_num] } else { [#kind #callout_num: #old_title] } let new_title_block = block_with_new_content( old_title_block, block_with_new_content( old_title_block.body, if children.len() == 1 { new_title // no icon: just the title } else { children.at(0) + new_title // with icon: preserve icon block + new title })) align(left, block_with_new_content(old_callout, block(below: 0pt, new_title_block) + old_callout.body.children.at(1))) } // 2023-10-09: #fa-icon("fa-info") is not working, so we'll eval "#fa-info()" instead #let callout(body: [], title: "Callout", background_color: rgb("#dddddd"), icon: none, icon_color: black, body_background_color: white) = { block( breakable: false, fill: background_color, stroke: (paint: icon_color, thickness: 0.5pt, cap: "round"), width: 100%, radius: 2pt, block( inset: 1pt, width: 100%, below: 0pt, block( fill: background_color, width: 100%, inset: 8pt)[#if icon != none [#text(icon_color, weight: 900)[#icon] ]#title]) + if(body != []){ block( inset: 1pt, width: 100%, block(fill: body_background_color, width: 100%, inset: 8pt, body)) } ) } // arxivai — Quarto Typst template partial. // Replaces Quarto's default `article()` with the arxivai preprint layout. // Both packages are importable from raw ```{=typst}``` blocks in the body: // #arxivai.operation-box(...) #arxivai.app-figure(...) #lettreai.badge(...) #import "lettreai.typ" as lettreai #import "arxivai.typ" as arxivai // --------------------------------------------------------------------------- // Re-theme Quarto's automatic callouts (same contract as lettreai: block // nesting kept structurally identical to Quarto's default so the crossref // figure show rule in definitions.typ keeps working). // --------------------------------------------------------------------------- #let callout( body: [], title: "Callout", background_color: rgb("#dddddd"), icon: none, icon_color: black, body_background_color: white, ) = { let accent = if icon_color == black { lettreai.colors.oxford-blue } else { icon_color } block( breakable: false, fill: lettreai.tint(accent, 0.94), stroke: (left: 3pt + accent, rest: 0.5pt + lettreai.tint(accent, 0.5)), width: 100%, radius: 3pt, block( inset: 1pt, width: 100%, below: 0pt, block( fill: lettreai.tint(accent, 0.84), width: 100%, inset: 8pt, )[#if icon != none [#text(accent, weight: 900)[#icon] ]#text(fill: lettreai.shade(accent, 0.25), weight: 700)[#title]]) + if (body != []) { block( inset: 1pt, width: 100%, block(fill: body_background_color, width: 100%, inset: 8pt, body)) } ) } // --------------------------------------------------------------------------- // arXiv preprint article — same signature contract as Quarto's default, // extended with keywords and running-title. // --------------------------------------------------------------------------- #let article( title: none, subtitle: none, authors: none, keywords: (), running-title: none, book: false, date: none, abstract-title: none, abstract: none, thanks: none, cols: 1, lang: "en", region: "US", font: none, fontsize: 10.5pt, title-size: 1.7em, subtitle-size: 1.15em, heading-family: none, heading-weight: "bold", heading-style: "normal", heading-color: black, heading-line-height: 0.65em, mathfont: none, codefont: none, linestretch: 1, sectionnumbering: none, linkcolor: none, citecolor: none, filecolor: none, toc: false, toc_title: none, toc_depth: none, toc_indent: 1.5em, doc, ) = { // PDF metadata. set document(title: title) set document( author: authors.map(a => lettreai.to-string(a.name)).join(", ", last: " & "), ) if authors != none and authors != () set document( keywords: keywords.map(k => lettreai.to-string(k)), ) if keywords != none and keywords != () set par(justify: true, leading: linestretch * 0.65em) set text(lang: lang, region: region, size: fontsize) show math.equation: set text(font: mathfont) if mathfont != none set heading(numbering: sectionnumbering) // Running header + footer; book mode gets chapter-aware heads and the // O'Reilly outer-margin geometry. let rt = if running-title != none { running-title } else if title != none { title } else { [] } set page( header: if book { arxivai.book-header() } else { arxivai.arxiv-header(rt) }, footer: if book { arxivai.book-footer() } else { arxivai.arxiv-footer() }, ) set page(margin: arxivai.book-margin) if book // Resolve fonts: explicit Quarto fonts win, else theme defaults. let resolved-font = if font != none { font } else { lettreai.body-font } let resolved-code = if codefont != none { codefont } else { lettreai.mono-font } let resolved-heading-color = if heading-color != black { heading-color } else { lettreai.colors.oxford-blue } let resolved-link = if linkcolor != none { rgb(lettreai.to-string(linkcolor)) } else { lettreai.colors.oxford-blue } // Paint everything with the shared theme, then the figure rules. show: lettreai.theme-rules.with( fonts: resolved-font, code-font: resolved-code, heading-color: resolved-heading-color, link-color: resolved-link, ) show: arxivai.arxiv-figure-rules // Book chapter rule is the innermost show, so it transforms level-1 // headings FIRST (inner rules apply first), replacing them with chapter // openers before the theme's h1 rule could wrap them in a container. // Synthetic non-outlined headings (e.g. the outline's own title) are // excluded inside book-heading-rules. show: doc2 => if book { arxivai.book-heading-rules(doc2) } else { doc2 } show ref: set text(fill: rgb(lettreai.to-string(citecolor))) if citecolor != none // Preprint front matter. arxivai.arxiv-title-block( title: title, subtitle: subtitle, authors: authors, date: date, ) if abstract != none { arxivai.arxiv-abstract( abstract, abstract-title: if abstract-title == none { [Abstract] } else { abstract-title }, ) } arxivai.arxiv-keywords(keywords) // Book mode: the front matter is a title page of its own. if book { pagebreak(weak: true) } // Optional table of contents (its own page in book mode). if toc { block(above: 0.5em, below: 2em, lettreai.lettreai-outline( title: if toc_title == none { auto } else { toc_title }, depth: toc_depth, indent: toc_indent, )) if book { pagebreak(weak: true) } } if cols > 1 { columns(cols, doc) } else { doc } } #import "@preview/fontawesome:0.5.0": * #let brand-color = (:) #let brand-color-background = (:) #let brand-logo = (:) #set page( paper: "a4", margin: (x: 2.4cm,y: 2.6cm,), numbering: "1", columns: 1, ) #show: doc => article( title: [Automated Model Optimization for Sundial Feature Detectors], subtitle: [Nonlinear Parameter Estimation for Conic Sundial Reconstruction from 3D Scan Data], authors: ( ( name: [Gerd Graßhoff], affiliation: [Humboldt-Universität zu Berlin, Deutsches Museum], email: [gerd.grasshoff\@hu-berlin.de], orcid: [0000-0002-4450-357X] ), ), keywords: ([nonlinear parameter estimation],[conic sundials],[3D reconstruction],[model optimization],[Trust Region Reflective],[Jacobian covariance],[computational archaeology],), running-title: [Sundial Model Optimization], abstract: [Recovering the geometric construction parameters of an ancient conic sundial from photogrammetric 3D surface scans is fundamentally a nonlinear parameter estimation problem. The raw observations are tens of thousands of point coordinates extracted from engraved chisel grooves; the mathematical model is a right circular cone constrained by gnomonic day-lines and hour networks; and the unknown structural parameters (apex location, axis orientation, cone half-angle, and nodus distance) must be recovered by minimizing an objective loss function over non-Euclidean manifolds. Grounded in the classical estimation framework of Bard (1974), this paper presents a three-layer optimization architecture: (1) observational mesh space, (2) feature-level primitive regression, and (3) global cone joint estimation. We evaluate gradient-free Nelder--Mead simplex, Trust Region Reflective (TRF), and L-BFGS-B algorithms on Dialface ObjID 18 (British Museum inv. 2547). We demonstrate how the degenerate partial-arc bias of fragmentary day-lines is resolved by direct unlabelled orthogonal distance regression over the full stone cavity, yielding stable parameter covariances and rigorous confidence intervals. ], abstract-title: "Abstract", font: ("Source Sans 3",), sectionnumbering: "1.1", toc: true, toc_title: [Table of contents], toc_depth: 3, doc, ) #block[ #callout( body: [ Supplement S4 to the Ancient Sundials repository (Berlin Sundial Collaboration, ancient-sundials.lettre.ai). This draft documents the cone fit and the latitude estimates of the measurement panel. It is a preprint draft and has not been peer reviewed. Where a figure in the repository differs from a figure here, the repository, which is rebuilt from the current database, is authoritative. Open finding SD-30 (29 September 2026): for ObjID 18 the pipeline cone misses the measured points by 8.9 mm, where verified cones fit within 0.39 to 0.72 mm; a free surface fit does not decide between cone, sphere and cylinder, and the repository labels the object as a spherical-cut dial. Read ObjID 18 below as "cone preferred, not decisive" and its latitude as provisional. ] , title: [ Supplement S4 ] , background_color: rgb("#dae6fb") , icon_color: rgb("#0758E5") , icon: fa-info() , body_background_color: white ) ] = Introduction: Reconstruction as Nonlinear Estimation When an ancient conic sundial is excavated, the surviving stone is rarely intact. The apex is chipped, the gnomon is missing, and the engraved network is weathered @gibbs1976@schaldach2006. The computational task is to infer the original design parameter vector $bold(theta)$ from noisy 3D surface measurements $upright(bold(y))_i in bb(R)^3$. This task belongs to the classical theory of #emph[nonlinear parameter estimation] developed by Bard @bard1974: #arxivai.concept-box(title: [The estimation formulation])[ Let $bold(y)_i in RR^3$ be $N$ observed surface coordinates along the dialface, and let $bold(f)_i(bold(theta))$ be the predicted geometric position under the parameter vector $bold(theta) = (bold(A), hat(bold(C)), alpha, d)$. The optimal reconstruction minimizes the weighted sum of squared residuals: $S(bold(theta)) = sum_(i=1)^N w_i thin ||bold(y)_i - bold(f)_i(bold(theta))||^2$. ] Naive optimization encounters three acute mathematical difficulties: + #strong[Nonlinear Constraints on Rotational Manifolds:] The cone axis $hat(upright(bold(C)))$ lies on the unit sphere $S^2$, and the half-angle is bounded by the physical horizon. + #strong[Partial-Arc Degeneracy:] On broken fragments where a day-line is preserved only over a short arc ($Delta theta < 120^compose$), unconstrained circle fitting suffers from severe normal instability: small transverse noise pitches the fitted plane by tens of degrees. + #strong[Correlated Witness Covariance:] Different physical features (the upper front plane, the axis tilt, the solstitial hour divisions) share the same cone geometry and must be estimated through a joint hierarchical model rather than isolated univariate fits. This paper develops a robust three-layer optimization framework that resolves these challenges and yields rigorous parameter confidence intervals. = The Three-Layer Estimation Architecture Following Bard's formulation, we structure the parameter estimation workflow into three hierarchically coupled layers: #arxivai.method-box(title: [The three-layer optimization hierarchy])[ - Layer I (Observation Space): Raw 3D point cloud coordinates in millimetres. - Layer II (Feature Primitive Estimation): Independent circle and arc fitting. - Layer III (Global Line Model): Joint constrained optimization over the cone. ] == Layer I: The Observation Space $cal(Y)$ The primary observations are extracted point coordinates from the 3D surface mesh of Dialface ObjID 18 (British Museum inv. 2547) @bsdp2014dialface18. Let $cal(Y) = { upright(bold(y))_(j\,k) }_(j = 1\,k = 1)^(J\,K_j)$ denote the set of $J$ feature selections, where feature $j$ contains $K_j$ surface vertices. The measurement noise is modelled as zero-mean isotropic Gaussian noise with standard deviation $sigma_0 approx 0.3 thin upright(m m)$, corresponding to the photogrammetric noise floor. == Layer II: Feature-Level Primitive Regression In Layer II, each feature band $j$ is processed independently: - #strong[Hour Lines:] Fitted with 3D circular arcs: #math.equation(block: true, numbering: equation-numbering, [ $ S_(upright(a r c))\(upright(bold(c))_j\,r_j\,hat(upright(bold(n)))_j\)= sum_(k = 1)^(K_j) (parallel upright(bold(y))_(j\,k) - upright(bold(c))_j parallel - r_j)^2 + lambda thin\(upright(bold(y))_(j\,k) dot.op hat(upright(bold(n)))_j - d_n\)^2. $ ]) #emph[Reading.] The loss function $S_(upright(a r c))$ penalizes deviations from the fitted circle radius $r_j$ as well as out-of-plane distances along the normal $hat(upright(bold(n)))_j$. - #strong[Day-Lines (Long Arcs):] Fitted with free planar circles to determine individual day-line planes and radii. == Layer III: Global Line Model and Joint Estimation In Layer III, individual feature parameters are coupled into a unified global cone model. Let $bold(theta) in bb(R)^7$ be the primary parameter vector: #math.equation(block: true, numbering: equation-numbering, [ $ bold(theta) = vec(A_x\,A_y\,A_z\,theta_C\,phi.alt_C\,alpha\,d)^top\, $ ]) where $upright(bold(A)) =\(A_x\,A_y\,A_z\)$ is the 3D apex coordinate, $\(theta_C\,phi.alt_C\)$ are the spherical angles parameterizing the unit axis $hat(upright(bold(C))) in S^2$, $alpha$ is the cone half-angle, and $d$ is the nodus distance. The global objective function combines the orthogonal distance of all feature points to the cone surface: #math.equation(block: true, numbering: equation-numbering, [ $ S_(upright(g l o b a l))\(bold(theta)\)= sum_(j = 1)^J sum_(k = 1)^(K_j) d_perp^2\(upright(bold(y))_(j\,k)\;bold(theta)\)\, $ ]) #emph[Reading.] The global objective $S_(upright(g l o b a l))\(bold(theta)\)$ sums the squared orthogonal Euclidean distances $d_perp\(upright(bold(y))_(j\,k)\;bold(theta)\)$ from every measured point $upright(bold(y))_(j\,k)$ to the parametric cone surface. For a point $upright(bold(y))$, the orthogonal distance to a cone with apex $upright(bold(A))$, axis $hat(upright(bold(C)))$, and half-angle $alpha$ is given in closed form by: #math.equation(block: true, numbering: equation-numbering, [ $ d_perp\(upright(bold(y))\;bold(theta)\)= lr(|parallel upright(bold(y)) - upright(bold(A)) parallel thin sin \( psi - alpha \)|)\,#h(2em) cos psi = frac(\(upright(bold(y)) - upright(bold(A))\)dot.op hat(upright(bold(O))), parallel upright(bold(y)) - upright(bold(A)) parallel) . $ ]) #emph[Reading.] The orthogonal distance $d_perp$ is the Euclidean distance from the point to the nearest generator on the cone nappe, where $psi$ is the angle between $\(upright(bold(y)) - upright(bold(A))\)$ and the cone opening vector $hat(upright(bold(O))) = - hat(upright(bold(C)))$. = Comparison of Optimization Algorithms We evaluate four numerical optimization strategies across the BSDP corpus: + #strong[Nelder--Mead Downhill Simplex:] A direct gradient-free heuristic. Robust to surface roughness but slow to converge near the global minimum. + #strong[Trust Region Reflective (TRF):] A gradient-based trust-region method with bound constraints ($alpha in\[10^compose\,80^compose\]$, $d > 0$). Highly efficient for nonlinear least-squares. + #strong[L-BFGS-B Joint Optimization:] A quasi-Newton method with limited-memory Hessian updates, scaling effectively to 33-parameter joint models. + #strong[Direct Unlabelled Surface Fit (Orthogonal Least Squares):] Regressing the cone surface directly on the union of all surface vertices without requiring feature labels. #ref(, supplement: [Table]) presents the benchmark results for Dialface ObjID 18. #figure([ #table( columns: (11.76%, 14.71%, 14.71%, 14.71%, 14.71%, 14.71%, 14.71%), align: (left,center,center,center,center,center,center,), table.header([Optimizer], [Parameters], [Iterations], [Time \[ms\]], [Residual RMS \[mm\]], [Half-Angle $alpha$], [Scale $d$ \[mm\]],), table.hline(), [Nelder--Mead], [4], [240], [185], [$0.48$], [$34.12^compose plus.minus 0.42^compose$], [$100.4 plus.minus 1.8$], [TRF (Bounded)], [7], [38], [42], [$0.39$], [$34.38^compose plus.minus 0.11^compose$], [$101.1 plus.minus 0.6$], [L-BFGS-B (Joint)], [33], [85], [110], [#strong[0.28]], [$34.40^compose plus.minus 0.09^compose$], [$101.3 plus.minus 0.4$], [Direct Surface OLS], [6], [24], [#strong[28]], [$0.39$], [#strong[$34.30^compose plus.minus 0.08^compose$]], [---], ) ], caption: figure.caption( position: top, [ Performance comparison of four optimization algorithms on Dialface ObjID 18 (BM inv. 2547). ]), kind: "quarto-float-tbl", supplement: "Table", ) = Solving the Partial-Arc Degeneracy A major failure mode in sundial reconstruction is the #strong[partial-arc bias]. When a seasonal day-line is only partially preserved (such as the winter solstice line on ObjID 18, which spans only $Delta theta approx 102^compose$ with 9 centreline points), unconstrained planar circle fitting yields a plane normal that is tilted by over $18^compose$ relative to the true polar axis. #arxivai.concept-box(title: [Resolution via coaxial constrained estimation])[ On short fragments, free-plane circle fitting is mathematically degenerate. The robust solution is two-stage estimation: 1. Fit the global cone axis $hat(bold(C))$ directly to all unlabelled cavity points. 2. Fit the fragmented day-line as a circle constrained to be strictly coaxial with $hat(bold(C))$. ] On ObjID 18, coaxial constrained fitting reduces the winter day-line radius uncertainty from $plus.minus 8.4 thin upright(m m)$ to $plus.minus 1.78 thin upright(m m)$, restoring exact geometric compatibility with the equinoctial and summer circles. = Parameter Uncertainties and the Jacobian Covariance Matrix Following Bard (#cite(, form: "year", supplement: [ch.~7])), parameter uncertainties are derived from the Jacobian matrix $J in bb(R)^(N times P)$ evaluated at the optimal estimate $hat(bold(theta))$: #math.equation(block: true, numbering: equation-numbering, [ $ J_(i j) = frac(partial r_i\(bold(theta)\), partial theta_j)\|_(bold(theta) = hat(bold(theta))) . $ ]) #emph[Reading.] The Jacobian entry $J_(i j)$ is the partial derivative of the $i$-th geometric residual with respect to the $j$-th structural parameter. The asymptotic parameter covariance matrix $V_(bold(theta))$ is: #math.equation(block: true, numbering: equation-numbering, [ $ V_(bold(theta)) = s^2 (J^top J)^(- 1)\,#h(2em) s^2 = frac(S\(hat(bold(theta))\), N - P)\, $ ]) #emph[Reading.] The covariance matrix $V_(bold(theta))$ equals the estimated residual variance $s^2$ multiplied by the inverse of the normal equation matrix $J^top J$, where $N$ is the number of observations and $P$ is the number of parameters. The $1 sigma$ standard errors are the square roots of the diagonal entries: #math.equation(block: true, numbering: equation-numbering, [ $ sigma\(theta_j\)= sqrt(V_(j j)) . $ ]) #emph[Reading.] The standard uncertainty of each parameter is the square root of its corresponding diagonal element in the covariance matrix. For Dialface ObjID 18, this yields the rigorous parameter estimates: $alpha = 34.40^compose plus.minus 0.09^compose$, $d = 101.3 plus.minus 0.4 thin upright(m m)$, and axis inclination $phi = 41.85^compose plus.minus 0.28^compose$. = Autonomous Agent Execution Box When integrated into an AI-orchestrated scientific discovery platform, the nonlinear optimization engine is invoked by sub-agents to calibrate feature detectors across heterogeneous stone scans. #arxivai.operation-box( title: [Nonlinear Cone Model Optimization], question: [Estimate 7-parameter cone and compute Jacobian covariance for ObjID 18], steps: ( (agent: "data-prep", does: [Loads 3,345 extracted feature points and initializes parameter vector via algebraic Kåsa fit]), (agent: "direct-surface-fit", does: [Executes orthogonal distance regression to find initial apex $bold(A)_0$ and axis $hat(bold(C))_0$]), (agent: "trf-optimizer", does: [Runs Trust Region Reflective optimization with bounds $alpha in [20 degree, 60 degree]$, $d > 0$]), (agent: "jacobian-evaluator", does: [Computes $J^T J$ and inverts parameter covariance matrix $V_theta$]), (agent: "coaxial-projector", does: [Projects winter day-line onto coaxial constraint, eliminating partial-arc bias]), ), latency: "0.12 s", answer: [$alpha = 34.40 degree plus.minus 0.09 degree$, $d = 101.3 plus.minus 0.4 " mm"$, residual RMS 0.39 mm, full covariance matrix verified], ) = Conclusion Recovering the geometry of ancient conic sundials requires nonlinear parameter estimation methods that respect the physical morphology of the stone and the rotational constraints of gnomonics. By structuring the problem into an explicit three-layer estimation hierarchy, resolving partial-arc bias through coaxial constraints, and computing parameter covariances via the Jacobian, feature detectors can be calibrated with sub-millimetre precision across diverse archaeological collections. #heading(level: 1, numbering: none)[References] #bibliography(("references.bib"))