Skip to main content

causal_triangulations/
errors.rs

1#![forbid(unsafe_code)]
2
3//! Error types for the CDT library.
4
5use crate::cdt::ergodic_moves::MoveType;
6use crate::cdt::foliation::FoliationError;
7use crate::config::CdtTopology;
8use markov_chain_monte_carlo::{McmcError, StepOutcome};
9use std::fmt;
10
11/// Highest cumulative upstream Delaunay validation level being enforced.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13#[non_exhaustive]
14pub enum DelaunayValidationLevel {
15    /// Validate Level 1 only.
16    One,
17    /// Validate Levels 1 through 2.
18    Two,
19    /// Validate Levels 1 through 3.
20    Three,
21    /// Validate Levels 1 through 4.
22    Four,
23}
24
25impl fmt::Display for DelaunayValidationLevel {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::One => formatter.write_str("Level 1"),
29            Self::Two => formatter.write_str("Level 1-2"),
30            Self::Three => formatter.write_str("Level 1-3"),
31            Self::Four => formatter.write_str("Level 1-4"),
32        }
33    }
34}
35
36/// Identifies the top-level or simulation configuration setting that failed validation.
37///
38/// Use this with [`CdtError::InvalidConfiguration`] and
39/// [`CdtError::InvalidSimulationConfiguration`] to inspect invalid settings
40/// without parsing rendered error messages.
41///
42/// # Examples
43///
44/// ```
45/// use causal_triangulations::prelude::errors::{CdtError, ConfigurationSetting};
46/// use causal_triangulations::prelude::CdtConfig;
47/// use std::assert_matches;
48///
49/// let config = CdtConfig {
50///     vertices: 2,
51///     ..CdtConfig::new(36, 3)
52/// };
53/// assert_matches!(
54///     config.into_validated(),
55///     Err(CdtError::InvalidConfiguration {
56///         setting: ConfigurationSetting::Vertices,
57///         ..
58///     })
59/// );
60/// ```
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62#[non_exhaustive]
63pub enum ConfigurationSetting {
64    /// CDT dimensionality setting.
65    Dimension,
66    /// Total vertex count setting.
67    Vertices,
68    /// Number of time slices setting.
69    Timeslices,
70    /// Metropolis temperature setting.
71    Temperature,
72    /// Number of Metropolis steps setting.
73    Steps,
74    /// Number of thermalization steps setting.
75    ThermalizationSteps,
76    /// Measurement cadence setting.
77    MeasurementFrequency,
78    /// Combined measurement schedule constraint.
79    MeasurementSchedule,
80    /// Bare inverse Newton coupling setting.
81    Coupling0,
82    /// Curvature coupling setting.
83    Coupling2,
84    /// Cosmological constant setting.
85    CosmologicalConstant,
86    /// Explicit per-slice volume profile setting.
87    VolumeProfile,
88}
89
90impl fmt::Display for ConfigurationSetting {
91    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::Dimension => formatter.write_str("dimension"),
94            Self::Vertices => formatter.write_str("vertices"),
95            Self::Timeslices => formatter.write_str("timeslices"),
96            Self::Temperature => formatter.write_str("temperature"),
97            Self::Steps => formatter.write_str("steps"),
98            Self::ThermalizationSteps => formatter.write_str("thermalization_steps"),
99            Self::MeasurementFrequency => formatter.write_str("measurement_frequency"),
100            Self::MeasurementSchedule => formatter.write_str("measurement schedule"),
101            Self::Coupling0 => formatter.write_str("coupling_0"),
102            Self::Coupling2 => formatter.write_str("coupling_2"),
103            Self::CosmologicalConstant => formatter.write_str("cosmological_constant"),
104            Self::VolumeProfile => formatter.write_str("volume_profile"),
105        }
106    }
107}
108
109/// Identifies the issue category for invalid triangulation generation parameters.
110///
111/// Use this with [`CdtError::InvalidGenerationParameters`] when a constructor
112/// rejects input before attempting triangulation.
113///
114/// # Examples
115///
116/// ```
117/// use causal_triangulations::prelude::errors::{CdtError, GenerationParameterIssue};
118/// use causal_triangulations::prelude::triangulation::CdtTriangulation;
119/// use std::assert_matches;
120///
121/// let err = CdtTriangulation::from_random_points(2, 2, 2)
122///     .expect_err("fewer than three vertices cannot form a triangulation");
123///
124/// assert_matches!(
125///     err,
126///     CdtError::InvalidGenerationParameters {
127///         issue: GenerationParameterIssue::InsufficientVertexCount,
128///         ..
129///     }
130/// );
131/// ```
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133#[non_exhaustive]
134pub enum GenerationParameterIssue {
135    /// Coordinate range has invalid ordering or bounds.
136    InvalidCoordinateRange,
137    /// Toroidal domain extents are invalid.
138    InvalidToroidalDomain,
139    /// A supplied vertex coordinate is NaN or infinite.
140    NonFiniteVertexCoordinate,
141    /// Total vertex count is below the constructor minimum.
142    InsufficientVertexCount,
143    /// Per-slice vertex count is below the constructor minimum.
144    InsufficientVerticesPerSlice,
145    /// Number of time slices is below the topology minimum.
146    InsufficientNumberOfTimeSlices,
147    /// A slice count was zero where at least one slice is required.
148    NonPositiveSliceCount,
149    /// Explicit volume profile has no slices.
150    EmptyVolumeProfile,
151    /// Explicit volume profile length cannot fit in supported counters.
152    VolumeProfileLengthOverflow,
153    /// A volume-profile slice has too few vertices.
154    InsufficientVerticesInVolumeProfileSlice,
155    /// Total vertex count cannot fit in supported counters.
156    VertexCountOverflow,
157    /// Simplex count cannot fit in supported counters.
158    SimplexCountOverflow,
159}
160
161impl fmt::Display for GenerationParameterIssue {
162    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::InvalidCoordinateRange => formatter.write_str("Invalid coordinate range"),
165            Self::InvalidToroidalDomain => formatter.write_str("Invalid toroidal domain"),
166            Self::NonFiniteVertexCoordinate => formatter.write_str("Non-finite vertex coordinate"),
167            Self::InsufficientVertexCount => formatter.write_str("Insufficient vertex count"),
168            Self::InsufficientVerticesPerSlice => {
169                formatter.write_str("Insufficient vertices per slice")
170            }
171            Self::InsufficientNumberOfTimeSlices => {
172                formatter.write_str("Insufficient number of time slices")
173            }
174            Self::NonPositiveSliceCount => formatter.write_str("Number of slices must be positive"),
175            Self::EmptyVolumeProfile => formatter.write_str("Empty volume profile"),
176            Self::VolumeProfileLengthOverflow => {
177                formatter.write_str("Volume profile length overflow")
178            }
179            Self::InsufficientVerticesInVolumeProfileSlice => {
180                formatter.write_str("Insufficient vertices in volume-profile slice")
181            }
182            Self::VertexCountOverflow => formatter.write_str("Vertex count overflow"),
183            Self::SimplexCountOverflow => formatter.write_str("Simplex count overflow"),
184        }
185    }
186}
187
188/// Identifies the CDT triangulation metadata field that failed validation.
189///
190/// Use this with [`CdtError::InvalidTriangulationMetadata`] to distinguish
191/// invalid metadata fields without relying on display text.
192///
193/// # Examples
194///
195/// ```
196/// use causal_triangulations::prelude::errors::{CdtError, TriangulationMetadataField};
197/// use causal_triangulations::prelude::triangulation::CdtTopology;
198/// use std::assert_matches;
199///
200/// let metadata_error = CdtError::InvalidTriangulationMetadata {
201///     field: TriangulationMetadataField::Timeslices,
202///     topology: CdtTopology::Toroidal,
203///     provided_value: "2".to_string(),
204///     expected: "at least three time slices".to_string(),
205/// };
206///
207/// assert_matches!(
208///     metadata_error,
209///     CdtError::InvalidTriangulationMetadata {
210///         field: TriangulationMetadataField::Timeslices,
211///         ..
212///     }
213/// );
214/// ```
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216#[non_exhaustive]
217pub enum TriangulationMetadataField {
218    /// Number of time slices recorded in triangulation metadata.
219    Timeslices,
220    /// Dimensionality recorded in triangulation metadata.
221    Dimension,
222}
223
224impl fmt::Display for TriangulationMetadataField {
225    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            Self::Timeslices => formatter.write_str("timeslices"),
228            Self::Dimension => formatter.write_str("dimension"),
229        }
230    }
231}
232
233/// Simulation output format for typed output read/write failures.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235#[non_exhaustive]
236pub enum OutputFormat {
237    /// Comma-separated trace output.
238    Csv,
239    /// JSON simulation summary output.
240    Json,
241}
242
243impl fmt::Display for OutputFormat {
244    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
245        match self {
246            Self::Csv => formatter.write_str("CSV"),
247            Self::Json => formatter.write_str("JSON"),
248        }
249    }
250}
251
252/// Checkpoint serialization operation that failed.
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
254#[non_exhaustive]
255pub enum CheckpointOperation {
256    /// Serializing a checkpoint or checkpoint-like payload failed.
257    Serialize,
258    /// Deserializing a checkpoint or checkpoint-like payload failed.
259    Deserialize,
260}
261
262impl fmt::Display for CheckpointOperation {
263    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
264        match self {
265            Self::Serialize => formatter.write_str("serialize"),
266            Self::Deserialize => formatter.write_str("deserialize"),
267        }
268    }
269}
270
271/// Backend mutation operation that failed while editing a CDT triangulation.
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
273#[non_exhaustive]
274pub enum BackendMutationOperation {
275    /// Write simplex payload by backend simplex key.
276    SetSimplexDataByKey,
277    /// Write vertex payload by backend vertex key.
278    SetVertexDataByKey,
279    /// Write vertex payload through a vertex handle.
280    SetVertexData,
281    /// Subdivide a face as part of a local move.
282    SubdivideFace,
283    /// Remove a vertex as part of a local move.
284    RemoveVertex,
285    /// Flip an edge as part of a local move.
286    FlipEdge,
287}
288
289impl fmt::Display for BackendMutationOperation {
290    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            Self::SetSimplexDataByKey => formatter.write_str("set_simplex_data_by_key"),
293            Self::SetVertexDataByKey => formatter.write_str("set_vertex_data_by_key"),
294            Self::SetVertexData => formatter.write_str("set_vertex_data"),
295            Self::SubdivideFace => formatter.write_str("subdivide_face"),
296            Self::RemoveVertex => formatter.write_str("remove_vertex"),
297            Self::FlipEdge => formatter.write_str("flip_edge"),
298        }
299    }
300}
301
302/// CDT validation check that failed.
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
304#[non_exhaustive]
305pub enum CdtValidationCheck {
306    /// Generic backend geometry validation.
307    Geometry,
308    /// Foliation assignment from coordinates failed.
309    FoliationAssignment,
310    /// Causality validation failed.
311    Causality,
312    /// Strict CDT simplex classification failed.
313    SimplexClassification,
314    /// Local ergodic move candidate geometry could not be interpreted.
315    ErgodicMoveCandidateGeometry,
316}
317
318impl fmt::Display for CdtValidationCheck {
319    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
320        match self {
321            Self::Geometry => formatter.write_str("geometry"),
322            Self::FoliationAssignment => formatter.write_str("foliation_assignment"),
323            Self::Causality => formatter.write_str("causality"),
324            Self::SimplexClassification => formatter.write_str("simplex_classification"),
325            Self::ErgodicMoveCandidateGeometry => {
326                formatter.write_str("ergodic_move_candidate_geometry")
327            }
328        }
329    }
330}
331
332/// Structured detail for crate-owned CDT validation failures.
333///
334/// This refines [`CdtError::ValidationFailed`] beyond a coarse
335/// [`CdtValidationCheck`] so callers can inspect common CDT invariant failures
336/// without parsing display text. Variants still keep string diagnostics where
337/// the source is an upstream backend message or an opaque backend handle.
338///
339/// # Examples
340///
341/// ```
342/// use causal_triangulations::prelude::errors::CdtValidationFailure;
343///
344/// let failure = CdtValidationFailure::InvalidCdtTriangle {
345///     face: "FaceKey(3v1)".to_string(),
346///     spacelike_edges: 3,
347///     timelike_edges: 0,
348/// };
349///
350/// assert!(format!("{failure}").contains("spacelike=3"));
351/// ```
352#[derive(Debug, Clone, PartialEq, Eq, Hash)]
353#[non_exhaustive]
354pub enum CdtValidationFailure {
355    /// Generic backend geometry validation failed with an upstream diagnostic.
356    BackendGeometry {
357        /// Upstream geometry validation diagnostic.
358        detail: String,
359    },
360    /// Face vertices could not be resolved through the geometry backend.
361    FaceVerticesUnavailable {
362        /// Face being validated.
363        face: String,
364        /// Lower-level face-vertex lookup diagnostic.
365        detail: String,
366    },
367    /// A face had the wrong number of vertices for a CDT triangle.
368    FaceVertexCount {
369        /// Face being validated.
370        face: String,
371        /// Number of vertices observed.
372        actual: usize,
373        /// Number of vertices expected.
374        expected: usize,
375    },
376    /// A vertex in a foliated triangulation was missing its time label.
377    MissingVertexTimeLabel {
378        /// Vertex missing its time label.
379        vertex: String,
380    },
381    /// A triangle had the wrong spacelike/timelike edge pattern.
382    InvalidCdtTriangle {
383        /// Face being validated.
384        face: String,
385        /// Number of spacelike edges observed.
386        spacelike_edges: u8,
387        /// Number of timelike edges observed.
388        timelike_edges: u8,
389    },
390    /// Coordinate lookup failed while assigning foliation labels.
391    VertexCoordinateReadFailed {
392        /// Vertex whose coordinates could not be read.
393        vertex: String,
394        /// Lower-level coordinate lookup diagnostic.
395        detail: String,
396    },
397    /// A vertex coordinate did not have enough dimensions for foliation assignment.
398    VertexCoordinateDimension {
399        /// Vertex whose coordinate dimensionality was invalid.
400        vertex: String,
401        /// Number of coordinates observed.
402        actual: usize,
403        /// Minimum number of coordinates expected.
404        expected_minimum: usize,
405    },
406    /// A foliated face was not classifiable as a strict Up or Down CDT simplex.
407    NonStrictSimplex {
408        /// Face being classified.
409        face: String,
410    },
411    /// Local ergodic-move candidate geometry failed a post-mutation invariant.
412    ErgodicMoveCandidateGeometry {
413        /// Diagnostic for the failed local candidate.
414        detail: String,
415    },
416}
417
418impl fmt::Display for CdtValidationFailure {
419    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
420        match self {
421            Self::BackendGeometry { detail } | Self::ErgodicMoveCandidateGeometry { detail } => {
422                formatter.write_str(detail)
423            }
424            Self::FaceVerticesUnavailable { face, detail } => {
425                write!(
426                    formatter,
427                    "failed to resolve vertices for face {face}: {detail}"
428                )
429            }
430            Self::FaceVertexCount {
431                face,
432                actual,
433                expected,
434            } => write!(
435                formatter,
436                "face {face} has {actual} vertices, expected {expected}"
437            ),
438            Self::MissingVertexTimeLabel { vertex } => write!(
439                formatter,
440                "vertex {vertex} has no time label in a foliated triangulation"
441            ),
442            Self::InvalidCdtTriangle {
443                face,
444                spacelike_edges,
445                timelike_edges,
446            } => write!(
447                formatter,
448                "invalid CDT triangle at face {face}: spacelike={spacelike_edges}, timelike={timelike_edges}"
449            ),
450            Self::VertexCoordinateReadFailed { vertex, detail } => {
451                write!(
452                    formatter,
453                    "failed to read coordinates for vertex {vertex}: {detail}"
454                )
455            }
456            Self::VertexCoordinateDimension {
457                vertex,
458                actual,
459                expected_minimum,
460            } => write!(
461                formatter,
462                "vertex {vertex} has {actual} coordinates, expected ≥ {expected_minimum}"
463            ),
464            Self::NonStrictSimplex { face } => write!(
465                formatter,
466                "face {face} is not a strict CDT simplex (expected Up or Down)"
467            ),
468        }
469    }
470}
471
472/// Move-statistics counter category used in checkpoint resume diagnostics.
473///
474/// Use this with [`CheckpointResumeFailure::MoveCounterOverflow`] and
475/// [`CheckpointResumeFailure::CounterConversionOverflow`] to distinguish which
476/// aggregated counter could not be represented without parsing display text.
477///
478/// # Examples
479///
480/// ```
481/// use causal_triangulations::prelude::errors::CheckpointMoveCounter;
482///
483/// assert_eq!(CheckpointMoveCounter::Attempted.to_string(), "attempted");
484/// ```
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
486#[non_exhaustive]
487pub enum CheckpointMoveCounter {
488    /// Attempted move counter.
489    Attempted,
490    /// Accepted move counter.
491    Accepted,
492    /// Rejected move counter.
493    Rejected,
494}
495
496impl fmt::Display for CheckpointMoveCounter {
497    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
498        match self {
499            Self::Attempted => formatter.write_str("attempted"),
500            Self::Accepted => formatter.write_str("accepted"),
501            Self::Rejected => formatter.write_str("rejected"),
502        }
503    }
504}
505
506/// Proposal-statistics counter category used in result telemetry diagnostics.
507///
508/// Use this with [`CheckpointResumeFailure::ProposalCounterOverflow`] to
509/// distinguish which proposal telemetry counter could not be represented
510/// without parsing display text.
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
512#[non_exhaustive]
513pub enum ProposalTelemetryCounter {
514    /// Selected move-family proposal counter.
515    MoveFamilyProposals,
516    /// Accepted proposal transition counter.
517    AcceptedTransitions,
518    /// Rejected proposal transition counter.
519    RejectedTransitions,
520}
521
522impl fmt::Display for ProposalTelemetryCounter {
523    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
524        match self {
525            Self::MoveFamilyProposals => formatter.write_str("move-family proposals"),
526            Self::AcceptedTransitions => formatter.write_str("accepted transitions"),
527            Self::RejectedTransitions => formatter.write_str("rejected transitions"),
528        }
529    }
530}
531
532/// Scalar trace field category used in checkpoint/result diagnostics.
533///
534/// # Examples
535///
536/// ```
537/// use causal_triangulations::prelude::errors::ScalarTraceField;
538///
539/// assert_eq!(ScalarTraceField::ActionBefore.to_string(), "action_before");
540/// ```
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
542#[non_exhaustive]
543pub enum ScalarTraceField {
544    /// Cached target log probability.
545    LogProb,
546    /// Current action observable.
547    Action,
548    /// Proposed or accepted action delta.
549    DeltaAction,
550    /// Action before the proposed move.
551    ActionBefore,
552    /// Action after an accepted move.
553    ActionAfter,
554}
555
556impl fmt::Display for ScalarTraceField {
557    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
558        match self {
559            Self::LogProb => formatter.write_str("log_prob"),
560            Self::Action => formatter.write_str("action"),
561            Self::DeltaAction => formatter.write_str("delta_action"),
562            Self::ActionBefore => formatter.write_str("action_before"),
563            Self::ActionAfter => formatter.write_str("action_after"),
564        }
565    }
566}
567
568/// Measurement count column with a strictly positive invariant.
569///
570/// # Examples
571///
572/// ```
573/// use causal_triangulations::prelude::errors::MeasurementCountField;
574///
575/// assert_eq!(MeasurementCountField::Vertices.to_string(), "vertices");
576/// ```
577#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
578#[non_exhaustive]
579pub enum MeasurementCountField {
580    /// Vertex-count column.
581    Vertices,
582    /// Edge-count column.
583    Edges,
584    /// Triangle-count column.
585    Triangles,
586}
587
588impl fmt::Display for MeasurementCountField {
589    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
590        match self {
591            Self::Vertices => formatter.write_str("vertices"),
592            Self::Edges => formatter.write_str("edges"),
593            Self::Triangles => formatter.write_str("triangles"),
594        }
595    }
596}
597
598/// CDT simplex-count field with a strictly positive triangulation-state invariant.
599///
600/// Use this with [`CdtError::InvalidSimplexCount`] when converting raw backend
601/// counts into an invariant-bearing CDT count snapshot.
602///
603/// # Examples
604///
605/// ```
606/// use causal_triangulations::prelude::errors::SimplexCountField;
607///
608/// assert_eq!(SimplexCountField::Triangles.to_string(), "triangles");
609/// ```
610#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
611#[non_exhaustive]
612pub enum SimplexCountField {
613    /// Vertex-count field.
614    Vertices,
615    /// Edge-count field.
616    Edges,
617    /// Triangle-count field.
618    Triangles,
619}
620
621impl fmt::Display for SimplexCountField {
622    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
623        match self {
624            Self::Vertices => formatter.write_str("vertices"),
625            Self::Edges => formatter.write_str("edges"),
626            Self::Triangles => formatter.write_str("triangles"),
627        }
628    }
629}
630
631/// Structured reason a CDT checkpoint could not be resumed.
632///
633/// [`CdtError::CheckpointResumeFailed`] wraps this enum for CDT-owned resume
634/// invariants such as incompatible schedules, inconsistent telemetry, and
635/// overflow while rebuilding upstream MCMC counters. Upstream framework,
636/// configuration, and triangulation validation failures remain separate
637/// [`CdtError`] variants so callers can keep matching the original typed error.
638///
639/// # Examples
640///
641/// ```
642/// use causal_triangulations::prelude::errors::{
643///     CdtError, CheckpointResumeFailure,
644/// };
645/// use std::assert_matches;
646///
647/// let err = CdtError::CheckpointResumeFailed {
648///     failure: CheckpointResumeFailure::IncompatibleTemperature,
649/// };
650///
651/// assert_matches!(
652///     err,
653///     CdtError::CheckpointResumeFailed {
654///         failure: CheckpointResumeFailure::IncompatibleTemperature,
655///     }
656/// );
657/// ```
658#[derive(Debug, Clone, PartialEq, thiserror::Error)]
659#[non_exhaustive]
660pub enum CheckpointResumeFailure {
661    /// Resumed step count would overflow.
662    #[error("resumed step count exceeds u32::MAX")]
663    StepCountOverflow,
664    /// Stored action disagrees with recomputed action.
665    #[error("checkpoint action mismatch: stored {stored}, recomputed {recomputed}")]
666    ActionMismatch {
667        /// Serialized action stored in the checkpoint.
668        stored: f64,
669        /// Action recomputed from the restored triangulation.
670        recomputed: f64,
671    },
672    /// Stored checkpoint action is NaN or infinite.
673    #[error("checkpoint action is non-finite: stored {stored}")]
674    NonFiniteCheckpointAction {
675        /// Serialized action stored in the checkpoint.
676        stored: f64,
677    },
678    /// Action configuration differs from the checkpoint.
679    #[error("action configuration differs from checkpoint")]
680    IncompatibleActionConfiguration,
681    /// Temperature differs from the checkpoint.
682    #[error("temperature differs from checkpoint")]
683    IncompatibleTemperature,
684    /// Thermalization schedule differs from the checkpoint.
685    #[error("thermalization schedule differs from checkpoint")]
686    IncompatibleThermalizationSchedule,
687    /// Measurement frequency differs from the checkpoint.
688    #[error("measurement frequency differs from checkpoint")]
689    IncompatibleMeasurementFrequency,
690    /// Generic MCMC chain counters disagree with CDT move statistics.
691    #[error(
692        "chain counters do not match move statistics: chain accepted={chain_accepted}, rejected={chain_rejected}; move accepted={move_accepted}, rejected={move_rejected}"
693    )]
694    ChainCounterMismatch {
695        /// Accepted proposals recorded by the upstream MCMC chain.
696        chain_accepted: usize,
697        /// Rejected proposals recorded by the upstream MCMC chain.
698        chain_rejected: usize,
699        /// Accepted proposals reconstructed from CDT move statistics.
700        move_accepted: usize,
701        /// Rejected proposals reconstructed from CDT move statistics.
702        move_rejected: usize,
703    },
704    /// Generic MCMC chain step count disagrees with checkpoint step.
705    #[error(
706        "chain step count does not match checkpoint step: chain steps={chain_steps}, checkpoint step={checkpoint_step}"
707    )]
708    ChainStepMismatch {
709        /// Total steps recorded by the upstream MCMC chain.
710        chain_steps: usize,
711        /// Current CDT step stored in the checkpoint.
712        checkpoint_step: u32,
713    },
714    /// Step telemetry length disagrees with the chain step count.
715    #[error("step telemetry length mismatch: got {actual}, expected {expected}")]
716    StepTelemetryLengthMismatch {
717        /// Number of serialized step records.
718        actual: usize,
719        /// Expected number of step records from the chain counters.
720        expected: usize,
721    },
722    /// Accepted-step telemetry count disagrees with the chain accepted count.
723    #[error("accepted step count mismatch: got {actual}, expected {expected}")]
724    StepTelemetryAcceptedCountMismatch {
725        /// Accepted steps recorded in CDT telemetry.
726        actual: usize,
727        /// Accepted proposals recorded by the upstream MCMC chain.
728        expected: usize,
729    },
730    /// Step telemetry index conversion overflowed.
731    #[error("step telemetry index exceeds u32::MAX")]
732    StepTelemetryIndexOverflow,
733    /// Step telemetry records are not sequential.
734    #[error("step telemetry must be sequential: got step {actual}, expected {expected}")]
735    StepTelemetrySequenceMismatch {
736        /// Serialized step value.
737        actual: u32,
738        /// Expected sequential step value.
739        expected: u32,
740    },
741    /// Step telemetry contains a non-finite pre-move action.
742    #[error("step {step} has non-finite action_before")]
743    NonFiniteStepActionBefore {
744        /// Step with invalid telemetry.
745        step: u32,
746    },
747    /// Step telemetry contains a non-finite action delta.
748    #[error("step {step} has non-finite delta_action")]
749    NonFiniteStepDeltaAction {
750        /// Step with invalid telemetry.
751        step: u32,
752    },
753    /// Accepted step telemetry has an action-after value inconsistent with the delta.
754    #[error("step {step} action_after does not match delta_action")]
755    StepActionAfterDeltaMismatch {
756        /// Step with invalid telemetry.
757        step: u32,
758    },
759    /// Accepted step telemetry contains a non-finite post-move action.
760    #[error("step {step} has non-finite action_after")]
761    NonFiniteStepActionAfter {
762        /// Step with invalid telemetry.
763        step: u32,
764    },
765    /// Scalar trace row count disagrees with step telemetry.
766    #[error("scalar trace row count mismatch: got {actual}, expected {expected}")]
767    ScalarTraceLengthMismatch {
768        /// Number of scalar trace rows.
769        actual: usize,
770        /// Expected scalar trace rows from step telemetry.
771        expected: usize,
772    },
773    /// Scalar trace row step disagrees with step telemetry.
774    #[error("scalar trace step mismatch: got {actual}, expected {expected}")]
775    ScalarTraceStepMismatch {
776        /// Serialized scalar trace step.
777        actual: u32,
778        /// Expected step from step telemetry.
779        expected: u32,
780    },
781    /// Scalar trace row step was zero before it could be aligned with step telemetry.
782    #[error("scalar trace step must be nonzero: got {actual}")]
783    ScalarTraceStepZero {
784        /// Serialized scalar trace step.
785        actual: u32,
786    },
787    /// Scalar trace move family disagrees with step telemetry.
788    #[error("step {step} scalar trace move type mismatch: got {actual:?}, expected {expected:?}")]
789    ScalarTraceMoveTypeMismatch {
790        /// Step with invalid scalar trace telemetry.
791        step: u32,
792        /// Move type stored in the scalar trace row.
793        actual: MoveType,
794        /// Move type stored in step telemetry.
795        expected: MoveType,
796    },
797    /// Scalar trace accepted outcome disagrees with step telemetry.
798    #[error("step {step} scalar trace accepted mismatch: got {actual}, expected {expected}")]
799    ScalarTraceAcceptedMismatch {
800        /// Step with invalid scalar trace telemetry.
801        step: u32,
802        /// Accepted flag reconstructed from scalar trace outcome.
803        actual: bool,
804        /// Accepted flag stored in step telemetry.
805        expected: bool,
806    },
807    /// Scalar trace optional action delta disagrees with step telemetry.
808    #[error("step {step} scalar trace delta_action does not match step telemetry")]
809    ScalarTraceDeltaActionMismatch {
810        /// Step with invalid scalar trace telemetry.
811        step: u32,
812    },
813    /// Scalar trace action-before value disagrees with step telemetry.
814    #[error("step {step} scalar trace action_before does not match step telemetry")]
815    ScalarTraceActionBeforeMismatch {
816        /// Step with invalid scalar trace telemetry.
817        step: u32,
818    },
819    /// Scalar trace current action value disagrees with step telemetry.
820    #[error("step {step} scalar trace action does not match step telemetry")]
821    ScalarTraceActionMismatch {
822        /// Step with invalid scalar trace telemetry.
823        step: u32,
824    },
825    /// Scalar trace optional action-after value disagrees with step telemetry.
826    #[error("step {step} scalar trace action_after does not match step telemetry")]
827    ScalarTraceActionAfterMismatch {
828        /// Step with invalid scalar trace telemetry.
829        step: u32,
830    },
831    /// Scalar trace log probability disagrees with the stored action and temperature.
832    #[error("step {step} scalar trace log_prob does not match action and temperature")]
833    ScalarTraceLogProbMismatch {
834        /// Step with invalid scalar trace telemetry.
835        step: u32,
836    },
837    /// Scalar trace RNG seed disagrees with the simulation configuration.
838    #[error("step {step} scalar trace seed mismatch: got {actual:?}, expected {expected:?}")]
839    ScalarTraceSeedMismatch {
840        /// Step with invalid scalar trace telemetry.
841        step: u32,
842        /// Seed stored in the scalar trace row.
843        actual: Option<u64>,
844        /// Seed from simulation configuration.
845        expected: Option<u64>,
846    },
847    /// Scalar trace volume profile exceeds the stored triangle count.
848    #[error(
849        "step {step} scalar trace volume profile total {profile_total} exceeds triangle count {triangles}"
850    )]
851    ScalarTraceVolumeProfileExceedsTriangles {
852        /// Step with invalid scalar trace telemetry.
853        step: u32,
854        /// Sum of the serialized volume profile entries.
855        profile_total: u64,
856        /// Stored scalar trace triangle count.
857        triangles: u32,
858    },
859    /// Scalar trace contains a non-finite numeric value.
860    #[error("step {step} scalar trace field {field} is non-finite")]
861    NonFiniteScalarTraceValue {
862        /// Step with invalid scalar trace telemetry.
863        step: u32,
864        /// Scalar trace field containing the invalid value.
865        field: ScalarTraceField,
866    },
867    /// Scalar trace accepted outcome count disagrees with proposal telemetry.
868    #[error("scalar trace accepted count mismatch: got {actual}, expected {expected}")]
869    ScalarTraceAcceptedCountMismatch {
870        /// Accepted outcomes recorded in scalar trace rows.
871        actual: u64,
872        /// Accepted outcomes recorded in proposal telemetry.
873        expected: u64,
874    },
875    /// Scalar trace rejected-proposal count disagrees with proposal telemetry.
876    #[error("scalar trace rejected-proposal count mismatch: got {actual}, expected {expected}")]
877    ScalarTraceRejectedProposalCountMismatch {
878        /// Rejected-proposal outcomes recorded in scalar trace rows.
879        actual: u64,
880        /// Metropolis rejection outcomes recorded in proposal telemetry.
881        expected: u64,
882    },
883    /// Scalar trace no-proposal count disagrees with proposal telemetry.
884    #[error("scalar trace no-proposal count mismatch: got {actual}, expected {expected}")]
885    ScalarTraceNoProposalCountMismatch {
886        /// No-proposal outcomes recorded in scalar trace rows.
887        actual: u64,
888        /// No-proposal outcomes recorded in proposal telemetry.
889        expected: u64,
890    },
891    /// Measurement count calculation overflowed.
892    #[error("scheduled measurement count exceeds usize::MAX")]
893    MeasurementCountOverflow,
894    /// Measurement telemetry length disagrees with the configured schedule.
895    #[error("scheduled measurement count mismatch: got {actual}, expected {expected}")]
896    MeasurementCountMismatch {
897        /// Number of serialized measurements.
898        actual: usize,
899        /// Expected measurement count from the sampling schedule.
900        expected: usize,
901    },
902    /// Measurement step calculation overflowed.
903    #[error("scheduled measurement step exceeds u32::MAX")]
904    MeasurementStepOverflow,
905    /// Measurement telemetry step disagrees with the configured schedule.
906    #[error("measurement telemetry step mismatch: got {actual}, expected {expected}")]
907    MeasurementStepMismatch {
908        /// Serialized measurement step.
909        actual: u32,
910        /// Expected measurement step from the sampling schedule.
911        expected: u32,
912    },
913    /// A per-move counter sum overflowed.
914    #[error("{counter} move count exceeds u64::MAX")]
915    MoveCounterOverflow {
916        /// Counter category that overflowed.
917        counter: CheckpointMoveCounter,
918    },
919    /// Resumable checkpoints cannot contain hard-failure move counters.
920    #[error("{move_type:?} hard-failure move count must be zero in resumable checkpoints")]
921    MoveHardFailures {
922        /// Move type with invalid hard-failure telemetry.
923        move_type: MoveType,
924    },
925    /// Accepted move counter exceeds attempted move counter.
926    #[error("{move_type:?} accepted move count exceeds attempted move count")]
927    MoveAcceptedExceedsAttempted {
928        /// Move type with impossible move telemetry.
929        move_type: MoveType,
930    },
931    /// Total accepted move count exceeds total attempted move count.
932    #[error("accepted move count exceeds attempted move count")]
933    TotalAcceptedExceedsAttempted,
934    /// Accepted or rejected counter conversion overflowed.
935    #[error("{counter} move count exceeds usize::MAX")]
936    CounterConversionOverflow {
937        /// Counter category that could not fit in upstream chain counters.
938        counter: CheckpointMoveCounter,
939    },
940    /// A proposal-statistics counter conversion overflowed.
941    #[error("{counter} proposal telemetry count exceeds u64::MAX")]
942    ProposalCounterOverflow {
943        /// Proposal telemetry counter that could not fit in the target type.
944        counter: ProposalTelemetryCounter,
945    },
946    /// Resume-validated proposal telemetry cannot contain hard failures.
947    #[error("proposal telemetry has {actual} hard failures; expected 0")]
948    ProposalHardFailures {
949        /// Number of hard proposal failures recorded in proposal telemetry.
950        actual: u64,
951    },
952    /// Proposal move-family count disagrees with the number of sampler steps.
953    #[error("proposal move-family count mismatch: got {actual}, expected {expected}")]
954    ProposalMoveFamilyCountMismatch {
955        /// Number of move-family proposals recorded in proposal telemetry.
956        actual: u64,
957        /// Expected proposal count from step telemetry.
958        expected: u64,
959    },
960    /// Proposal accepted-transition count disagrees with step telemetry.
961    #[error("proposal accepted-transition count mismatch: got {actual}, expected {expected}")]
962    ProposalAcceptedCountMismatch {
963        /// Number of accepted transitions recorded in proposal telemetry.
964        actual: u64,
965        /// Expected accepted count from step telemetry.
966        expected: u64,
967    },
968    /// Proposal rejected-transition count disagrees with step telemetry.
969    #[error("proposal rejected-transition count mismatch: got {actual}, expected {expected}")]
970    ProposalRejectedCountMismatch {
971        /// Number of rejected transitions reconstructed from proposal telemetry.
972        actual: u64,
973        /// Expected rejected count from step telemetry.
974        expected: u64,
975    },
976}
977
978/// Lower-level source for a Metropolis-accepted move that could not be applied.
979///
980/// [`CdtError::MetropolisMoveApplicationFailed`] uses this enum to preserve the
981/// category and structured context of a hard failure after Metropolis has
982/// accepted a move type. It is intentionally smaller than recursively storing a
983/// full [`CdtError`] while still giving callers typed branches for backend,
984/// validation, topology, foliation, and causality failures.
985///
986/// # Examples
987///
988/// ```
989/// use causal_triangulations::prelude::errors::{
990///     BackendMutationOperation, MetropolisMoveApplicationFailure,
991/// };
992///
993/// let failure = MetropolisMoveApplicationFailure::BackendMutation {
994///     operation: BackendMutationOperation::RemoveVertex,
995///     target: "vertex VertexKey(7v1)".to_string(),
996///     detail: "backend reported invalid vertex key".to_string(),
997/// };
998///
999/// assert!(format!("{failure}").contains("remove_vertex"));
1000/// ```
1001#[derive(Debug, Clone, PartialEq, thiserror::Error)]
1002#[non_exhaustive]
1003pub enum MetropolisMoveApplicationFailure {
1004    /// A backend payload or topology edit failed while applying the accepted move.
1005    #[error("backend mutation failed [{operation}] on {target}: {detail}")]
1006    BackendMutation {
1007        /// Mutation operation being attempted.
1008        operation: BackendMutationOperation,
1009        /// Human-readable target handle.
1010        target: String,
1011        /// Additional failure detail.
1012        detail: String,
1013    },
1014    /// A backend mutation failed, then rollback of staged payloads also failed.
1015    #[error(
1016        "backend mutation failed [{operation}] on {target}: {detail}; rollback failed: {rollback_errors}"
1017    )]
1018    BackendRollback {
1019        /// Mutation operation being attempted when the first failure occurred.
1020        operation: BackendMutationOperation,
1021        /// Human-readable target handle for the first failure.
1022        target: String,
1023        /// Primary mutation failure detail.
1024        detail: String,
1025        /// Rollback failure details for one or more payloads.
1026        rollback_errors: String,
1027    },
1028    /// Upstream Delaunay validation rejected the evolved geometry.
1029    #[error("Delaunay validation failed [{level}]: {detail}")]
1030    DelaunayValidation {
1031        /// Cumulative upstream validation level being enforced.
1032        level: DelaunayValidationLevel,
1033        /// Upstream validation diagnostic.
1034        detail: String,
1035    },
1036    /// CDT validation rejected the evolved triangulation.
1037    #[error("validation failed [{check}]: {failure}")]
1038    Validation {
1039        /// Validation check that failed.
1040        check: CdtValidationCheck,
1041        /// Structured validation failure detail.
1042        failure: CdtValidationFailure,
1043    },
1044    /// Topology metadata did not match the evolved backend Euler characteristic.
1045    #[error(
1046        "topology mismatch for {topology}: Euler characteristic χ={euler_characteristic}, expected one of {expected_euler_characteristics:?} (V={vertices}, E={edges}, F={faces})"
1047    )]
1048    TopologyMismatch {
1049        /// Topology requested by CDT metadata.
1050        topology: CdtTopology,
1051        /// Observed Euler characteristic from the backend.
1052        euler_characteristic: i128,
1053        /// Accepted Euler characteristics for the requested topology.
1054        expected_euler_characteristics: Vec<i128>,
1055        /// Backend vertex count at validation time.
1056        vertices: usize,
1057        /// Backend edge count at validation time.
1058        edges: usize,
1059        /// Backend face count at validation time.
1060        faces: usize,
1061    },
1062    /// Foliation bookkeeping or validation failed.
1063    #[error("foliation validation failed: {0}")]
1064    Foliation(FoliationError),
1065    /// A post-mutation edge violated CDT causality.
1066    #[error("{}", format_causality_violation(*time_0, *time_1, *step_distance))]
1067    CausalityViolation {
1068        /// Time label of the first endpoint.
1069        time_0: u32,
1070        /// Time label of the second endpoint.
1071        time_1: u32,
1072        /// Topology-aware temporal step distance between the two labels.
1073        step_distance: u32,
1074    },
1075    /// A hard failure reached the Metropolis boundary through an unexpected error category.
1076    #[error("unexpected accepted-move failure: {detail}")]
1077    Unexpected {
1078        /// Lower-level error text retained for diagnostics.
1079        detail: String,
1080    },
1081}
1082
1083impl From<CdtError> for MetropolisMoveApplicationFailure {
1084    fn from(error: CdtError) -> Self {
1085        match error {
1086            CdtError::BackendMutationFailed {
1087                operation,
1088                target,
1089                detail,
1090            } => Self::BackendMutation {
1091                operation,
1092                target,
1093                detail,
1094            },
1095            CdtError::BackendRollbackFailed {
1096                operation,
1097                target,
1098                detail,
1099                rollback_errors,
1100            } => Self::BackendRollback {
1101                operation,
1102                target,
1103                detail,
1104                rollback_errors,
1105            },
1106            CdtError::DelaunayValidationFailed { level, detail } => {
1107                Self::DelaunayValidation { level, detail }
1108            }
1109            CdtError::ValidationFailed { check, failure } => Self::Validation { check, failure },
1110            CdtError::TopologyMismatch {
1111                topology,
1112                euler_characteristic,
1113                expected_euler_characteristics,
1114                vertices,
1115                edges,
1116                faces,
1117            } => Self::TopologyMismatch {
1118                topology,
1119                euler_characteristic,
1120                expected_euler_characteristics,
1121                vertices,
1122                edges,
1123                faces,
1124            },
1125            CdtError::Foliation(error) => Self::Foliation(error),
1126            CdtError::CausalityViolation {
1127                time_0,
1128                time_1,
1129                step_distance,
1130            } => Self::CausalityViolation {
1131                time_0,
1132                time_1,
1133                step_distance,
1134            },
1135            CdtError::MetropolisMoveApplicationFailed { source, .. }
1136            | CdtError::ProposalApplicationFailed { source, .. } => source,
1137            unexpected @ (CdtError::UnsupportedDimension(_)
1138            | CdtError::DelaunayGenerationFailed { .. }
1139            | CdtError::InvalidGenerationParameters { .. }
1140            | CdtError::InvalidConfiguration { .. }
1141            | CdtError::InvalidSimplexCount { .. }
1142            | CdtError::InvalidMeasurementAction { .. }
1143            | CdtError::InvalidMeasurementCount { .. }
1144            | CdtError::InvalidMeasurementVolumeProfile { .. }
1145            | CdtError::InvalidScalarTraceCount { .. }
1146            | CdtError::MeasurementCountOverflow { .. }
1147            | CdtError::InvalidSimulationConfiguration { .. }
1148            | CdtError::PlannedProposalStepFailed { .. }
1149            | CdtError::UnexpectedPlannedStepOutcome { .. }
1150            | CdtError::PlannedProposalTelemetryMissing { .. }
1151            | CdtError::InvalidTriangulationMetadata { .. }
1152            | CdtError::VertexBuildFailed { .. }
1153            | CdtError::Mcmc(_)
1154            | CdtError::OutputWriteFailed { .. }
1155            | CdtError::OutputPathResolutionFailed { .. }
1156            | CdtError::OutputPathConflict { .. }
1157            | CdtError::OutputReadFailed { .. }
1158            | CdtError::CheckpointSerializationFailed { .. }
1159            | CdtError::CheckpointResumeFailed { .. }) => Self::Unexpected {
1160                detail: unexpected.to_string(),
1161            },
1162        }
1163    }
1164}
1165
1166/// Main error type for CDT operations.
1167#[derive(Debug, Clone, PartialEq, thiserror::Error)]
1168#[non_exhaustive]
1169pub enum CdtError {
1170    /// Invalid dimension specified
1171    #[error("Unsupported dimension: {0}. Only 2D is currently supported")]
1172    UnsupportedDimension(u32),
1173    /// Delaunay triangulation generation failed with detailed context
1174    #[error(
1175        "Delaunay triangulation generation failed: {vertex_count} vertices, range [{}, {}], attempt {attempt}: {underlying_error}",
1176        coordinate_range.0,
1177        coordinate_range.1
1178    )]
1179    DelaunayGenerationFailed {
1180        /// Number of vertices requested for the triangulation
1181        vertex_count: u32,
1182        /// Coordinate range used for generation
1183        coordinate_range: (f64, f64),
1184        /// Attempt number when the failure occurred
1185        attempt: u32,
1186        /// Description of the underlying error that caused the failure
1187        underlying_error: String,
1188    },
1189    /// Upstream Delaunay validation rejected a geometry backend.
1190    #[error("Delaunay validation failed [{level}]: {detail}")]
1191    DelaunayValidationFailed {
1192        /// Cumulative upstream validation level being enforced.
1193        level: DelaunayValidationLevel,
1194        /// Upstream validation diagnostic.
1195        detail: String,
1196    },
1197    /// Invalid generation parameters detected before attempting triangulation
1198    #[error(
1199        "Invalid triangulation parameters: {issue} (got: {provided_value}, expected: {expected_range})"
1200    )]
1201    InvalidGenerationParameters {
1202        /// Structured category for the rejected generation parameter.
1203        issue: GenerationParameterIssue,
1204        /// The actual value that was provided
1205        provided_value: String,
1206        /// The expected range or constraint for the parameter
1207        expected_range: String,
1208    },
1209    /// Top-level CDT configuration failed validation.
1210    #[error("Invalid configuration: {setting} (got: {provided_value}, expected: {expected})")]
1211    InvalidConfiguration {
1212        /// Structured category for the invalid configuration setting.
1213        setting: ConfigurationSetting,
1214        /// Value supplied for the setting.
1215        provided_value: String,
1216        /// Expected constraint for the setting.
1217        expected: String,
1218    },
1219    /// Metropolis / simulation configuration failed validation.
1220    #[error(
1221        "Invalid simulation configuration: {setting} (got: {provided_value}, expected: {expected})"
1222    )]
1223    InvalidSimulationConfiguration {
1224        /// Structured category for the invalid simulation setting.
1225        setting: ConfigurationSetting,
1226        /// Value supplied for the setting.
1227        provided_value: String,
1228        /// Expected constraint for the setting.
1229        expected: String,
1230    },
1231    /// Live CDT simplex counts failed the strictly-positive triangulation-state invariant.
1232    #[error(
1233        "Invalid CDT simplex count: {field} (got: {provided_value}, expected: strictly positive)"
1234    )]
1235    InvalidSimplexCount {
1236        /// Structured category for the invalid simplex count.
1237        field: SimplexCountField,
1238        /// Value observed for the count.
1239        provided_value: usize,
1240    },
1241    /// [`Measurement`](crate::cdt::results::Measurement) construction failed
1242    /// because a count was not strictly positive.
1243    #[error(
1244        "Invalid measurement count: {field} (got: {provided_value}, expected: strictly positive)"
1245    )]
1246    InvalidMeasurementCount {
1247        /// Structured category for the invalid measurement count.
1248        field: MeasurementCountField,
1249        /// Value supplied for the count.
1250        provided_value: u32,
1251    },
1252    /// [`Measurement`](crate::cdt::results::Measurement) construction failed
1253    /// because a per-slice volume profile could not fit the stored triangle count.
1254    #[error(
1255        "Invalid measurement volume profile at step {step}: total {profile_total} exceeds triangle count {triangles}"
1256    )]
1257    InvalidMeasurementVolumeProfile {
1258        /// Measurement step with invalid volume-profile telemetry.
1259        step: u32,
1260        /// Sum of the supplied volume profile entries.
1261        profile_total: u64,
1262        /// Stored measurement triangle count.
1263        triangles: u32,
1264    },
1265    /// Scalar trace row construction failed because a count was not strictly positive.
1266    #[error(
1267        "Invalid scalar trace count: {field} (got: {provided_value}, expected: strictly positive)"
1268    )]
1269    InvalidScalarTraceCount {
1270        /// Structured category for the invalid scalar trace count.
1271        field: MeasurementCountField,
1272        /// Value supplied for the count.
1273        provided_value: u32,
1274    },
1275    /// [`Measurement`](crate::cdt::results::Measurement) construction failed
1276    /// because a live triangulation count could not fit the serialized count type.
1277    #[error("Measurement count overflow: {field} (got: {provided_value}, max: {max})")]
1278    MeasurementCountOverflow {
1279        /// Structured category for the overflowing measurement count.
1280        field: MeasurementCountField,
1281        /// Value supplied for the count.
1282        provided_value: usize,
1283        /// Maximum representable count for serialized telemetry.
1284        max: u32,
1285    },
1286    /// [`Measurement`](crate::cdt::results::Measurement) construction failed
1287    /// because the action value was not finite.
1288    #[error("Invalid measurement action at step {step}: got {provided_value}, expected finite")]
1289    InvalidMeasurementAction {
1290        /// Monte Carlo step associated with the measurement.
1291        step: u32,
1292        /// Value supplied for the action.
1293        provided_value: f64,
1294    },
1295    /// Metropolis accepted a move, but a hard backend or invariant failure stopped application.
1296    ///
1297    /// The [`Self::MetropolisMoveApplicationFailed::source`] field keeps the
1298    /// lower-level failure category as [`MetropolisMoveApplicationFailure`] so
1299    /// callers can distinguish backend mutation, validation, topology,
1300    /// foliation, and causality failures without parsing the rendered message.
1301    #[error(
1302        "Metropolis accepted {move_type:?} at step {step}, but applying it failed after {attempts} attempts; source: {source}"
1303    )]
1304    MetropolisMoveApplicationFailed {
1305        /// Monte Carlo step whose accepted move could not be applied.
1306        step: u32,
1307        /// Accepted move type being applied.
1308        move_type: MoveType,
1309        /// Number of application attempts made before failing.
1310        attempts: usize,
1311        /// Most specific lower-level rejection or failure observed.
1312        source: MetropolisMoveApplicationFailure,
1313    },
1314    /// Planning or committing a standalone planned CDT proposal hit a hard failure.
1315    #[error("CDT proposal failed while applying {move_type:?} on attempt {attempt}: {source}")]
1316    ProposalApplicationFailed {
1317        /// Move type whose concrete proposal application failed.
1318        move_type: MoveType,
1319        /// Local-site attempt that hit the hard failure.
1320        attempt: usize,
1321        /// Most specific lower-level rejection or failure observed.
1322        source: MetropolisMoveApplicationFailure,
1323    },
1324    /// A planned CDT proposal step completed without required proposal telemetry.
1325    #[error("planned CDT proposal step {step} completed without required proposal telemetry")]
1326    PlannedProposalTelemetryMissing {
1327        /// Monte Carlo step that was missing metadata or accepted-step action evidence.
1328        step: u32,
1329    },
1330    /// A planned CDT proposal step failed in a way CDT cannot classify yet.
1331    #[error("planned CDT proposal step {step} failed: {detail}")]
1332    PlannedProposalStepFailed {
1333        /// Monte Carlo step whose planned-proposal execution failed.
1334        step: u32,
1335        /// Upstream sampler diagnostic.
1336        detail: String,
1337    },
1338    /// A planned CDT proposal step returned an upstream outcome CDT does not support yet.
1339    #[error("planned CDT proposal step {step} returned unsupported upstream outcome {outcome:?}")]
1340    UnexpectedPlannedStepOutcome {
1341        /// Monte Carlo step whose planned-proposal execution produced the outcome.
1342        step: u32,
1343        /// Upstream outcome variant that CDT does not support yet.
1344        outcome: StepOutcome,
1345    },
1346    /// Constructed triangulation metadata is internally inconsistent.
1347    #[error(
1348        "Invalid triangulation metadata: {field} for {topology} (got: {provided_value}, expected: {expected})"
1349    )]
1350    InvalidTriangulationMetadata {
1351        /// Structured category for the invalid metadata field.
1352        field: TriangulationMetadataField,
1353        /// Topology whose invariant was violated.
1354        topology: CdtTopology,
1355        /// Value stored in the triangulation metadata.
1356        provided_value: String,
1357        /// Expected constraint for the metadata field.
1358        expected: String,
1359    },
1360    /// Validation of a constructed triangulation failed.
1361    ///
1362    /// The [`Self::ValidationFailed::check`] field identifies the broad
1363    /// validation phase, while [`Self::ValidationFailed::failure`] carries the
1364    /// typed invariant failure within that phase.
1365    #[error("Validation failed [{check}]: {failure}")]
1366    ValidationFailed {
1367        /// Validation check that failed.
1368        check: CdtValidationCheck,
1369        /// Structured validation failure detail.
1370        failure: CdtValidationFailure,
1371    },
1372    /// Topology metadata does not match the backend Euler characteristic.
1373    #[error(
1374        "Topology mismatch for {topology}: Euler characteristic χ={euler_characteristic}, expected one of {expected_euler_characteristics:?} (V={vertices}, E={edges}, F={faces})"
1375    )]
1376    TopologyMismatch {
1377        /// Topology requested by CDT metadata.
1378        topology: CdtTopology,
1379        /// Observed Euler characteristic from the backend.
1380        euler_characteristic: i128,
1381        /// Accepted Euler characteristics for the requested topology.
1382        expected_euler_characteristics: Vec<i128>,
1383        /// Backend vertex count at validation time.
1384        vertices: usize,
1385        /// Backend edge count at validation time.
1386        edges: usize,
1387        /// Backend face count at validation time.
1388        faces: usize,
1389    },
1390    /// Foliation construction or validation failed with a typed foliation error.
1391    #[error("Foliation validation failed: {0}")]
1392    Foliation(#[from] FoliationError),
1393    /// Vertex construction failed during triangulation generation
1394    #[error("Vertex construction failed [{context}]: {underlying_error}")]
1395    VertexBuildFailed {
1396        /// Human-readable context (e.g., function name or vertex index)
1397        context: String,
1398        /// The underlying builder error message
1399        underlying_error: String,
1400    },
1401    /// Backend payload mutation failed due to an invalid or unavailable handle.
1402    #[error("Backend mutation failed [{operation}] on {target}: {detail}")]
1403    BackendMutationFailed {
1404        /// Mutation operation being attempted.
1405        operation: BackendMutationOperation,
1406        /// Human-readable target handle (e.g., "vertex `VertexKey`(..)").
1407        target: String,
1408        /// Additional failure detail.
1409        detail: String,
1410    },
1411    /// Backend mutation failed and restoring previously staged payloads also failed.
1412    #[error(
1413        "Backend mutation failed [{operation}] on {target}: {detail}; rollback failed: {rollback_errors}"
1414    )]
1415    BackendRollbackFailed {
1416        /// Mutation operation being attempted when the first failure occurred.
1417        operation: BackendMutationOperation,
1418        /// Human-readable target handle for the first failure.
1419        target: String,
1420        /// Primary mutation failure detail.
1421        detail: String,
1422        /// Rollback failure details for one or more payloads.
1423        rollback_errors: String,
1424    },
1425    /// An edge violates the causal structure by spanning more than one time slice
1426    /// (or, on toroidal topology, more than one *circular* slice step).
1427    #[error("{}", format_causality_violation(*time_0, *time_1, *step_distance))]
1428    CausalityViolation {
1429        /// Time label of the first endpoint.
1430        time_0: u32,
1431        /// Time label of the second endpoint.
1432        time_1: u32,
1433        /// Topology-aware temporal step distance between the two labels.
1434        ///
1435        /// On `OpenBoundary` topology this equals `time_0.abs_diff(time_1)`.
1436        /// On `Toroidal` topology it is the circular distance
1437        /// `min(d, T − d)`, so the wrap-around edge between slice `T − 1`
1438        /// and slice `0` reads as `1` rather than `T − 1`.  This is the
1439        /// quantity that triggers the violation (`step_distance > 1`).
1440        step_distance: u32,
1441    },
1442    /// Upstream MCMC framework error, such as a non-finite log-probability.
1443    #[error("MCMC error: {0}")]
1444    Mcmc(#[from] McmcError),
1445    /// Writing CSV/JSON simulation output failed.
1446    #[error("Failed to write {format} output to {path}: {detail}")]
1447    OutputWriteFailed {
1448        /// Target output path.
1449        path: String,
1450        /// Output format being written.
1451        format: OutputFormat,
1452        /// Lower-level I/O or serialization error.
1453        detail: String,
1454    },
1455    /// Resolving a configured output path failed before writing began.
1456    #[error("Failed to resolve output path from base {base_path}: {detail}")]
1457    OutputPathResolutionFailed {
1458        /// Base path used for resolving configured output paths.
1459        base_path: String,
1460        /// Lower-level path resolution error.
1461        detail: String,
1462    },
1463    /// Configured CSV and JSON output paths resolve to the same file.
1464    #[error("CSV output path {csv_path} and JSON output path {json_path} resolve to the same file")]
1465    OutputPathConflict {
1466        /// Resolved CSV output path.
1467        csv_path: String,
1468        /// Resolved JSON output path.
1469        json_path: String,
1470    },
1471    /// Reading or decoding CSV/JSON simulation output failed.
1472    #[error("Failed to read {format} output from {path}: {detail}")]
1473    OutputReadFailed {
1474        /// Source output path.
1475        path: String,
1476        /// Output format being read.
1477        format: OutputFormat,
1478        /// Lower-level I/O or decoding error.
1479        detail: String,
1480    },
1481    /// Serializing or deserializing a CDT or MCMC checkpoint failed.
1482    #[error("Failed to {operation} {target} checkpoint: {detail}")]
1483    CheckpointSerializationFailed {
1484        /// Checkpoint operation being attempted.
1485        operation: CheckpointOperation,
1486        /// Human-readable checkpoint target, such as "final triangulation".
1487        target: String,
1488        /// Lower-level serialization error.
1489        detail: String,
1490    },
1491    /// Restoring or continuing an MCMC checkpoint failed before sampling resumed.
1492    ///
1493    /// The [`CheckpointResumeFailure`] source is reserved for CDT-owned
1494    /// resume invariants. Upstream MCMC, configuration, and triangulation
1495    /// validation errors are reported through their more specific variants.
1496    #[error("Failed to resume MCMC checkpoint: {failure}")]
1497    CheckpointResumeFailed {
1498        /// Structured resume failure with typed context.
1499        #[source]
1500        failure: CheckpointResumeFailure,
1501    },
1502}
1503
1504/// Keeps causality error formatting centralized so open and toroidal distances stay consistent.
1505fn format_causality_violation(time_0: u32, time_1: u32, step_distance: u32) -> String {
1506    let raw = time_0.abs_diff(time_1);
1507    if raw == step_distance {
1508        format!(
1509            "Causality violation: edge spans {step_distance} time-slice steps \
1510             (t={time_0} to t={time_1}), maximum allowed is 1"
1511        )
1512    } else {
1513        // Toroidal: the displayed step distance is the circular distance,
1514        // smaller than the raw label difference.
1515        format!(
1516            "Causality violation: edge spans {step_distance} time-slice steps \
1517             (t={time_0} to t={time_1}, |Δt|={raw} on the time circle), \
1518             maximum allowed is 1"
1519        )
1520    }
1521}
1522
1523/// Result type for CDT operations.
1524pub type CdtResult<T> = Result<T, CdtError>;
1525
1526#[cfg(test)]
1527mod tests {
1528    use super::*;
1529    use std::assert_matches;
1530    use std::error::Error;
1531
1532    #[test]
1533    fn test_invalid_configuration_error() {
1534        let error = CdtError::InvalidConfiguration {
1535            setting: ConfigurationSetting::Vertices,
1536            provided_value: "2".to_string(),
1537            expected: "≥ 3".to_string(),
1538        };
1539        let display = format!("{error}");
1540        assert_eq!(
1541            display,
1542            "Invalid configuration: vertices (got: 2, expected: ≥ 3)"
1543        );
1544    }
1545
1546    #[test]
1547    fn test_invalid_simulation_configuration_error() {
1548        let error = CdtError::InvalidSimulationConfiguration {
1549            setting: ConfigurationSetting::Temperature,
1550            provided_value: "NaN".to_string(),
1551            expected: "finite and positive".to_string(),
1552        };
1553        let display = format!("{error}");
1554        assert_eq!(
1555            display,
1556            "Invalid simulation configuration: temperature (got: NaN, expected: finite and positive)"
1557        );
1558    }
1559
1560    #[test]
1561    fn test_invalid_triangulation_metadata_error() {
1562        let error = CdtError::InvalidTriangulationMetadata {
1563            field: TriangulationMetadataField::Timeslices,
1564            topology: CdtTopology::Toroidal,
1565            provided_value: "2".to_string(),
1566            expected: "≥ 3".to_string(),
1567        };
1568        let display = format!("{error}");
1569        assert_eq!(
1570            display,
1571            "Invalid triangulation metadata: timeslices for toroidal (got: 2, expected: ≥ 3)"
1572        );
1573    }
1574
1575    #[test]
1576    fn test_delaunay_generation_failed_error() {
1577        let error = CdtError::DelaunayGenerationFailed {
1578            vertex_count: 10,
1579            coordinate_range: (-1.0, 1.0),
1580            attempt: 5,
1581            underlying_error: "Too many duplicate points".to_string(),
1582        };
1583        let display = format!("{error}");
1584        assert_eq!(
1585            display,
1586            "Delaunay triangulation generation failed: 10 vertices, range [-1, 1], attempt 5: Too many duplicate points"
1587        );
1588    }
1589
1590    #[test]
1591    fn test_delaunay_validation_failed_error() {
1592        let error = CdtError::DelaunayValidationFailed {
1593            level: DelaunayValidationLevel::Four,
1594            detail: "upstream validation failed".to_string(),
1595        };
1596        let display = format!("{error}");
1597        assert_eq!(
1598            display,
1599            "Delaunay validation failed [Level 1-4]: upstream validation failed"
1600        );
1601    }
1602
1603    #[test]
1604    fn validation_level_display_covers_all_levels() {
1605        assert_eq!(DelaunayValidationLevel::One.to_string(), "Level 1");
1606        assert_eq!(DelaunayValidationLevel::Two.to_string(), "Level 1-2");
1607        assert_eq!(DelaunayValidationLevel::Three.to_string(), "Level 1-3");
1608        assert_eq!(DelaunayValidationLevel::Four.to_string(), "Level 1-4");
1609    }
1610
1611    #[test]
1612    fn validation_check_display_covers_all_categories() {
1613        assert_eq!(CdtValidationCheck::Geometry.to_string(), "geometry");
1614        assert_eq!(
1615            CdtValidationCheck::FoliationAssignment.to_string(),
1616            "foliation_assignment"
1617        );
1618        assert_eq!(CdtValidationCheck::Causality.to_string(), "causality");
1619        assert_eq!(
1620            CdtValidationCheck::SimplexClassification.to_string(),
1621            "simplex_classification"
1622        );
1623        assert_eq!(
1624            CdtValidationCheck::ErgodicMoveCandidateGeometry.to_string(),
1625            "ergodic_move_candidate_geometry"
1626        );
1627    }
1628
1629    #[test]
1630    fn configuration_setting_display_covers_all_settings() {
1631        let cases = [
1632            (ConfigurationSetting::Dimension, "dimension"),
1633            (ConfigurationSetting::Vertices, "vertices"),
1634            (ConfigurationSetting::Timeslices, "timeslices"),
1635            (ConfigurationSetting::Temperature, "temperature"),
1636            (ConfigurationSetting::Steps, "steps"),
1637            (
1638                ConfigurationSetting::ThermalizationSteps,
1639                "thermalization_steps",
1640            ),
1641            (
1642                ConfigurationSetting::MeasurementFrequency,
1643                "measurement_frequency",
1644            ),
1645            (
1646                ConfigurationSetting::MeasurementSchedule,
1647                "measurement schedule",
1648            ),
1649            (ConfigurationSetting::Coupling0, "coupling_0"),
1650            (ConfigurationSetting::Coupling2, "coupling_2"),
1651            (
1652                ConfigurationSetting::CosmologicalConstant,
1653                "cosmological_constant",
1654            ),
1655            (ConfigurationSetting::VolumeProfile, "volume_profile"),
1656        ];
1657
1658        for (setting, expected) in cases {
1659            assert_eq!(setting.to_string(), expected);
1660        }
1661    }
1662
1663    #[test]
1664    fn generation_parameter_issue_display_covers_all_issues() {
1665        let cases = [
1666            (
1667                GenerationParameterIssue::InvalidCoordinateRange,
1668                "Invalid coordinate range",
1669            ),
1670            (
1671                GenerationParameterIssue::InvalidToroidalDomain,
1672                "Invalid toroidal domain",
1673            ),
1674            (
1675                GenerationParameterIssue::NonFiniteVertexCoordinate,
1676                "Non-finite vertex coordinate",
1677            ),
1678            (
1679                GenerationParameterIssue::InsufficientVertexCount,
1680                "Insufficient vertex count",
1681            ),
1682            (
1683                GenerationParameterIssue::InsufficientVerticesPerSlice,
1684                "Insufficient vertices per slice",
1685            ),
1686            (
1687                GenerationParameterIssue::InsufficientNumberOfTimeSlices,
1688                "Insufficient number of time slices",
1689            ),
1690            (
1691                GenerationParameterIssue::NonPositiveSliceCount,
1692                "Number of slices must be positive",
1693            ),
1694            (
1695                GenerationParameterIssue::EmptyVolumeProfile,
1696                "Empty volume profile",
1697            ),
1698            (
1699                GenerationParameterIssue::VolumeProfileLengthOverflow,
1700                "Volume profile length overflow",
1701            ),
1702            (
1703                GenerationParameterIssue::InsufficientVerticesInVolumeProfileSlice,
1704                "Insufficient vertices in volume-profile slice",
1705            ),
1706            (
1707                GenerationParameterIssue::VertexCountOverflow,
1708                "Vertex count overflow",
1709            ),
1710            (
1711                GenerationParameterIssue::SimplexCountOverflow,
1712                "Simplex count overflow",
1713            ),
1714        ];
1715
1716        for (issue, expected) in cases {
1717            assert_eq!(issue.to_string(), expected);
1718        }
1719    }
1720
1721    #[test]
1722    fn triangulation_metadata_field_display_covers_all_fields() {
1723        assert_eq!(
1724            TriangulationMetadataField::Timeslices.to_string(),
1725            "timeslices"
1726        );
1727        assert_eq!(
1728            TriangulationMetadataField::Dimension.to_string(),
1729            "dimension"
1730        );
1731    }
1732
1733    #[test]
1734    fn output_format_display_covers_all_formats() {
1735        assert_eq!(OutputFormat::Csv.to_string(), "CSV");
1736        assert_eq!(OutputFormat::Json.to_string(), "JSON");
1737    }
1738
1739    #[test]
1740    fn checkpoint_operation_display_covers_all_operations() {
1741        assert_eq!(CheckpointOperation::Serialize.to_string(), "serialize");
1742        assert_eq!(CheckpointOperation::Deserialize.to_string(), "deserialize");
1743    }
1744
1745    #[test]
1746    fn backend_mutation_operation_display_covers_all_operations() {
1747        let cases = [
1748            (
1749                BackendMutationOperation::SetSimplexDataByKey,
1750                "set_simplex_data_by_key",
1751            ),
1752            (
1753                BackendMutationOperation::SetVertexDataByKey,
1754                "set_vertex_data_by_key",
1755            ),
1756            (BackendMutationOperation::SetVertexData, "set_vertex_data"),
1757            (BackendMutationOperation::SubdivideFace, "subdivide_face"),
1758            (BackendMutationOperation::RemoveVertex, "remove_vertex"),
1759            (BackendMutationOperation::FlipEdge, "flip_edge"),
1760        ];
1761
1762        for (operation, expected) in cases {
1763            assert_eq!(operation.to_string(), expected);
1764        }
1765    }
1766
1767    #[test]
1768    fn checkpoint_move_counter_display_covers_all_categories() {
1769        let cases = [
1770            (CheckpointMoveCounter::Attempted, "attempted"),
1771            (CheckpointMoveCounter::Accepted, "accepted"),
1772            (CheckpointMoveCounter::Rejected, "rejected"),
1773        ];
1774
1775        for (counter, expected) in cases {
1776            assert_eq!(counter.to_string(), expected);
1777        }
1778    }
1779
1780    #[test]
1781    fn proposal_telemetry_counter_display_covers_all_categories() {
1782        let cases = [
1783            (
1784                ProposalTelemetryCounter::MoveFamilyProposals,
1785                "move-family proposals",
1786            ),
1787            (
1788                ProposalTelemetryCounter::AcceptedTransitions,
1789                "accepted transitions",
1790            ),
1791            (
1792                ProposalTelemetryCounter::RejectedTransitions,
1793                "rejected transitions",
1794            ),
1795        ];
1796
1797        for (counter, expected) in cases {
1798            assert_eq!(counter.to_string(), expected);
1799        }
1800    }
1801
1802    #[test]
1803    fn scalar_trace_field_display_covers_all_fields() {
1804        let cases = [
1805            (ScalarTraceField::LogProb, "log_prob"),
1806            (ScalarTraceField::Action, "action"),
1807            (ScalarTraceField::DeltaAction, "delta_action"),
1808            (ScalarTraceField::ActionBefore, "action_before"),
1809            (ScalarTraceField::ActionAfter, "action_after"),
1810        ];
1811
1812        for (field, expected) in cases {
1813            assert_eq!(field.to_string(), expected);
1814        }
1815    }
1816
1817    #[test]
1818    fn simplex_count_field_display_covers_all_fields() {
1819        let cases = [
1820            (SimplexCountField::Vertices, "vertices"),
1821            (SimplexCountField::Edges, "edges"),
1822            (SimplexCountField::Triangles, "triangles"),
1823        ];
1824
1825        for (field, expected) in cases {
1826            assert_eq!(field.to_string(), expected);
1827        }
1828    }
1829
1830    #[test]
1831    fn checkpoint_resume_failure_display_includes_structured_context() {
1832        let failure = CheckpointResumeFailure::ChainCounterMismatch {
1833            chain_accepted: 1,
1834            chain_rejected: 2,
1835            move_accepted: 3,
1836            move_rejected: 4,
1837        };
1838
1839        assert_eq!(
1840            failure.to_string(),
1841            "chain counters do not match move statistics: chain accepted=1, rejected=2; move accepted=3, rejected=4"
1842        );
1843
1844        let action_failure =
1845            CheckpointResumeFailure::NonFiniteCheckpointAction { stored: f64::NAN };
1846        assert_eq!(
1847            action_failure.to_string(),
1848            "checkpoint action is non-finite: stored NaN"
1849        );
1850    }
1851
1852    #[test]
1853    fn proposal_resume_failures_display_structured_context() {
1854        let overflow = CheckpointResumeFailure::ProposalCounterOverflow {
1855            counter: ProposalTelemetryCounter::AcceptedTransitions,
1856        };
1857        assert_eq!(
1858            overflow.to_string(),
1859            "accepted transitions proposal telemetry count exceeds u64::MAX"
1860        );
1861
1862        let hard_failures = CheckpointResumeFailure::ProposalHardFailures { actual: 3 };
1863        assert_eq!(
1864            hard_failures.to_string(),
1865            "proposal telemetry has 3 hard failures; expected 0"
1866        );
1867    }
1868
1869    #[test]
1870    fn test_unsupported_dimension_error() {
1871        let error = CdtError::UnsupportedDimension(3);
1872        let display = format!("{error}");
1873        assert_eq!(
1874            display,
1875            "Unsupported dimension: 3. Only 2D is currently supported"
1876        );
1877    }
1878
1879    #[test]
1880    fn test_invalid_generation_parameters_error() {
1881        let error = CdtError::InvalidGenerationParameters {
1882            issue: GenerationParameterIssue::InsufficientVertexCount,
1883            provided_value: "2".to_string(),
1884            expected_range: "at least 3".to_string(),
1885        };
1886        let display = format!("{error}");
1887        assert_eq!(
1888            display,
1889            "Invalid triangulation parameters: Insufficient vertex count (got: 2, expected: at least 3)"
1890        );
1891    }
1892
1893    #[test]
1894    fn test_validation_failed_error() {
1895        let error = CdtError::ValidationFailed {
1896            check: CdtValidationCheck::Geometry,
1897            failure: CdtValidationFailure::BackendGeometry {
1898                detail: "backend reported invalid triangulation structure".to_string(),
1899            },
1900        };
1901        let display = format!("{error}");
1902        assert_eq!(
1903            display,
1904            "Validation failed [geometry]: backend reported invalid triangulation structure"
1905        );
1906    }
1907
1908    #[test]
1909    fn cdt_validation_failure_display_covers_structured_variants() {
1910        let cases = [
1911            (
1912                CdtValidationFailure::BackendGeometry {
1913                    detail: "backend rejected structure".to_string(),
1914                },
1915                "backend rejected structure",
1916            ),
1917            (
1918                CdtValidationFailure::FaceVerticesUnavailable {
1919                    face: "FaceKey(3v1)".to_string(),
1920                    detail: "backend reported invalid simplex key".to_string(),
1921                },
1922                "failed to resolve vertices for face FaceKey(3v1): backend reported invalid simplex key",
1923            ),
1924            (
1925                CdtValidationFailure::FaceVertexCount {
1926                    face: "FaceKey(3v1)".to_string(),
1927                    actual: 4,
1928                    expected: 3,
1929                },
1930                "face FaceKey(3v1) has 4 vertices, expected 3",
1931            ),
1932            (
1933                CdtValidationFailure::MissingVertexTimeLabel {
1934                    vertex: "VertexKey(7v1)".to_string(),
1935                },
1936                "vertex VertexKey(7v1) has no time label in a foliated triangulation",
1937            ),
1938            (
1939                CdtValidationFailure::InvalidCdtTriangle {
1940                    face: "FaceKey(3v1)".to_string(),
1941                    spacelike_edges: 3,
1942                    timelike_edges: 0,
1943                },
1944                "invalid CDT triangle at face FaceKey(3v1): spacelike=3, timelike=0",
1945            ),
1946            (
1947                CdtValidationFailure::VertexCoordinateReadFailed {
1948                    vertex: "VertexKey(7v1)".to_string(),
1949                    detail: "missing vertex".to_string(),
1950                },
1951                "failed to read coordinates for vertex VertexKey(7v1): missing vertex",
1952            ),
1953            (
1954                CdtValidationFailure::VertexCoordinateDimension {
1955                    vertex: "VertexKey(7v1)".to_string(),
1956                    actual: 1,
1957                    expected_minimum: 2,
1958                },
1959                "vertex VertexKey(7v1) has 1 coordinates, expected ≥ 2",
1960            ),
1961            (
1962                CdtValidationFailure::NonStrictSimplex {
1963                    face: "FaceKey(3v1)".to_string(),
1964                },
1965                "face FaceKey(3v1) is not a strict CDT simplex (expected Up or Down)",
1966            ),
1967            (
1968                CdtValidationFailure::ErgodicMoveCandidateGeometry {
1969                    detail: "candidate edge has no adjacent faces".to_string(),
1970                },
1971                "candidate edge has no adjacent faces",
1972            ),
1973        ];
1974
1975        for (failure, expected) in cases {
1976            assert_eq!(failure.to_string(), expected);
1977        }
1978    }
1979
1980    #[test]
1981    fn test_topology_mismatch_error() {
1982        let error = CdtError::TopologyMismatch {
1983            topology: CdtTopology::Toroidal,
1984            euler_characteristic: 1,
1985            expected_euler_characteristics: vec![0],
1986            vertices: 3,
1987            edges: 3,
1988            faces: 1,
1989        };
1990        let display = format!("{error}");
1991        assert_eq!(
1992            display,
1993            "Topology mismatch for toroidal: Euler characteristic χ=1, expected one of [0] (V=3, E=3, F=1)"
1994        );
1995    }
1996
1997    #[test]
1998    fn test_foliation_error_variant() {
1999        let error = CdtError::Foliation(FoliationError::EmptySlice { slice: 3 });
2000        let display = format!("{error}");
2001        assert_eq!(
2002            display,
2003            "Foliation validation failed: time slice 3 is empty"
2004        );
2005    }
2006
2007    #[test]
2008    fn test_vertex_build_failed_error() {
2009        let error = CdtError::VertexBuildFailed {
2010            context: "explicit CDT vertex 7".to_string(),
2011            underlying_error: "Missing required field: `point`".to_string(),
2012        };
2013        let display = format!("{error}");
2014        assert_eq!(
2015            display,
2016            "Vertex construction failed [explicit CDT vertex 7]: Missing required field: `point`"
2017        );
2018    }
2019
2020    #[test]
2021    fn test_backend_rollback_failed_error() {
2022        let error = CdtError::BackendRollbackFailed {
2023            operation: BackendMutationOperation::SetVertexDataByKey,
2024            target: "vertex VertexKey(123v1)".to_string(),
2025            detail: "backend reported invalid vertex key".to_string(),
2026            rollback_errors: "vertex VertexKey(7v1): backend reported invalid vertex key"
2027                .to_string(),
2028        };
2029        let display = format!("{error}");
2030        assert_eq!(
2031            display,
2032            "Backend mutation failed [set_vertex_data_by_key] on vertex VertexKey(123v1): backend reported invalid vertex key; rollback failed: vertex VertexKey(7v1): backend reported invalid vertex key"
2033        );
2034    }
2035
2036    #[test]
2037    fn test_backend_mutation_failed_error() {
2038        let error = CdtError::BackendMutationFailed {
2039            operation: BackendMutationOperation::SetVertexData,
2040            target: "vertex VertexKey(123v1)".to_string(),
2041            detail: "backend reported invalid vertex key".to_string(),
2042        };
2043        let display = format!("{error}");
2044        assert_eq!(
2045            display,
2046            "Backend mutation failed [set_vertex_data] on vertex VertexKey(123v1): backend reported invalid vertex key"
2047        );
2048    }
2049
2050    #[test]
2051    fn test_metropolis_move_application_failed_error() {
2052        let source = MetropolisMoveApplicationFailure::BackendMutation {
2053            operation: BackendMutationOperation::SetVertexData,
2054            target: "vertex VertexKey(123v1)".to_string(),
2055            detail: "backend reported invalid vertex key".to_string(),
2056        };
2057        let error = CdtError::MetropolisMoveApplicationFailed {
2058            step: 17,
2059            move_type: MoveType::Move31Remove,
2060            attempts: 8,
2061            source: source.clone(),
2062        };
2063        let display = format!("{error}");
2064        assert_eq!(
2065            display,
2066            "Metropolis accepted Move31Remove at step 17, but applying it failed after 8 attempts; source: backend mutation failed [set_vertex_data] on vertex VertexKey(123v1): backend reported invalid vertex key"
2067        );
2068        assert_eq!(
2069            Error::source(&error).map(ToString::to_string),
2070            Some(source.to_string())
2071        );
2072    }
2073
2074    #[test]
2075    fn test_proposal_application_failed_error() {
2076        let source = MetropolisMoveApplicationFailure::BackendMutation {
2077            operation: BackendMutationOperation::SetVertexDataByKey,
2078            target: "vertex VertexKey(123v1)".to_string(),
2079            detail: "backend reported invalid vertex key".to_string(),
2080        };
2081        let error = CdtError::ProposalApplicationFailed {
2082            move_type: MoveType::Move13Add,
2083            attempt: 2,
2084            source: source.clone(),
2085        };
2086        let display = format!("{error}");
2087        assert_eq!(
2088            display,
2089            "CDT proposal failed while applying Move13Add on attempt 2: backend mutation failed [set_vertex_data_by_key] on vertex VertexKey(123v1): backend reported invalid vertex key"
2090        );
2091        assert_eq!(
2092            Error::source(&error).map(ToString::to_string),
2093            Some(source.to_string())
2094        );
2095    }
2096
2097    #[test]
2098    fn planned_proposal_telemetry_missing_reports_step_without_fake_move_type() {
2099        let error = CdtError::PlannedProposalTelemetryMissing { step: 23 };
2100
2101        assert_eq!(
2102            format!("{error}"),
2103            "planned CDT proposal step 23 completed without required proposal telemetry"
2104        );
2105        assert!(Error::source(&error).is_none());
2106    }
2107
2108    #[test]
2109    fn planned_proposal_step_failed_preserves_upstream_detail() {
2110        let error = CdtError::PlannedProposalStepFailed {
2111            step: 23,
2112            detail: "future upstream sampler failure".to_string(),
2113        };
2114
2115        assert_eq!(
2116            format!("{error}"),
2117            "planned CDT proposal step 23 failed: future upstream sampler failure"
2118        );
2119        assert!(Error::source(&error).is_none());
2120    }
2121
2122    #[test]
2123    fn metropolis_move_application_failure_preserves_backend_mutation_fields() {
2124        let failure = MetropolisMoveApplicationFailure::from(CdtError::BackendMutationFailed {
2125            operation: BackendMutationOperation::RemoveVertex,
2126            target: "vertex VertexKey(7v1)".to_string(),
2127            detail: "backend reported invalid vertex key".to_string(),
2128        });
2129
2130        let MetropolisMoveApplicationFailure::BackendMutation {
2131            operation,
2132            target,
2133            detail,
2134        } = failure
2135        else {
2136            panic!("expected backend mutation failure source");
2137        };
2138
2139        assert_eq!(operation, BackendMutationOperation::RemoveVertex);
2140        assert_eq!(target, "vertex VertexKey(7v1)");
2141        assert_eq!(detail, "backend reported invalid vertex key");
2142    }
2143
2144    #[test]
2145    fn metropolis_move_application_failure_preserves_validation_fields() {
2146        let validation_failure = CdtValidationFailure::InvalidCdtTriangle {
2147            face: "FaceKey(3v1)".to_string(),
2148            spacelike_edges: 3,
2149            timelike_edges: 0,
2150        };
2151        let failure = MetropolisMoveApplicationFailure::from(CdtError::ValidationFailed {
2152            check: CdtValidationCheck::Causality,
2153            failure: validation_failure.clone(),
2154        });
2155
2156        let MetropolisMoveApplicationFailure::Validation { check, failure } = failure else {
2157            panic!("expected validation failure source");
2158        };
2159
2160        assert_eq!(check, CdtValidationCheck::Causality);
2161        assert_eq!(failure, validation_failure);
2162    }
2163
2164    #[test]
2165    fn metropolis_move_application_failure_preserves_structured_sources() {
2166        let cases = [
2167            (
2168                CdtError::BackendRollbackFailed {
2169                    operation: BackendMutationOperation::FlipEdge,
2170                    target: "edge EdgeKey(5v1)".to_string(),
2171                    detail: "flip failed".to_string(),
2172                    rollback_errors: "rollback failed".to_string(),
2173                },
2174                MetropolisMoveApplicationFailure::BackendRollback {
2175                    operation: BackendMutationOperation::FlipEdge,
2176                    target: "edge EdgeKey(5v1)".to_string(),
2177                    detail: "flip failed".to_string(),
2178                    rollback_errors: "rollback failed".to_string(),
2179                },
2180            ),
2181            (
2182                CdtError::DelaunayValidationFailed {
2183                    level: DelaunayValidationLevel::Three,
2184                    detail: "invalid triangulation".to_string(),
2185                },
2186                MetropolisMoveApplicationFailure::DelaunayValidation {
2187                    level: DelaunayValidationLevel::Three,
2188                    detail: "invalid triangulation".to_string(),
2189                },
2190            ),
2191            (
2192                CdtError::TopologyMismatch {
2193                    topology: CdtTopology::Toroidal,
2194                    euler_characteristic: 1,
2195                    expected_euler_characteristics: vec![0],
2196                    vertices: 3,
2197                    edges: 3,
2198                    faces: 1,
2199                },
2200                MetropolisMoveApplicationFailure::TopologyMismatch {
2201                    topology: CdtTopology::Toroidal,
2202                    euler_characteristic: 1,
2203                    expected_euler_characteristics: vec![0],
2204                    vertices: 3,
2205                    edges: 3,
2206                    faces: 1,
2207                },
2208            ),
2209            (
2210                CdtError::Foliation(FoliationError::EmptySlice { slice: 3 }),
2211                MetropolisMoveApplicationFailure::Foliation(FoliationError::EmptySlice {
2212                    slice: 3,
2213                }),
2214            ),
2215            (
2216                CdtError::CausalityViolation {
2217                    time_0: 0,
2218                    time_1: 2,
2219                    step_distance: 2,
2220                },
2221                MetropolisMoveApplicationFailure::CausalityViolation {
2222                    time_0: 0,
2223                    time_1: 2,
2224                    step_distance: 2,
2225                },
2226            ),
2227        ];
2228
2229        for (error, expected) in cases {
2230            assert_eq!(MetropolisMoveApplicationFailure::from(error), expected);
2231        }
2232    }
2233
2234    #[test]
2235    fn metropolis_move_application_failure_from_wrapper_preserves_source() {
2236        let source = MetropolisMoveApplicationFailure::BackendMutation {
2237            operation: BackendMutationOperation::RemoveVertex,
2238            target: "vertex VertexKey(7v1)".to_string(),
2239            detail: "backend reported invalid vertex key".to_string(),
2240        };
2241        let failure =
2242            MetropolisMoveApplicationFailure::from(CdtError::MetropolisMoveApplicationFailed {
2243                step: 17,
2244                move_type: MoveType::Move31Remove,
2245                attempts: 8,
2246                source: source.clone(),
2247            });
2248
2249        assert_eq!(failure, source);
2250    }
2251
2252    #[test]
2253    fn metropolis_move_application_failure_unexpected_retains_diagnostic() {
2254        let failure = MetropolisMoveApplicationFailure::from(CdtError::UnsupportedDimension(3));
2255
2256        let MetropolisMoveApplicationFailure::Unexpected { detail } = failure else {
2257            panic!("expected unexpected failure source");
2258        };
2259
2260        assert_eq!(
2261            detail,
2262            "Unsupported dimension: 3. Only 2D is currently supported"
2263        );
2264    }
2265
2266    #[test]
2267    fn test_causality_violation_open_boundary_error() {
2268        // OpenBoundary topology: step_distance == |Δt|.
2269        let error = CdtError::CausalityViolation {
2270            time_0: 0,
2271            time_1: 3,
2272            step_distance: 3,
2273        };
2274        let display = format!("{error}");
2275        assert_eq!(
2276            display,
2277            "Causality violation: edge spans 3 time-slice steps (t=0 to t=3), maximum allowed is 1"
2278        );
2279    }
2280
2281    #[test]
2282    fn test_causality_violation_toroidal_error_reports_circular_distance() {
2283        // Toroidal T=10, t0=0, t1=8: raw |Δt|=8 but circular step distance is 2.
2284        let error = CdtError::CausalityViolation {
2285            time_0: 0,
2286            time_1: 8,
2287            step_distance: 2,
2288        };
2289        let display = format!("{error}");
2290        assert_eq!(
2291            display,
2292            "Causality violation: edge spans 2 time-slice steps \
2293             (t=0 to t=8, |Δt|=8 on the time circle), maximum allowed is 1"
2294        );
2295    }
2296
2297    #[test]
2298    fn test_mcmc_error() {
2299        let error = CdtError::Mcmc(McmcError::NanProposedLogProb);
2300        let display = format!("{error}");
2301        assert_eq!(
2302            display,
2303            "MCMC error: target returned NaN log-probability for a proposed state"
2304        );
2305    }
2306
2307    #[test]
2308    fn test_mcmc_error_from_conversion() {
2309        let mcmc_err = McmcError::NanProposedLogProb;
2310        let cdt_err: CdtError = mcmc_err.into();
2311        assert_matches!(cdt_err, CdtError::Mcmc(McmcError::NanProposedLogProb));
2312        let display = format!("{cdt_err}");
2313        assert!(
2314            display.contains("MCMC error"),
2315            "Should contain MCMC error prefix: {display}"
2316        );
2317        assert!(
2318            display.contains("NaN"),
2319            "Should contain NaN context: {display}"
2320        );
2321    }
2322
2323    #[test]
2324    fn test_output_write_failed_error() {
2325        let error = CdtError::OutputWriteFailed {
2326            path: "trace.csv".to_string(),
2327            format: OutputFormat::Csv,
2328            detail: "permission denied".to_string(),
2329        };
2330        let CdtError::OutputWriteFailed {
2331            path,
2332            format,
2333            detail,
2334        } = &error
2335        else {
2336            panic!("expected OutputWriteFailed variant");
2337        };
2338        assert_eq!(path, "trace.csv");
2339        assert_eq!(*format, OutputFormat::Csv);
2340        assert_eq!(detail, "permission denied");
2341        let display = format!("{error}");
2342        assert_eq!(
2343            display,
2344            "Failed to write CSV output to trace.csv: permission denied"
2345        );
2346    }
2347
2348    #[test]
2349    fn test_output_path_resolution_failed_error() {
2350        let error = CdtError::OutputPathResolutionFailed {
2351            base_path: ".".to_string(),
2352            detail: "No such file or directory".to_string(),
2353        };
2354        let CdtError::OutputPathResolutionFailed { base_path, detail } = &error else {
2355            panic!("expected OutputPathResolutionFailed variant");
2356        };
2357        assert_eq!(base_path, ".");
2358        assert_eq!(detail, "No such file or directory");
2359        let display = format!("{error}");
2360        assert_eq!(
2361            display,
2362            "Failed to resolve output path from base .: No such file or directory"
2363        );
2364    }
2365
2366    #[test]
2367    fn test_output_path_conflict_error() {
2368        let error = CdtError::OutputPathConflict {
2369            csv_path: "output/results".to_string(),
2370            json_path: "output/results".to_string(),
2371        };
2372        let CdtError::OutputPathConflict {
2373            csv_path,
2374            json_path,
2375        } = &error
2376        else {
2377            panic!("expected OutputPathConflict variant");
2378        };
2379        assert_eq!(csv_path, "output/results");
2380        assert_eq!(json_path, "output/results");
2381        assert_eq!(
2382            format!("{error}"),
2383            "CSV output path output/results and JSON output path output/results resolve to the same file"
2384        );
2385    }
2386
2387    #[test]
2388    fn test_output_read_failed_error() {
2389        let error = CdtError::OutputReadFailed {
2390            path: "summary.json".to_string(),
2391            format: OutputFormat::Json,
2392            detail: "expected value at line 1 column 1".to_string(),
2393        };
2394        let CdtError::OutputReadFailed {
2395            path,
2396            format,
2397            detail,
2398        } = &error
2399        else {
2400            panic!("expected OutputReadFailed variant");
2401        };
2402        assert_eq!(path, "summary.json");
2403        assert_eq!(*format, OutputFormat::Json);
2404        assert_eq!(detail, "expected value at line 1 column 1");
2405        let display = format!("{error}");
2406        assert_eq!(
2407            display,
2408            "Failed to read JSON output from summary.json: expected value at line 1 column 1"
2409        );
2410    }
2411
2412    #[test]
2413    fn test_checkpoint_serialization_failed_error() {
2414        let error = CdtError::CheckpointSerializationFailed {
2415            operation: CheckpointOperation::Deserialize,
2416            target: "final triangulation".to_string(),
2417            detail: "missing field `geometry`".to_string(),
2418        };
2419        let CdtError::CheckpointSerializationFailed {
2420            operation,
2421            target,
2422            detail,
2423        } = &error
2424        else {
2425            panic!("expected CheckpointSerializationFailed variant");
2426        };
2427        assert_eq!(*operation, CheckpointOperation::Deserialize);
2428        assert_eq!(target, "final triangulation");
2429        assert_eq!(detail, "missing field `geometry`");
2430        let display = format!("{error}");
2431        assert_eq!(
2432            display,
2433            "Failed to deserialize final triangulation checkpoint: missing field `geometry`"
2434        );
2435    }
2436
2437    #[test]
2438    fn test_checkpoint_resume_failed_error() {
2439        let error = CdtError::CheckpointResumeFailed {
2440            failure: CheckpointResumeFailure::IncompatibleTemperature,
2441        };
2442        let CdtError::CheckpointResumeFailed { failure } = &error else {
2443            panic!("expected CheckpointResumeFailed variant");
2444        };
2445        assert_eq!(*failure, CheckpointResumeFailure::IncompatibleTemperature);
2446        assert_eq!(
2447            format!("{error}"),
2448            "Failed to resume MCMC checkpoint: temperature differs from checkpoint"
2449        );
2450        assert_eq!(
2451            Error::source(&error).map(ToString::to_string),
2452            Some("temperature differs from checkpoint".to_string())
2453        );
2454    }
2455
2456    #[test]
2457    fn test_error_equality() {
2458        let error1 = CdtError::InvalidConfiguration {
2459            setting: ConfigurationSetting::Steps,
2460            provided_value: "0".to_string(),
2461            expected: "≥ 1".to_string(),
2462        };
2463        let error2 = CdtError::InvalidConfiguration {
2464            setting: ConfigurationSetting::Steps,
2465            provided_value: "0".to_string(),
2466            expected: "≥ 1".to_string(),
2467        };
2468        let error3 = CdtError::InvalidConfiguration {
2469            setting: ConfigurationSetting::Steps,
2470            provided_value: "10".to_string(),
2471            expected: "≥ 1".to_string(),
2472        };
2473
2474        assert_eq!(error1, error2);
2475        assert_ne!(error1, error3);
2476    }
2477
2478    #[test]
2479    fn test_error_clone() {
2480        let error = CdtError::UnsupportedDimension(4);
2481        let cloned = error.clone();
2482        assert_eq!(error, cloned);
2483    }
2484
2485    #[test]
2486    fn test_error_debug() {
2487        let error = CdtError::InvalidConfiguration {
2488            setting: ConfigurationSetting::Vertices,
2489            provided_value: "2".to_string(),
2490            expected: "≥ 3".to_string(),
2491        };
2492        let debug_str = format!("{error:?}");
2493        assert!(debug_str.contains("InvalidConfiguration"));
2494        assert!(debug_str.contains("Vertices"));
2495    }
2496
2497    #[test]
2498    fn test_cdt_result_type() {
2499        let success: CdtResult<i32> = Ok(42);
2500        let failure: CdtResult<i32> = Err(CdtError::InvalidConfiguration {
2501            setting: ConfigurationSetting::Steps,
2502            provided_value: "0".to_string(),
2503            expected: "≥ 1".to_string(),
2504        });
2505
2506        assert!(success.is_ok());
2507        assert!(failure.is_err());
2508        assert_eq!(success, Ok(42));
2509    }
2510
2511    #[test]
2512    fn test_error_is_send_sync() {
2513        fn assert_send_sync<T: Send + Sync>() {}
2514        assert_send_sync::<CdtError>();
2515    }
2516
2517    #[test]
2518    fn test_std_error_trait() {
2519        let error = CdtError::InvalidConfiguration {
2520            setting: ConfigurationSetting::Temperature,
2521            provided_value: "NaN".to_string(),
2522            expected: "finite and positive".to_string(),
2523        };
2524        let _: &dyn Error = &error;
2525        // If this compiles, the trait is implemented correctly
2526    }
2527}