Skip to main content

ucal_core/
profile.rs

1//! Profiles (§2, Rule P) and the datum (Rule Q).
2//!
3//! A profile is an immutable, named constant set fixing the datum and the tick.
4//! `UC-1` is normative.
5//!
6//! Three things about this module are load-bearing:
7//!
8//! - **Rule P.** Instants are parameterised by profile at the type level, so
9//!   cross-profile arithmetic does not compile. Every serialised form carries
10//!   [`Profile::TAG`].
11//! - **Rule Q.1.** Tick 0 is a **stipulated** datum: exact by declaration,
12//!   unrevisable within a profile. Nothing here describes it as measured,
13//!   derived, observed, or as the creation of anything. The permitted phrasing is
14//!   *the datum, conventionally identified with the FLRW t→0 limit*.
15//! - **Rule Q.3 / Q.4.** [`Profile::big_bang_claim`] returns a [`SignedWindow`],
16//!   which has no arithmetic. [`Profile::datum_provenance`] returns
17//!   machine-readable data, not prose, so the chain can be re-executed and
18//!   audited — the P0 harness does exactly that.
19//!
20//! ### Deviation from §13's literal signatures
21//!
22//! §13 writes the profile constants as `const BEAT: Ticks` and
23//! `fn bridge() -> &'static Bridge`. Associated `const`s and `static`s cannot hold
24//! a heap value, so on the `bigint` backend those forms are impossible. The trait
25//! therefore uses by-value functions throughout. On the default backend the
26//! underlying literals remain `const` — see [`uc1::consts`] — which is what §3.3
27//! actually requires.
28
29use crate::backend::{TickInt, Ticks};
30use crate::error::Result;
31use crate::tier::TierTable;
32use crate::value::{Delta, Instant, SignedWindow};
33
34/// The reference frame a profile declares (Rule F).
35///
36/// Implementations MUST NOT convert between frames and MUST NOT claim
37/// observer-independence (N2).
38#[derive(Clone, Copy, PartialEq, Eq, Debug)]
39#[non_exhaustive]
40pub enum Frame {
41    /// Proper time along a comoving worldline in an FLRW frame — cosmological
42    /// time in the CMB rest frame. The frame in which "the universe is 13.787 Gyr
43    /// old" is a meaningful statement (§1.1).
44    FlrwComoving,
45}
46
47impl Frame {
48    /// Human description, for `ucal doctor` and `ucal datum`.
49    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/// A literature citation.
57#[derive(Clone, Copy, PartialEq, Eq, Debug)]
58pub struct Citation {
59    /// Full source reference.
60    pub source: &'static str,
61    /// DOI, bibcode or URL, where one exists.
62    pub locator: Option<&'static str>,
63}
64
65/// A foreign-unit value recorded verbatim with its unit (Rule Y.1).
66///
67/// Rule Y concedes metrology and nothing else: empirical inputs arrive in foreign
68/// units because that is how measurement works, but the *declared constant* — the
69/// value the specification and the code use — is always the tick value.
70#[derive(Clone, Copy, PartialEq, Eq, Debug)]
71pub struct MeasuredValue {
72    /// The value exactly as published, as a string. Never parsed into a float.
73    pub verbatim: &'static str,
74    /// The unit the published value is in.
75    pub unit: &'static str,
76    /// What quantity this is.
77    pub quantity: &'static str,
78    /// Published uncertainty, verbatim, where one is quoted.
79    pub uncertainty: Option<&'static str>,
80    /// Where it came from.
81    pub citation: Citation,
82}
83
84/// The rounding applied when converting an empirical input into a declared
85/// constant (Rule Q.4).
86#[derive(Clone, Copy, PartialEq, Eq, Debug)]
87pub struct RoundingRecord {
88    /// What the value was rounded to, e.g. `"BEAT"`.
89    pub to: &'static str,
90    /// The mode, e.g. `"half_even"`.
91    pub mode: &'static str,
92    /// Signed residual in ticks, as a decimal string.
93    pub residual_ticks: &'static str,
94    /// The residual rendered in a foreign unit, for human orientation only.
95    pub residual_rendered: &'static str,
96    /// Why this rounding target was chosen.
97    pub rationale: &'static str,
98}
99
100/// A machine-readable provenance record for a profile's datum (Rule Q.4).
101///
102/// Provenance is **data, not prose** — auditable, re-executable, and replaceable
103/// without editing specification text. Absence is `UCAL-E0013`.
104#[derive(Clone, Copy, PartialEq, Eq, Debug)]
105pub struct Provenance {
106    /// The empirical input, verbatim, with unit and citation.
107    pub input: MeasuredValue,
108    /// Definitions of any units the chain uses, so the chain is self-contained.
109    pub unit_defs: &'static [(&'static str, &'static str)],
110    /// The exact conversion chain, step by step. Every step must be reproducible
111    /// by exact integer arithmetic; the UC-P0 harness re-executes all of them.
112    pub chain: &'static [&'static str],
113    /// The rounding applied at the end of the chain.
114    pub rounding: RoundingRecord,
115    /// An explicit statement of what Earth-derived quantities the chain touches
116    /// and why they do not reach any computation (Rule Y, F12).
117    pub earth_dependency: &'static str,
118    /// Routes a future profile might take instead. Documented so the choice is
119    /// visible rather than implicit (D-21, GE-6).
120    pub alternative_routes: &'static [&'static str],
121}
122
123/// A bridge constant: a profile constant whose sole purpose is conversion to a
124/// foreign unit system (Rule A.3).
125///
126/// Profile `UC-1` declares exactly one. A bridge constant MUST be an exact
127/// integer number of ticks, so conversion *into* absolute time is multiplication
128/// and never requires rounding (Rule A.4).
129#[derive(Clone, PartialEq, Eq, Debug)]
130pub struct Bridge {
131    /// The foreign unit's name, e.g. `"second"`.
132    pub name: &'static str,
133    /// Its exact length in ticks.
134    pub ticks: Ticks,
135    /// The largest `n` such that `5^n` divides `ticks`. This is what makes the
136    /// §2.4 alignment invariants hold and decimal subdivisions exact (D-3).
137    pub divisibility: u32,
138    /// What the bridge's zero point is, as a human label. This anchors the
139    /// *bridge*, not the datum and not any calendar (§2.1).
140    pub epoch_label: &'static str,
141}
142
143/// An immutable, named constant set fixing a datum and a tick (§2.1).
144pub trait Profile: 'static + Copy + Clone + PartialEq + Eq + core::fmt::Debug {
145    /// The profile tag carried by every serialised form (Rule P).
146    const TAG: &'static str;
147
148    /// The declared frame (Rule F).
149    const FRAME: Frame;
150
151    /// The base tier in ticks: `5^60` for `UC-1` (D-2).
152    fn beat() -> Ticks;
153
154    /// The tick value of the bridge epoch, i.e. how far the bridge's zero point
155    /// lies after the datum.
156    fn origin_offset() -> Ticks;
157
158    /// The largest representable tick value.
159    fn domain_max() -> Ticks;
160
161    /// The profile's single door to foreign units (Rule A.3).
162    fn bridge() -> Bridge;
163
164    /// The materialised tier grid.
165    fn tiers() -> TierTable {
166        TierTable::build()
167    }
168
169    /// The signed tick window within which this profile asserts the FLRW t→0
170    /// limit lies, relative to its own datum (Rule Q.3).
171    ///
172    /// **Metadata only.** No arithmetic operation may consume this. It exists so
173    /// that a user learns the physical *interpretation* is uncertain while the
174    /// *arithmetic* is exact. The return type has no operators and no conversion
175    /// into [`Delta`], [`Instant`] or `Window`, which is what makes misuse a
176    /// compile error rather than a runtime `UCAL-E0025`.
177    fn big_bang_claim() -> SignedWindow;
178
179    /// Citation for the `big_bang_claim` window.
180    fn big_bang_claim_citation() -> Citation;
181
182    /// The datum provenance record (Rule Q.4). Absence is `UCAL-E0013`, which is
183    /// why this returns a `Result` rather than an `Option`.
184    fn datum_provenance() -> Result<&'static Provenance>;
185
186    /// The datum statement, in the phrasing Rule Q.1 permits.
187    ///
188    /// Implementations MUST NOT describe tick 0 as measured, derived, observed, or
189    /// as "the creation of the universe". A documentation lint enforces this  // ucal-lint-allow(datum-no-overclaim): mention, not use
190    /// (§21.3-5), and `ucal datum` prints this string verbatim (§19.2).
191    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    /// The datum itself.
197    fn datum() -> Instant<Self>
198    where
199        Self: Sized,
200    {
201        Instant::zero()
202    }
203
204    /// The bridge epoch as an instant.
205    fn bridge_epoch() -> Result<Instant<Self>>
206    where
207        Self: Sized,
208    {
209        Instant::from_ticks(Self::origin_offset())
210    }
211}
212
213/// Profile `UC-1` — normative (§2, Appendix A).
214#[derive(Clone, Copy, PartialEq, Eq, Debug)]
215pub struct UC1;
216
217/// The `UC-1` constants.
218pub mod uc1 {
219    /// The declared constants, `const` on the default backend (§3.3).
220    ///
221    /// These decimal literals are the only transcribed constants in the crate,
222    /// and the UC-P0 harness reproduces every one of them by two independent
223    /// exact-integer routes. `§21.3` forbids any hand-transcribed constant the
224    /// harness does not reproduce.
225    pub mod consts {
226        /// `BEAT = 5^60`.
227        pub const BEAT_DEC: &str = "867361737988403547205962240695953369140625";
228
229        /// `ORIGIN_OFFSET = 9 304 311 741 502 590 385 x BEAT`.
230        ///
231        /// 203 bits; 88 base-5 digits; **61** trailing base-5 zeros. (Appendix A
232        /// annotates 62; the exact valuation is 61, because
233        /// `9 304 311 741 502 590 385` contributes exactly one factor of five.
234        /// See `spec/SPEC-DELTAS.md` D-A2. §2.4 requires at least 60, so the
235        /// correction has no behavioural effect.)
236        pub const ORIGIN_OFFSET_DEC: &str =
237            "8070204002895596515944343085635637180530466139316558837890625";
238
239        /// The beat count of `ORIGIN_OFFSET`, retained so the datum's whole-beat
240        /// structure is checkable without factoring.
241        pub const ORIGIN_OFFSET_BEATS_DEC: &str = "9304311741502590385";
242
243        /// `SECOND = 18 548 584 399 861 x 10^30` ticks — the sole bridge constant.
244        pub const SECOND_DEC: &str = "18548584399861000000000000000000000000000000";
245
246        /// The largest `n` with `5^n | SECOND`. D-3 chooses the nearest multiple
247        /// of `10^30` to the measured reciprocal Planck time precisely so that
248        /// every decimal SI subdivision down to `10^-30` s is exact.
249        pub const SECOND_DIVISIBILITY: u32 = 30;
250
251        /// Half-width of `BIG_BANG_CLAIM`, in ticks: `+/- 0.020 Gyr`.
252        ///
253        /// Private to the profile: reachable only through
254        /// [`super::super::Profile::big_bang_claim`], which returns an inert
255        /// [`crate::value::SignedWindow`] (Rule Q.3, §3.3).
256        pub(in crate::profile) const BIG_BANG_CLAIM_HALFWIDTH_DEC: &str =
257            "11706976141141069872000000000000000000000000000000000000000";
258    }
259
260    use super::{Citation, MeasuredValue, Provenance, RoundingRecord};
261
262    /// Planck 2018 cosmological parameters.
263    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    /// The `UC-1` datum provenance record (§2.2, Rule Q.4).
269    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/// A profile with no provenance record, used only to prove `UCAL-E0013` fires.
354///
355/// Rule Q.4 makes provenance mandatory. A conforming implementation must reject
356/// such a profile rather than treat the absence as a default, so the failure path
357/// needs something to exercise it.
358#[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
389/// The base-5 valuation of a tick count: how many trailing base-5 digits are zero.
390///
391/// This is the quantity the §2.4 alignment invariants are stated in, and the one
392/// that decides how many groups a tick-exact rendering needs.
393pub 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        // §21.3-2: the chain must re-execute to ORIGIN_OFFSET with the stated
437        // residual. The full two-route re-execution lives in the UC-P0 harness;
438        // this asserts the endpoint so the library alone cannot drift from it.
439        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        // 10^6 = 5^6 x 2^6, kept factored so the test needs no decimal literal.
446        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        // round half even
452        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        // The residual is negative: the rounded datum precedes the unrounded age.
468        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        // §2.4 / §21.3-1
485        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        // SI_EPOCH is zero in all tiers below T0, i.e. v5 >= 60. The exact
495        // valuation is 61 (delta D-A2), not the 62 Appendix A annotates.
496        assert_eq!(base5_valuation(&UC1::origin_offset()), 61);
497        assert!(base5_valuation(&UC1::origin_offset()) >= 60);
498
499        // Every whole SI second keeps at least 30 trailing base-5 zeros.
500        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        // 0.020 Gyr exactly: 20 x 10^6 Julian years x SECOND.
531        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        // Rule Q.4: absence is an error, never a default.
542        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        // Rule Q.1 / §21.3-5. The documentation lint covers the whole tree; this
551        // pins the one string that gets printed to users.
552        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        // Rule F: every profile must declare its frame.
568        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        // Rule A.4: exact integer of ticks, so conversion in is multiplication.
577        assert_eq!(b.ticks, dec(uc1::consts::SECOND_DEC));
578        // D-3: divisible by 10^30, hence by 5^30.
579        let p5 = <Ticks as TickInt>::pow5(b.divisibility).unwrap();
580        assert!(b.ticks.quot_rem(&p5).1.is_zero_ticks());
581    }
582}