1use crate::backend::{TickInt, Ticks};
30use crate::error::Result;
31use crate::tier::TierTable;
32use crate::value::{Delta, Instant, SignedWindow};
33
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
39#[non_exhaustive]
40pub enum Frame {
41 FlrwComoving,
45}
46
47impl Frame {
48 pub const fn describe(self) -> &'static str {
50 match self {
51 Frame::FlrwComoving => "FLRW comoving (cosmological time, CMB rest frame)",
52 }
53 }
54}
55
56#[derive(Clone, Copy, PartialEq, Eq, Debug)]
58pub struct Citation {
59 pub source: &'static str,
61 pub locator: Option<&'static str>,
63}
64
65#[derive(Clone, Copy, PartialEq, Eq, Debug)]
71pub struct MeasuredValue {
72 pub verbatim: &'static str,
74 pub unit: &'static str,
76 pub quantity: &'static str,
78 pub uncertainty: Option<&'static str>,
80 pub citation: Citation,
82}
83
84#[derive(Clone, Copy, PartialEq, Eq, Debug)]
87pub struct RoundingRecord {
88 pub to: &'static str,
90 pub mode: &'static str,
92 pub residual_ticks: &'static str,
94 pub residual_rendered: &'static str,
96 pub rationale: &'static str,
98}
99
100#[derive(Clone, Copy, PartialEq, Eq, Debug)]
105pub struct Provenance {
106 pub input: MeasuredValue,
108 pub unit_defs: &'static [(&'static str, &'static str)],
110 pub chain: &'static [&'static str],
113 pub rounding: RoundingRecord,
115 pub earth_dependency: &'static str,
118 pub alternative_routes: &'static [&'static str],
121}
122
123#[derive(Clone, PartialEq, Eq, Debug)]
130pub struct Bridge {
131 pub name: &'static str,
133 pub ticks: Ticks,
135 pub divisibility: u32,
138 pub epoch_label: &'static str,
141}
142
143pub trait Profile: 'static + Copy + Clone + PartialEq + Eq + core::fmt::Debug {
145 const TAG: &'static str;
147
148 const FRAME: Frame;
150
151 fn beat() -> Ticks;
153
154 fn origin_offset() -> Ticks;
157
158 fn domain_max() -> Ticks;
160
161 fn bridge() -> Bridge;
163
164 fn tiers() -> TierTable {
166 TierTable::build()
167 }
168
169 fn big_bang_claim() -> SignedWindow;
178
179 fn big_bang_claim_citation() -> Citation;
181
182 fn datum_provenance() -> Result<&'static Provenance>;
185
186 fn datum_statement() -> &'static str {
192 "tick 0 is a stipulated reference point, conventionally identified with \
193 the FLRW t→0 limit"
194 }
195
196 fn datum() -> Instant<Self>
198 where
199 Self: Sized,
200 {
201 Instant::zero()
202 }
203
204 fn bridge_epoch() -> Result<Instant<Self>>
206 where
207 Self: Sized,
208 {
209 Instant::from_ticks(Self::origin_offset())
210 }
211}
212
213#[derive(Clone, Copy, PartialEq, Eq, Debug)]
215pub struct UC1;
216
217pub mod uc1 {
219 pub mod consts {
226 pub const BEAT_DEC: &str = "867361737988403547205962240695953369140625";
228
229 pub const ORIGIN_OFFSET_DEC: &str =
237 "8070204002895596515944343085635637180530466139316558837890625";
238
239 pub const ORIGIN_OFFSET_BEATS_DEC: &str = "9304311741502590385";
242
243 pub const SECOND_DEC: &str = "18548584399861000000000000000000000000000000";
245
246 pub const SECOND_DIVISIBILITY: u32 = 30;
250
251 pub(in crate::profile) const BIG_BANG_CLAIM_HALFWIDTH_DEC: &str =
257 "11706976141141069872000000000000000000000000000000000000000";
258 }
259
260 use super::{Citation, MeasuredValue, Provenance, RoundingRecord};
261
262 pub const PLANCK_2018: Citation = Citation {
264 source: "Planck 2018 results VI: Cosmological parameters, A&A 641, A6 (2020)",
265 locator: Some("doi:10.1051/0004-6361/201833910"),
266 };
267
268 pub static PROVENANCE: Provenance = Provenance {
270 input: MeasuredValue {
271 verbatim: "13.787",
272 unit: "Gyr",
273 quantity: "age_of_universe",
274 uncertainty: Some("0.020 Gyr"),
275 citation: PLANCK_2018,
276 },
277 unit_defs: &[(
278 "Gyr",
279 "10^9 x 31 557 600 s (Julian years, exact by definition)",
280 )],
281 chain: &[
282 "AGE_s = 13 787 000 000 x 31 557 600 = 435 084 631 200 000 000 s (exact)",
283 "AGE_ticks = AGE_s x SECOND = \
284 8070204002895596516263200000000000000000000000000000000000000 (exact)",
285 "beats = round_half_even(AGE_ticks / BEAT) = 9 304 311 741 502 590 385",
286 "ORIGIN_OFFSET = beats x BEAT = \
287 8070204002895596515944343085635637180530466139316558837890625",
288 ],
289 rounding: RoundingRecord {
290 to: "BEAT",
291 mode: "half_even",
292 residual_ticks: "-318856914364362819469533860683441162109375",
293 residual_rendered: "-0.017190364 s",
294 rationale: "a whole-beat datum makes all sub-beat digits of the bridge \
295 epoch zero (§2.4)",
296 },
297 earth_dependency:
298 "The input arrives in Julian years and the bridge anchor is an Earth \
299 calendar date. Both are metrology (Rule Y). Neither appears in any \
300 computation: ORIGIN_OFFSET is a declared integer of ticks.",
301 alternative_routes: &[
302 "A future profile MAY anchor provenance on an observable — e.g. CMB last \
303 scattering at z = 1089.9 +/- 0.4 — and derive the offset to the datum \
304 through ucal-cosmo in ticks, removing the Julian year and the Earth date \
305 from the chain. This improves auditability, not exactness: measurement \
306 yields a window and a datum is a point, so any route terminates in a \
307 stipulation (Rule Q.2). See GE-6.",
308 ],
309 };
310}
311
312impl Profile for UC1 {
313 const TAG: &'static str = "UC1";
314 const FRAME: Frame = Frame::FlrwComoving;
315
316 fn beat() -> Ticks {
317 crate::backend::konst(uc1::consts::BEAT_DEC)
318 }
319
320 fn origin_offset() -> Ticks {
321 crate::backend::konst(uc1::consts::ORIGIN_OFFSET_DEC)
322 }
323
324 fn domain_max() -> Ticks {
325 <Ticks as TickInt>::domain_max()
326 }
327
328 fn bridge() -> Bridge {
329 Bridge {
330 name: "second",
331 ticks: crate::backend::konst(uc1::consts::SECOND_DEC),
332 divisibility: uc1::consts::SECOND_DIVISIBILITY,
333 epoch_label: "0000-01-01T00:00:00.000 TT, proleptic Gregorian, \
334 astronomical year numbering",
335 }
336 }
337
338 fn big_bang_claim() -> SignedWindow {
339 SignedWindow::symmetric(Delta::from_ticks(crate::backend::konst(
340 uc1::consts::BIG_BANG_CLAIM_HALFWIDTH_DEC,
341 )))
342 }
343
344 fn big_bang_claim_citation() -> Citation {
345 uc1::PLANCK_2018
346 }
347
348 fn datum_provenance() -> Result<&'static Provenance> {
349 Ok(&uc1::PROVENANCE)
350 }
351}
352
353#[cfg(test)]
359#[derive(Clone, Copy, PartialEq, Eq, Debug)]
360pub struct ProfileWithoutProvenance;
361
362#[cfg(test)]
363impl Profile for ProfileWithoutProvenance {
364 const TAG: &'static str = "TEST-NOPROV";
365 const FRAME: Frame = Frame::FlrwComoving;
366 fn beat() -> Ticks {
367 UC1::beat()
368 }
369 fn origin_offset() -> Ticks {
370 UC1::origin_offset()
371 }
372 fn domain_max() -> Ticks {
373 UC1::domain_max()
374 }
375 fn bridge() -> Bridge {
376 UC1::bridge()
377 }
378 fn big_bang_claim() -> SignedWindow {
379 UC1::big_bang_claim()
380 }
381 fn big_bang_claim_citation() -> Citation {
382 UC1::big_bang_claim_citation()
383 }
384 fn datum_provenance() -> Result<&'static Provenance> {
385 Err(crate::error::TimeError::new(crate::error::Code::E0013))
386 }
387}
388
389pub fn base5_valuation(ticks: &Ticks) -> u32 {
394 if ticks.is_zero_ticks() {
395 return 0;
396 }
397 let five = <Ticks as TickInt>::from_u64(5);
398 let mut n = ticks.clone();
399 let mut k = 0u32;
400 loop {
401 let (q, r) = n.quot_rem(&five);
402 if !r.is_zero_ticks() {
403 return k;
404 }
405 n = q;
406 k += 1;
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::error::Code;
414 use crate::tier::Tier;
415 use crate::value::Sign;
416
417 fn dec(s: &str) -> Ticks {
418 <Ticks as TickInt>::from_dec_str(s).unwrap()
419 }
420
421 #[test]
422 fn beat_is_five_to_the_sixtieth() {
423 assert_eq!(UC1::beat(), <Ticks as TickInt>::pow5(60).unwrap());
424 assert_eq!(UC1::beat(), Tier::BEAT.ticks());
425 }
426
427 #[test]
428 fn origin_offset_is_a_whole_number_of_beats() {
429 let (q, r) = UC1::origin_offset().quot_rem(&UC1::beat());
430 assert!(r.is_zero_ticks(), "the datum must be a whole beat count (§2.2)");
431 assert_eq!(q, dec(uc1::consts::ORIGIN_OFFSET_BEATS_DEC));
432 }
433
434 #[test]
435 fn provenance_chain_reaches_the_declared_origin_offset() {
436 let julian_year = <Ticks as TickInt>::from_u64(31_557_600);
440 let age_s = dec("13787")
441 .try_mul(&<Ticks as TickInt>::pow5(6).unwrap())
442 .and_then(|v| v.try_mul(&<Ticks as TickInt>::from_u64(2u64.pow(6))))
443 .and_then(|v| v.try_mul(&julian_year))
444 .unwrap();
445 assert_eq!(age_s, dec("435084631200000000"));
447
448 let second = UC1::bridge().ticks;
449 let age_ticks = age_s.try_mul(&second).unwrap();
450
451 let (q, r) = age_ticks.quot_rem(&UC1::beat());
453 let twice = r.try_add(&r).unwrap();
454 let beats = match twice.cmp(&UC1::beat()) {
455 core::cmp::Ordering::Greater => q.try_add(&<Ticks as TickInt>::one()).unwrap(),
456 core::cmp::Ordering::Less => q,
457 core::cmp::Ordering::Equal if q.is_odd() => {
458 q.try_add(&<Ticks as TickInt>::one()).unwrap()
459 }
460 core::cmp::Ordering::Equal => q,
461 };
462 assert_eq!(beats, dec(uc1::consts::ORIGIN_OFFSET_BEATS_DEC));
463
464 let oo = beats.try_mul(&UC1::beat()).unwrap();
465 assert_eq!(oo, UC1::origin_offset());
466
467 assert!(oo < age_ticks);
469 let residual = age_ticks.try_sub(&oo).unwrap();
470 assert_eq!(
471 residual,
472 dec("318856914364362819469533860683441162109375")
473 );
474 let rec = UC1::datum_provenance().unwrap().rounding;
475 assert_eq!(
476 rec.residual_ticks,
477 "-318856914364362819469533860683441162109375"
478 );
479 assert_eq!(rec.mode, "half_even");
480 }
481
482 #[test]
483 fn alignment_invariants_hold() {
484 let second = UC1::bridge().ticks;
486 assert_eq!(base5_valuation(&second), 30);
487 assert_eq!(UC1::bridge().divisibility, 30);
488
489 let ten9 = <Ticks as TickInt>::from_u64(1_000_000_000);
490 let (nanosecond, r) = second.quot_rem(&ten9);
491 assert!(r.is_zero_ticks(), "SECOND must divide exactly by 10^9");
492 assert_eq!(base5_valuation(&nanosecond), 21);
493
494 assert_eq!(base5_valuation(&UC1::origin_offset()), 61);
497 assert!(base5_valuation(&UC1::origin_offset()) >= 60);
498
499 for n in 1..64u64 {
501 let t = UC1::origin_offset()
502 .try_add(&second.try_mul(&<Ticks as TickInt>::from_u64(n)).unwrap())
503 .unwrap();
504 assert!(base5_valuation(&t) >= 30, "n = {n}");
505 let t = UC1::origin_offset()
506 .try_add(
507 &nanosecond
508 .try_mul(&<Ticks as TickInt>::from_u64(n))
509 .unwrap(),
510 )
511 .unwrap();
512 assert!(base5_valuation(&t) >= 21, "n = {n}");
513 }
514 }
515
516 #[test]
517 fn origin_offset_structure_matches_appendix_a() {
518 let oo = UC1::origin_offset();
519 assert_eq!(oo.bit_len(), 203);
520 #[cfg(feature = "alloc")]
521 assert_eq!(oo.to_radix_string(5).len(), 88);
522 }
523
524 #[test]
525 fn big_bang_claim_is_symmetric_and_inert() {
526 let claim = UC1::big_bang_claim();
527 assert_eq!(claim.lo().sign(), Sign::Negative);
528 assert_eq!(claim.hi().sign(), Sign::Positive);
529 assert_eq!(claim.lo().magnitude(), claim.hi().magnitude());
530 let expected = <Ticks as TickInt>::from_u64(20)
532 .try_mul(&dec("1000000"))
533 .and_then(|v| v.try_mul(&<Ticks as TickInt>::from_u64(31_557_600)))
534 .and_then(|v| v.try_mul(&UC1::bridge().ticks))
535 .unwrap();
536 assert_eq!(claim.hi().magnitude().ticks(), &expected);
537 }
538
539 #[test]
540 fn missing_provenance_is_e0013() {
541 let err = ProfileWithoutProvenance::datum_provenance().unwrap_err();
543 assert_eq!(err.code, Code::E0013);
544 assert_eq!(err.code.exit_code(), 6);
545 assert!(UC1::datum_provenance().is_ok());
546 }
547
548 #[test]
549 fn datum_statement_makes_no_measurement_claim() {
550 let s = UC1::datum_statement().to_lowercase();
553 assert!(s.contains("stipulated"));
554 for forbidden in [
555 "creation of the universe",
556 "age of the universe is",
557 "measured",
558 "observed",
559 "big bang occurred",
560 ] {
561 assert!(!s.contains(forbidden), "datum statement claims too much: {forbidden}");
562 }
563 }
564
565 #[test]
566 fn frame_is_declared() {
567 assert_eq!(UC1::FRAME, Frame::FlrwComoving);
569 assert!(UC1::FRAME.describe().contains("FLRW"));
570 }
571
572 #[test]
573 fn bridge_is_the_only_foreign_unit_door() {
574 let b = UC1::bridge();
575 assert_eq!(b.name, "second");
576 assert_eq!(b.ticks, dec(uc1::consts::SECOND_DEC));
578 let p5 = <Ticks as TickInt>::pow5(b.divisibility).unwrap();
580 assert!(b.ticks.quot_rem(&p5).1.is_zero_ticks());
581 }
582}