Skip to main content

erdos_unit_distance/
lib.rs

1// Copyright (c) 2026 Lars Brusletto
2// Under MIT and Apache-2.0 Licenses.
3//
4// This library implements certified finite prototypes for the Erdős unit-distance
5// problem, based on OpenAI's 2026 disproof and the companion mathematical remarks.
6// The current native algebraic implementation follows the Section 2 geometric shape;
7// it does not yet implement the full class-field tower construction.
8
9//! # erdos-unit-distance
10//!
11//! Certified unit-distance point sets and unit distance graphs for discrete geometry.
12//!
13//! This crate generates classical examples such as the Moser spindle and finite
14//! certified multiquadratic constructions related to the Erdős/Erdos unit-distance
15//! problem. Classical constructions are exact; native finite multiquadratic
16//! prototype inputs are validated and unsupported configurations are rejected
17//! before generation.
18//!
19//! ## Correctness Contract
20//!
21//! - Classical constructors return fixed, deterministic point sets.
22//! - Native multiquadratic constructors validate squarefree generators and
23//!   complete splitting of the requested odd prime before generating points.
24//! - Native multiquadratic point generation is a bounded Section 2-style
25//!   candidate search followed by projection, deduplication, and density pruning;
26//!   it is not the full class-field tower construction.
27//! - `generate_certified` returns a finite certificate whose edge claims are
28//!   independently recomputed by `ConstructionCertificate::verify`.
29//!
30//! ## Quick Start
31//!
32//! ```
33//! use erdos_unit_distance::UnitDistanceSet;
34//! use erdos_unit_distance::utils::count_unit_distances;
35//!
36//! // Generate a classic Moser Spindle (7 points, 11 unit-distance edges)
37//! let spindle = UnitDistanceSet::moser_spindle();
38//! let points = spindle.generate_points(7).unwrap();
39//! assert_eq!(points.len(), 7);
40//!
41//! let point_arrays = points.iter().map(|point| point.to_array()).collect::<Vec<_>>();
42//! let edges = count_unit_distances(&point_arrays, 1e-4);
43//! assert_eq!(edges, 11);
44//! ```
45//!
46//! ## Native Finite Multiquadratic Prototype
47//!
48//! ```
49//! use erdos_unit_distance::{MultiquadraticConfig, UnitDistanceSet};
50//! use erdos_unit_distance::utils::count_unit_distances;
51//!
52//! let config = MultiquadraticConfig::builder(vec![5, 17], 101, 1).build().unwrap();
53//! let builder = UnitDistanceSet::multiquadratic(config);
54//! let points = builder.generate_points(50).unwrap();
55//! assert_eq!(points.len(), 50);
56//!
57//! let point_arrays = points.iter().map(|point| point.to_array()).collect::<Vec<_>>();
58//! let edges = count_unit_distances(&point_arrays, 1e-4);
59//! assert!(edges > 0);
60//! ```
61//!
62//! ## Certified Output
63//!
64//! ```
65//! use erdos_unit_distance::{MultiquadraticConfig, UnitDistanceSet};
66//!
67//! let config = MultiquadraticConfig::builder(vec![5, 17], 101, 1).build().unwrap();
68//! let builder = UnitDistanceSet::multiquadratic(config);
69//! let certified = builder.generate_certified(20).unwrap();
70//! let report = certified.verify().unwrap();
71//! assert_eq!(report.point_count, 20);
72//! assert!(report.edge_count > 0);
73//! ```
74
75pub mod algebraic {
76    pub mod field;
77    pub mod lattice;
78    pub mod section2;
79    pub mod units;
80    pub mod window;
81}
82pub mod backend;
83pub mod certificate;
84pub mod classical;
85pub mod error;
86mod numeric;
87pub mod utils;
88
89use crate::algebraic::field::MultiQuadraticField;
90use crate::algebraic::section2::generate_native_multiquadratic_section2;
91pub use crate::certificate::{
92    CertificateVerificationReport, CertifiedPointSet, ClassicalCertificate,
93    ClassicalConstructionKind, ConstructionCertificate, FloatingAuditReport,
94    MultiquadraticCertificate, Point2,
95};
96use crate::classical::{generate_moser_spindle, generate_square_grid, generate_triangular_grid};
97pub use crate::error::{GenerationError, VerificationError};
98
99const DEFAULT_MAX_PRIME_EXPONENT: usize = 3;
100const DEFAULT_MAX_RADIUS_ATTEMPTS: usize = 15;
101const DEFAULT_INITIAL_RADIUS: f64 = 2.0;
102const DEFAULT_RADIUS_GROWTH: f64 = 1.5;
103const DEFAULT_CANDIDATE_MULTIPLIER: usize = 10;
104const DEFAULT_PROJECTION_TOLERANCE: f64 = 1e-7;
105const DEFAULT_UNIT_DISTANCE_TOLERANCE: f64 = 1e-4;
106
107/// Validated parameters for the native finite multiquadratic prototype.
108#[derive(Clone, Debug)]
109pub struct MultiquadraticConfig {
110    generators: Vec<i64>,
111    split_prime: i64,
112    k: usize,
113    max_prime_exponent: usize,
114    max_radius_attempts: usize,
115    initial_radius: f64,
116    radius_growth: f64,
117    candidate_multiplier: usize,
118    projection_tolerance: f64,
119    unit_distance_tolerance: f64,
120}
121
122impl MultiquadraticConfig {
123    pub fn builder(
124        generators: Vec<i64>,
125        split_prime: i64,
126        k: usize,
127    ) -> MultiquadraticConfigBuilder {
128        MultiquadraticConfigBuilder {
129            generators,
130            split_prime,
131            k,
132            max_prime_exponent: DEFAULT_MAX_PRIME_EXPONENT,
133            max_radius_attempts: DEFAULT_MAX_RADIUS_ATTEMPTS,
134            initial_radius: DEFAULT_INITIAL_RADIUS,
135            radius_growth: DEFAULT_RADIUS_GROWTH,
136            candidate_multiplier: DEFAULT_CANDIDATE_MULTIPLIER,
137            projection_tolerance: DEFAULT_PROJECTION_TOLERANCE,
138            unit_distance_tolerance: DEFAULT_UNIT_DISTANCE_TOLERANCE,
139        }
140    }
141
142    /// Creates a validated multiquadratic configuration with conservative default search limits.
143    pub fn new(generators: Vec<i64>, split_prime: i64, k: usize) -> Result<Self, GenerationError> {
144        let mut generators = generators;
145        if !generators.contains(&-1) {
146            generators.push(-1);
147        }
148        MultiQuadraticField::try_new(&generators)?;
149        validate_split_prime(split_prime)?;
150        validate_complete_splitting(&generators, split_prime)?;
151
152        Ok(Self {
153            generators,
154            split_prime,
155            k,
156            max_prime_exponent: DEFAULT_MAX_PRIME_EXPONENT,
157            max_radius_attempts: DEFAULT_MAX_RADIUS_ATTEMPTS,
158            initial_radius: DEFAULT_INITIAL_RADIUS,
159            radius_growth: DEFAULT_RADIUS_GROWTH,
160            candidate_multiplier: DEFAULT_CANDIDATE_MULTIPLIER,
161            projection_tolerance: DEFAULT_PROJECTION_TOLERANCE,
162            unit_distance_tolerance: DEFAULT_UNIT_DISTANCE_TOLERANCE,
163        })
164    }
165
166    pub fn generators(&self) -> &[i64] {
167        &self.generators
168    }
169
170    pub fn split_prime(&self) -> i64 {
171        self.split_prime
172    }
173
174    pub fn k(&self) -> usize {
175        self.k
176    }
177
178    pub fn max_prime_exponent(&self) -> usize {
179        self.max_prime_exponent
180    }
181
182    pub fn max_radius_attempts(&self) -> usize {
183        self.max_radius_attempts
184    }
185
186    pub fn initial_radius(&self) -> f64 {
187        self.initial_radius
188    }
189
190    pub fn radius_growth(&self) -> f64 {
191        self.radius_growth
192    }
193
194    pub fn candidate_multiplier(&self) -> usize {
195        self.candidate_multiplier
196    }
197
198    pub fn projection_tolerance(&self) -> f64 {
199        self.projection_tolerance
200    }
201
202    pub fn unit_distance_tolerance(&self) -> f64 {
203        self.unit_distance_tolerance
204    }
205
206    pub fn with_prime_search_limit(
207        mut self,
208        max_prime_exponent: usize,
209    ) -> Result<Self, GenerationError> {
210        validate_nonzero_usize("max_prime_exponent", max_prime_exponent)?;
211        self.max_prime_exponent = max_prime_exponent;
212        Ok(self)
213    }
214
215    pub fn with_radius_search(
216        mut self,
217        max_radius_attempts: usize,
218        initial_radius: f64,
219        radius_growth: f64,
220    ) -> Result<Self, GenerationError> {
221        validate_nonzero_usize("max_radius_attempts", max_radius_attempts)?;
222        validate_positive_f64("initial_radius", initial_radius)?;
223        if !radius_growth.is_finite() || radius_growth <= 1.0 {
224            return Err(GenerationError::InvalidSearchParameter {
225                parameter: "radius_growth",
226                reason: "expected a finite value greater than 1.0",
227            });
228        }
229        self.max_radius_attempts = max_radius_attempts;
230        self.initial_radius = initial_radius;
231        self.radius_growth = radius_growth;
232        Ok(self)
233    }
234
235    pub fn with_candidate_multiplier(
236        mut self,
237        candidate_multiplier: usize,
238    ) -> Result<Self, GenerationError> {
239        validate_nonzero_usize("candidate_multiplier", candidate_multiplier)?;
240        self.candidate_multiplier = candidate_multiplier;
241        Ok(self)
242    }
243
244    pub fn with_tolerances(
245        mut self,
246        projection_tolerance: f64,
247        unit_distance_tolerance: f64,
248    ) -> Result<Self, GenerationError> {
249        validate_positive_f64("projection_tolerance", projection_tolerance)?;
250        validate_positive_f64("unit_distance_tolerance", unit_distance_tolerance)?;
251        self.projection_tolerance = projection_tolerance;
252        self.unit_distance_tolerance = unit_distance_tolerance;
253        Ok(self)
254    }
255}
256
257#[derive(Clone, Debug)]
258pub struct MultiquadraticConfigBuilder {
259    generators: Vec<i64>,
260    split_prime: i64,
261    k: usize,
262    max_prime_exponent: usize,
263    max_radius_attempts: usize,
264    initial_radius: f64,
265    radius_growth: f64,
266    candidate_multiplier: usize,
267    projection_tolerance: f64,
268    unit_distance_tolerance: f64,
269}
270
271impl MultiquadraticConfigBuilder {
272    pub fn max_prime_exponent(mut self, value: usize) -> Self {
273        self.max_prime_exponent = value;
274        self
275    }
276
277    pub fn radius_search(mut self, attempts: usize, initial_radius: f64, growth: f64) -> Self {
278        self.max_radius_attempts = attempts;
279        self.initial_radius = initial_radius;
280        self.radius_growth = growth;
281        self
282    }
283
284    pub fn candidate_multiplier(mut self, value: usize) -> Self {
285        self.candidate_multiplier = value;
286        self
287    }
288
289    pub fn tolerances(mut self, projection_tolerance: f64, unit_distance_tolerance: f64) -> Self {
290        self.projection_tolerance = projection_tolerance;
291        self.unit_distance_tolerance = unit_distance_tolerance;
292        self
293    }
294
295    pub fn build(self) -> Result<MultiquadraticConfig, GenerationError> {
296        MultiquadraticConfig::new(self.generators, self.split_prime, self.k)?
297            .with_prime_search_limit(self.max_prime_exponent)?
298            .with_radius_search(
299                self.max_radius_attempts,
300                self.initial_radius,
301                self.radius_growth,
302            )?
303            .with_candidate_multiplier(self.candidate_multiplier)?
304            .with_tolerances(self.projection_tolerance, self.unit_distance_tolerance)
305    }
306}
307
308/// Describes the type of unit-distance construction to generate.
309#[derive(Clone, Debug)]
310pub enum ConstructionType {
311    /// A rectangular grid with unit spacing.
312    SquareGrid { rows: usize, cols: usize },
313    /// A hexagonal/triangular grid with unit spacing.
314    TriangularGrid,
315    /// The canonical 7-point, 11-edge Moser Spindle.
316    MoserSpindle,
317    /// Native finite Section 2 prototype over a multiquadratic field.
318    Multiquadratic(MultiquadraticConfig),
319}
320
321/// Builder for generating unit-distance point sets.
322///
323/// # Behaviour by construction type
324///
325/// - **`SquareGrid`**: Returns `rows × cols` points; `n_target` is ignored.
326/// - **`MoserSpindle`**: Always returns exactly 7 points; `n_target` is ignored.
327/// - **`TriangularGrid`**: Returns exactly `n_target` points.
328/// - **`Multiquadratic`**: Returns exactly `n_target` points from the native multiquadratic prototype.
329#[derive(Clone, Debug)]
330pub struct UnitDistanceSet {
331    construction: ConstructionType,
332}
333
334impl UnitDistanceSet {
335    /// Creates a new builder for a square grid.
336    pub fn square_grid(rows: usize, cols: usize) -> Self {
337        UnitDistanceSet {
338            construction: ConstructionType::SquareGrid { rows, cols },
339        }
340    }
341
342    /// Creates a new builder for a triangular grid.
343    pub fn triangular_grid() -> Self {
344        UnitDistanceSet {
345            construction: ConstructionType::TriangularGrid,
346        }
347    }
348
349    /// Creates a new builder for the Moser Spindle.
350    pub fn moser_spindle() -> Self {
351        UnitDistanceSet {
352            construction: ConstructionType::MoserSpindle,
353        }
354    }
355
356    /// Creates a validated builder for the native finite multiquadratic prototype.
357    ///
358    /// `generators` are the positive squarefree integers generating the real subfield.
359    /// The imaginary generator `-1` is appended automatically if not already present.
360    /// `split_prime` must be an odd prime split completely in K.
361    /// `k` is the exponent/depth parameter.
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// use erdos_unit_distance::{ConstructionType, MultiquadraticConfig, UnitDistanceSet};
367    ///
368    /// let config = MultiquadraticConfig::builder(vec![5, 17], 101, 1).build().unwrap();
369    /// let set = UnitDistanceSet::multiquadratic(config);
370    /// match set.construction() {
371    ///     ConstructionType::Multiquadratic(config) => assert!(config.generators().contains(&-1)),
372    ///     _ => unreachable!(),
373    /// }
374    /// ```
375    pub fn try_new_multiquadratic(
376        generators: Vec<i64>,
377        split_prime: i64,
378        k: usize,
379    ) -> Result<Self, GenerationError> {
380        Ok(UnitDistanceSet {
381            construction: ConstructionType::Multiquadratic(MultiquadraticConfig::new(
382                generators,
383                split_prime,
384                k,
385            )?),
386        })
387    }
388
389    pub fn multiquadratic(config: MultiquadraticConfig) -> Self {
390        UnitDistanceSet {
391            construction: ConstructionType::Multiquadratic(config),
392        }
393    }
394
395    pub fn construction(&self) -> &ConstructionType {
396        &self.construction
397    }
398
399    /// Generates the point set of target size `n_target`.
400    ///
401    /// For `SquareGrid` and `MoserSpindle`, the `n_target` parameter is ignored
402    /// and the natural point count of the construction is returned.
403    pub fn generate(&self, n_target: usize) -> Result<Vec<[f64; 2]>, GenerationError> {
404        match &self.construction {
405            ConstructionType::SquareGrid { rows, cols } => Ok(generate_square_grid(*rows, *cols)),
406            ConstructionType::TriangularGrid => Ok(generate_triangular_grid(n_target)),
407            ConstructionType::MoserSpindle => Ok(generate_moser_spindle()),
408            ConstructionType::Multiquadratic(config) => {
409                Ok(generate_native_multiquadratic_section2(config, n_target)?.projected_points)
410            }
411        }
412    }
413
414    pub fn generate_points(&self, n_target: usize) -> Result<Vec<Point2>, GenerationError> {
415        Ok(self
416            .generate(n_target)?
417            .into_iter()
418            .map(Point2::from)
419            .collect())
420    }
421
422    /// Generates a point set plus a machine-checkable certificate for the finite output.
423    ///
424    /// The certificate is authoritative for verification; floating coordinates are
425    /// still exposed for display, export, and downstream numerical workflows.
426    pub fn generate_certified(
427        &self,
428        n_target: usize,
429    ) -> Result<CertifiedPointSet, GenerationError> {
430        Ok(match &self.construction {
431            ConstructionType::SquareGrid { rows, cols } => {
432                let points = generate_square_grid(*rows, *cols);
433                CertifiedPointSet::new_classical(
434                    ClassicalConstructionKind::SquareGrid {
435                        rows: *rows,
436                        cols: *cols,
437                    },
438                    n_target,
439                    points,
440                    DEFAULT_UNIT_DISTANCE_TOLERANCE,
441                )
442            }
443            ConstructionType::TriangularGrid => {
444                let points = generate_triangular_grid(n_target);
445                CertifiedPointSet::new_classical(
446                    ClassicalConstructionKind::TriangularGrid,
447                    n_target,
448                    points,
449                    DEFAULT_UNIT_DISTANCE_TOLERANCE,
450                )
451            }
452            ConstructionType::MoserSpindle => {
453                let points = generate_moser_spindle();
454                CertifiedPointSet::new_classical(
455                    ClassicalConstructionKind::MoserSpindle,
456                    n_target,
457                    points,
458                    DEFAULT_UNIT_DISTANCE_TOLERANCE,
459                )
460            }
461            ConstructionType::Multiquadratic(config) => {
462                CertifiedPointSet::new_algebraic_from_section2(
463                    generate_native_multiquadratic_section2(config, n_target)?,
464                )?
465            }
466        })
467    }
468}
469
470fn validate_split_prime(split_prime: i64) -> Result<(), GenerationError> {
471    if split_prime <= 2 || !is_prime(split_prime) {
472        return Err(GenerationError::InvalidSplitPrime { split_prime });
473    }
474    Ok(())
475}
476
477fn validate_nonzero_usize(parameter: &'static str, value: usize) -> Result<(), GenerationError> {
478    if value == 0 {
479        return Err(GenerationError::InvalidSearchParameter {
480            parameter,
481            reason: "expected a value greater than zero",
482        });
483    }
484    Ok(())
485}
486
487fn validate_positive_f64(parameter: &'static str, value: f64) -> Result<(), GenerationError> {
488    if !value.is_finite() || value <= 0.0 {
489        return Err(GenerationError::InvalidSearchParameter {
490            parameter,
491            reason: "expected a finite value greater than zero",
492        });
493    }
494    Ok(())
495}
496
497fn validate_complete_splitting(
498    generators: &[i64],
499    split_prime: i64,
500) -> Result<(), GenerationError> {
501    generators.iter().try_for_each(|&generator| {
502        if legendre_symbol(generator, split_prime) == 1 {
503            Ok(())
504        } else {
505            Err(GenerationError::PrimeNotSplit {
506                split_prime,
507                generator,
508            })
509        }
510    })
511}
512
513fn is_prime(n: i64) -> bool {
514    if n < 2 {
515        return false;
516    }
517    if n == 2 {
518        return true;
519    }
520    if n % 2 == 0 {
521        return false;
522    }
523    !(3..)
524        .step_by(2)
525        .take_while(|d| d * d <= n)
526        .any(|d| n % d == 0)
527}
528
529fn legendre_symbol(a: i64, p: i64) -> i64 {
530    let a = a.rem_euclid(p);
531    if a == 0 {
532        return 0;
533    }
534    let value = mod_pow(a, (p - 1) / 2, p);
535    if value == p - 1 { -1 } else { value }
536}
537
538fn mod_pow(mut base: i64, exp: i64, modulus: i64) -> i64 {
539    base = base.rem_euclid(modulus);
540    std::iter::successors(Some((base, exp)), |&(base, exp)| {
541        (exp / 2 > 0).then_some(((base * base).rem_euclid(modulus), exp / 2))
542    })
543    .filter(|&(_, exp)| exp % 2 == 1)
544    .fold(1, |result, (base, _)| (result * base).rem_euclid(modulus))
545}