use crate::backend::{TickInt, Ticks};
use crate::error::Result;
use crate::tier::TierTable;
use crate::value::{Delta, Instant, SignedWindow};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum Frame {
FlrwComoving,
}
impl Frame {
pub const fn describe(self) -> &'static str {
match self {
Frame::FlrwComoving => "FLRW comoving (cosmological time, CMB rest frame)",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Citation {
pub source: &'static str,
pub locator: Option<&'static str>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct MeasuredValue {
pub verbatim: &'static str,
pub unit: &'static str,
pub quantity: &'static str,
pub uncertainty: Option<&'static str>,
pub citation: Citation,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct RoundingRecord {
pub to: &'static str,
pub mode: &'static str,
pub residual_ticks: &'static str,
pub residual_rendered: &'static str,
pub rationale: &'static str,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Provenance {
pub input: MeasuredValue,
pub unit_defs: &'static [(&'static str, &'static str)],
pub chain: &'static [&'static str],
pub rounding: RoundingRecord,
pub earth_dependency: &'static str,
pub alternative_routes: &'static [&'static str],
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Bridge {
pub name: &'static str,
pub ticks: Ticks,
pub divisibility: u32,
pub epoch_label: &'static str,
}
pub trait Profile: 'static + Copy + Clone + PartialEq + Eq + core::fmt::Debug {
const TAG: &'static str;
const FRAME: Frame;
fn beat() -> Ticks;
fn origin_offset() -> Ticks;
fn domain_max() -> Ticks;
fn bridge() -> Bridge;
fn tiers() -> TierTable {
TierTable::build()
}
fn big_bang_claim() -> SignedWindow;
fn big_bang_claim_citation() -> Citation;
fn datum_provenance() -> Result<&'static Provenance>;
fn datum_statement() -> &'static str {
"tick 0 is a stipulated reference point, conventionally identified with \
the FLRW t→0 limit"
}
fn datum() -> Instant<Self>
where
Self: Sized,
{
Instant::zero()
}
fn bridge_epoch() -> Result<Instant<Self>>
where
Self: Sized,
{
Instant::from_ticks(Self::origin_offset())
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct UC1;
pub mod uc1 {
pub mod consts {
pub const BEAT_DEC: &str = "867361737988403547205962240695953369140625";
pub const ORIGIN_OFFSET_DEC: &str =
"8070204002895596515944343085635637180530466139316558837890625";
pub const ORIGIN_OFFSET_BEATS_DEC: &str = "9304311741502590385";
pub const SECOND_DEC: &str = "18548584399861000000000000000000000000000000";
pub const SECOND_DIVISIBILITY: u32 = 30;
pub(in crate::profile) const BIG_BANG_CLAIM_HALFWIDTH_DEC: &str =
"11706976141141069872000000000000000000000000000000000000000";
}
use super::{Citation, MeasuredValue, Provenance, RoundingRecord};
pub const PLANCK_2018: Citation = Citation {
source: "Planck 2018 results VI: Cosmological parameters, A&A 641, A6 (2020)",
locator: Some("doi:10.1051/0004-6361/201833910"),
};
pub static PROVENANCE: Provenance = Provenance {
input: MeasuredValue {
verbatim: "13.787",
unit: "Gyr",
quantity: "age_of_universe",
uncertainty: Some("0.020 Gyr"),
citation: PLANCK_2018,
},
unit_defs: &[(
"Gyr",
"10^9 x 31 557 600 s (Julian years, exact by definition)",
)],
chain: &[
"AGE_s = 13 787 000 000 x 31 557 600 = 435 084 631 200 000 000 s (exact)",
"AGE_ticks = AGE_s x SECOND = \
8070204002895596516263200000000000000000000000000000000000000 (exact)",
"beats = round_half_even(AGE_ticks / BEAT) = 9 304 311 741 502 590 385",
"ORIGIN_OFFSET = beats x BEAT = \
8070204002895596515944343085635637180530466139316558837890625",
],
rounding: RoundingRecord {
to: "BEAT",
mode: "half_even",
residual_ticks: "-318856914364362819469533860683441162109375",
residual_rendered: "-0.017190364 s",
rationale: "a whole-beat datum makes all sub-beat digits of the bridge \
epoch zero (§2.4)",
},
earth_dependency:
"The input arrives in Julian years and the bridge anchor is an Earth \
calendar date. Both are metrology (Rule Y). Neither appears in any \
computation: ORIGIN_OFFSET is a declared integer of ticks.",
alternative_routes: &[
"A future profile MAY anchor provenance on an observable — e.g. CMB last \
scattering at z = 1089.9 +/- 0.4 — and derive the offset to the datum \
through ucal-cosmo in ticks, removing the Julian year and the Earth date \
from the chain. This improves auditability, not exactness: measurement \
yields a window and a datum is a point, so any route terminates in a \
stipulation (Rule Q.2). See GE-6.",
],
};
}
impl Profile for UC1 {
const TAG: &'static str = "UC1";
const FRAME: Frame = Frame::FlrwComoving;
fn beat() -> Ticks {
crate::backend::konst(uc1::consts::BEAT_DEC)
}
fn origin_offset() -> Ticks {
crate::backend::konst(uc1::consts::ORIGIN_OFFSET_DEC)
}
fn domain_max() -> Ticks {
<Ticks as TickInt>::domain_max()
}
fn bridge() -> Bridge {
Bridge {
name: "second",
ticks: crate::backend::konst(uc1::consts::SECOND_DEC),
divisibility: uc1::consts::SECOND_DIVISIBILITY,
epoch_label: "0000-01-01T00:00:00.000 TT, proleptic Gregorian, \
astronomical year numbering",
}
}
fn big_bang_claim() -> SignedWindow {
SignedWindow::symmetric(Delta::from_ticks(crate::backend::konst(
uc1::consts::BIG_BANG_CLAIM_HALFWIDTH_DEC,
)))
}
fn big_bang_claim_citation() -> Citation {
uc1::PLANCK_2018
}
fn datum_provenance() -> Result<&'static Provenance> {
Ok(&uc1::PROVENANCE)
}
}
#[cfg(test)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ProfileWithoutProvenance;
#[cfg(test)]
impl Profile for ProfileWithoutProvenance {
const TAG: &'static str = "TEST-NOPROV";
const FRAME: Frame = Frame::FlrwComoving;
fn beat() -> Ticks {
UC1::beat()
}
fn origin_offset() -> Ticks {
UC1::origin_offset()
}
fn domain_max() -> Ticks {
UC1::domain_max()
}
fn bridge() -> Bridge {
UC1::bridge()
}
fn big_bang_claim() -> SignedWindow {
UC1::big_bang_claim()
}
fn big_bang_claim_citation() -> Citation {
UC1::big_bang_claim_citation()
}
fn datum_provenance() -> Result<&'static Provenance> {
Err(crate::error::TimeError::new(crate::error::Code::E0013))
}
}
pub fn base5_valuation(ticks: &Ticks) -> u32 {
if ticks.is_zero_ticks() {
return 0;
}
let five = <Ticks as TickInt>::from_u64(5);
let mut n = ticks.clone();
let mut k = 0u32;
loop {
let (q, r) = n.quot_rem(&five);
if !r.is_zero_ticks() {
return k;
}
n = q;
k += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Code;
use crate::tier::Tier;
use crate::value::Sign;
fn dec(s: &str) -> Ticks {
<Ticks as TickInt>::from_dec_str(s).unwrap()
}
#[test]
fn beat_is_five_to_the_sixtieth() {
assert_eq!(UC1::beat(), <Ticks as TickInt>::pow5(60).unwrap());
assert_eq!(UC1::beat(), Tier::BEAT.ticks());
}
#[test]
fn origin_offset_is_a_whole_number_of_beats() {
let (q, r) = UC1::origin_offset().quot_rem(&UC1::beat());
assert!(r.is_zero_ticks(), "the datum must be a whole beat count (§2.2)");
assert_eq!(q, dec(uc1::consts::ORIGIN_OFFSET_BEATS_DEC));
}
#[test]
fn provenance_chain_reaches_the_declared_origin_offset() {
let julian_year = <Ticks as TickInt>::from_u64(31_557_600);
let age_s = dec("13787")
.try_mul(&<Ticks as TickInt>::pow5(6).unwrap())
.and_then(|v| v.try_mul(&<Ticks as TickInt>::from_u64(2u64.pow(6))))
.and_then(|v| v.try_mul(&julian_year))
.unwrap();
assert_eq!(age_s, dec("435084631200000000"));
let second = UC1::bridge().ticks;
let age_ticks = age_s.try_mul(&second).unwrap();
let (q, r) = age_ticks.quot_rem(&UC1::beat());
let twice = r.try_add(&r).unwrap();
let beats = match twice.cmp(&UC1::beat()) {
core::cmp::Ordering::Greater => q.try_add(&<Ticks as TickInt>::one()).unwrap(),
core::cmp::Ordering::Less => q,
core::cmp::Ordering::Equal if q.is_odd() => {
q.try_add(&<Ticks as TickInt>::one()).unwrap()
}
core::cmp::Ordering::Equal => q,
};
assert_eq!(beats, dec(uc1::consts::ORIGIN_OFFSET_BEATS_DEC));
let oo = beats.try_mul(&UC1::beat()).unwrap();
assert_eq!(oo, UC1::origin_offset());
assert!(oo < age_ticks);
let residual = age_ticks.try_sub(&oo).unwrap();
assert_eq!(
residual,
dec("318856914364362819469533860683441162109375")
);
let rec = UC1::datum_provenance().unwrap().rounding;
assert_eq!(
rec.residual_ticks,
"-318856914364362819469533860683441162109375"
);
assert_eq!(rec.mode, "half_even");
}
#[test]
fn alignment_invariants_hold() {
let second = UC1::bridge().ticks;
assert_eq!(base5_valuation(&second), 30);
assert_eq!(UC1::bridge().divisibility, 30);
let ten9 = <Ticks as TickInt>::from_u64(1_000_000_000);
let (nanosecond, r) = second.quot_rem(&ten9);
assert!(r.is_zero_ticks(), "SECOND must divide exactly by 10^9");
assert_eq!(base5_valuation(&nanosecond), 21);
assert_eq!(base5_valuation(&UC1::origin_offset()), 61);
assert!(base5_valuation(&UC1::origin_offset()) >= 60);
for n in 1..64u64 {
let t = UC1::origin_offset()
.try_add(&second.try_mul(&<Ticks as TickInt>::from_u64(n)).unwrap())
.unwrap();
assert!(base5_valuation(&t) >= 30, "n = {n}");
let t = UC1::origin_offset()
.try_add(
&nanosecond
.try_mul(&<Ticks as TickInt>::from_u64(n))
.unwrap(),
)
.unwrap();
assert!(base5_valuation(&t) >= 21, "n = {n}");
}
}
#[test]
fn origin_offset_structure_matches_appendix_a() {
let oo = UC1::origin_offset();
assert_eq!(oo.bit_len(), 203);
#[cfg(feature = "alloc")]
assert_eq!(oo.to_radix_string(5).len(), 88);
}
#[test]
fn big_bang_claim_is_symmetric_and_inert() {
let claim = UC1::big_bang_claim();
assert_eq!(claim.lo().sign(), Sign::Negative);
assert_eq!(claim.hi().sign(), Sign::Positive);
assert_eq!(claim.lo().magnitude(), claim.hi().magnitude());
let expected = <Ticks as TickInt>::from_u64(20)
.try_mul(&dec("1000000"))
.and_then(|v| v.try_mul(&<Ticks as TickInt>::from_u64(31_557_600)))
.and_then(|v| v.try_mul(&UC1::bridge().ticks))
.unwrap();
assert_eq!(claim.hi().magnitude().ticks(), &expected);
}
#[test]
fn missing_provenance_is_e0013() {
let err = ProfileWithoutProvenance::datum_provenance().unwrap_err();
assert_eq!(err.code, Code::E0013);
assert_eq!(err.code.exit_code(), 6);
assert!(UC1::datum_provenance().is_ok());
}
#[test]
fn datum_statement_makes_no_measurement_claim() {
let s = UC1::datum_statement().to_lowercase();
assert!(s.contains("stipulated"));
for forbidden in [
"creation of the universe",
"age of the universe is",
"measured",
"observed",
"big bang occurred",
] {
assert!(!s.contains(forbidden), "datum statement claims too much: {forbidden}");
}
}
#[test]
fn frame_is_declared() {
assert_eq!(UC1::FRAME, Frame::FlrwComoving);
assert!(UC1::FRAME.describe().contains("FLRW"));
}
#[test]
fn bridge_is_the_only_foreign_unit_door() {
let b = UC1::bridge();
assert_eq!(b.name, "second");
assert_eq!(b.ticks, dec(uc1::consts::SECOND_DEC));
let p5 = <Ticks as TickInt>::pow5(b.divisibility).unwrap();
assert!(b.ticks.quot_rem(&p5).1.is_zero_ticks());
}
}