1348 lines
70 KiB
Rust
1348 lines
70 KiB
Rust
//! # The Cosmic Timeline
|
||
//!
|
||
//! The Observatory's horizontal bar from Creation to Annihilation: the whole
|
||
//! of the calendar's representable domain, some 10^154 Planck times of it,
|
||
//! laid across a single row of glyph cells and offered up for clicking.
|
||
//! Every click selects one segment of the bar and makes that segment the
|
||
//! whole bar; the view narrows by a factor of the segment count at each
|
||
//! descent, the descents compose, and they may be repeated until the span
|
||
//! beneath the pointer is narrower than a hundred Planck times, at which
|
||
//! point the Observatory declines to descend further, there being nothing
|
||
//! finer to show and no wish to pretend otherwise.
|
||
//!
|
||
//! The events of cosmic history are not painted on the bar all at once.
|
||
//! Each is withheld until the view has narrowed enough to give it room, so
|
||
//! that the widest views list the first stars and the death of the Sun, a
|
||
//! window a billion years wide lists the Snowball and the Cambrian, and a
|
||
//! window seventeen years wide lists the Whisper and the Unix epoch — each
|
||
//! event in the company of its own era, and never more of them than the
|
||
//! panel beneath the bar has lines. The curious observer who drills down is
|
||
//! rewarded. The incurious one sees a bar with a word at either end, which
|
||
//! is also a true statement about time, if a coarse one, and the calendar
|
||
//! has a mask for coarse.
|
||
//!
|
||
//! Every quantity here that is a time is an `I512` or a `U512`, and every
|
||
//! comparison, subtraction and quotient is performed on those integers in
|
||
//! full. The single exemption, tolerated because a GPU cannot be addressed
|
||
//! in any other tongue, is the last line of `TimelineView::position_of`,
|
||
//! which turns an exact ratio into a fraction of a bar's width and confesses
|
||
//! to the sin at length where it is committed.
|
||
|
||
use loic::{
|
||
As, I512, I512_ZERO, LOIC_TO_UNIX_OFFSET_SECONDS, U512, U512_ZERO, annihilation_planck,
|
||
creation_planck,
|
||
};
|
||
use std::str::FromStr;
|
||
|
||
use crate::events::EventId;
|
||
|
||
/// The first nineteen powers of ten, transcribed in full and looked up by
|
||
/// index rather than raised at runtime, so that no exponentiation is ever
|
||
/// entrusted to a floating-point unit, whose powers of ten are exact only
|
||
/// until they are not and which does not say when. Index `n` furnishes
|
||
/// 10^n, exactly, as a `u64`, from 10^0 to 10^18, the largest power of ten
|
||
/// that type can hold; that reaches every count of attoseconds,
|
||
/// femtoseconds, picoseconds, nanoseconds and microseconds that the span
|
||
/// label of `TimelineView::format_span` prints from the table, and it is
|
||
/// the first thing the event table's scientific-notation helpers reach for
|
||
/// before they resign themselves to multiplying by ten one power at a time.
|
||
const POWERS_OF_10: [u64; 19] = [
|
||
1, // 10^0
|
||
10, // 10^1
|
||
100, // 10^2
|
||
1_000, // 10^3
|
||
10_000, // 10^4
|
||
100_000, // 10^5
|
||
1_000_000, // 10^6
|
||
10_000_000, // 10^7
|
||
100_000_000, // 10^8
|
||
1_000_000_000, // 10^9
|
||
10_000_000_000, // 10^10
|
||
100_000_000_000, // 10^11
|
||
1_000_000_000_000, // 10^12
|
||
10_000_000_000_000, // 10^13
|
||
100_000_000_000_000, // 10^14
|
||
1_000_000_000_000_000, // 10^15
|
||
10_000_000_000_000_000, // 10^16
|
||
100_000_000_000_000_000, // 10^17
|
||
1_000_000_000_000_000_000, // 10^18
|
||
];
|
||
|
||
/// Planck times per second, transcribed in decimal: the integer part of
|
||
/// `PLANCK_DENOMINATOR / PLANCK_NUMERATOR`, which is 10^50 divided by
|
||
/// 5,391,247 — forty-four digits of it, approximately 1.855 × 10^43. The
|
||
/// reliquary is parsed once, when the event table is first built, into the
|
||
/// `I512` by which every second in that table is multiplied. It is kept as
|
||
/// a string because a string can be read against its derivation, which is
|
||
/// `derive_constants.sh` at the workspace root, and because a forty-four
|
||
/// digit literal is a thing the eye can check and the compiler cannot be
|
||
/// asked to have opinions about. The fraction of a Planck time that the
|
||
/// integer part leaves off — less than one — is the whole of what any
|
||
/// marker placed by this number is early by, per second of the interval it
|
||
/// was multiplied across.
|
||
const PLANCK_PER_SECOND_DECIMAL: &str = "18548584399861479171701834473545730700151560";
|
||
|
||
/// Planck times per Julian year, transcribed in decimal: the figure above
|
||
/// multiplied by `SECONDS_PER_JULIAN_YEAR`, 31,557,600, which is 365.25
|
||
/// days of 86,400 SI seconds and the only year this calendar acknowledges,
|
||
/// because it is a definition and not a measurement of anything that
|
||
/// wobbles. Fifty-one digits, approximately 5.853 × 10^50; derivation,
|
||
/// `derive_constants.sh` at the workspace root. Every event in the table
|
||
/// dated in years — which is most of them, the universe having been
|
||
/// unhurried for the greater part of its life — is placed by multiplying
|
||
/// its count of years by this integer, exactly, with no intermediate that a
|
||
/// floating-point unit was invited to round.
|
||
const PLANCK_PER_YEAR_DECIMAL: &str = "585348807057068615108897811582366751143102869856000";
|
||
|
||
/// The seconds by which the kernel's atomic clock leads civil time: the ten
|
||
/// that Coordinated Universal Time began with on the first of January 1972,
|
||
/// and the twenty-seven leap seconds inserted since, the last of them at
|
||
/// the close of 2016 — thirty-seven in all, every one of them a negotiation
|
||
/// with a rotating planet. The workspace README, section 1.2, holds all
|
||
/// thirty-seven in the contempt they have earned and discloses, in the same
|
||
/// breath, that the calendar's own epoch is pinned by exactly this many of
|
||
/// them: `CLOCK_TAI` on Linux is `CLOCK_REALTIME` plus this offset, and the
|
||
/// library's Unix offset is stated on that scale. This module needs the
|
||
/// number for the return journey, from the atomic scale the library counts
|
||
/// in to the civil scale the history books use, and takes it once, here,
|
||
/// to state the Whisper as a Unix timestamp.
|
||
const TAI_MINUS_UTC_SECONDS: i64 = 37;
|
||
|
||
/// The Whisper on the Unix scale: 1969-10-30 06:30:41 UTC, which is
|
||
/// 5,419,759 seconds before the Unix epoch and therefore negative, Unix
|
||
/// time not having begun yet. The library states the same instant on the
|
||
/// atomic scale, thirty-seven seconds ahead, as `LOIC_TO_UNIX_OFFSET_SECONDS`,
|
||
/// because `CLOCK_TAI` is what it reads; this module wants the civil number,
|
||
/// because the dates in the event table are civil ones, recorded by people
|
||
/// who did not know what a leap second was and would not have cared. The
|
||
/// relation between the two is one addition, performed here, once, by the
|
||
/// compiler.
|
||
///
|
||
/// A civil date is placed on the timeline exactly where `now()` would place
|
||
/// it on a kernel whose clock read that date with today's offset: its
|
||
/// distance from this constant in seconds, times the Planck times in a
|
||
/// second. That is the only arithmetic the calendar has for turning a
|
||
/// clock's reading into an instant, and the timeline uses it rather than
|
||
/// inventing a second arithmetic that could one day disagree with the
|
||
/// first.
|
||
const WHISPER_UNIX_SECONDS: i64 = -(LOIC_TO_UNIX_OFFSET_SECONDS + TAI_MINUS_UTC_SECONDS);
|
||
|
||
/// A compile-time proof that the civil timestamp of the Whisper, written
|
||
/// down here as every history of the ARPANET would give it, agrees with the
|
||
/// library's atomic offset plus the leap seconds it stands on. The two
|
||
/// arrive by independent roads — one transcribed from the record of the
|
||
/// first transmission, one derived from `CLOCK_TAI`'s arithmetic — and they
|
||
/// are meant to meet at −5,419,759. Were they not to, either the workspace
|
||
/// README's disclosure in section 1.2 or the library's constant would be
|
||
/// wrong, and the crate declines to build until someone says which; a
|
||
/// binary whose epoch is in dispute by thirty-seven seconds is not a
|
||
/// calendar, it is a rumour with a compiler.
|
||
const _WHISPER_UTC_AGREES_WITH_THE_LIBRARY: () = assert!(
|
||
WHISPER_UNIX_SECONDS == -5_419_759,
|
||
"the Whisper's civil second and the library's atomic offset disagree beyond the leap seconds"
|
||
);
|
||
|
||
/// Consult the table for 10^`exp`, and answer 1 — the zeroth power, the
|
||
/// multiplicative identity, the number that changes nothing it touches —
|
||
/// for any exponent the table does not hold, whether negative or past
|
||
/// eighteen, rather than a fraction the `u64` could not carry or an error
|
||
/// the caller could not use. The span label that consults it has already
|
||
/// shifted its exponent into the range of a unit prefix before asking, and
|
||
/// the shift, not this function, is where the label's honesty is decided.
|
||
fn pow10(exp: i32) -> u64 {
|
||
usize::try_from(exp)
|
||
.ok()
|
||
.and_then(|index| POWERS_OF_10.get(index))
|
||
.copied()
|
||
.unwrap_or(1)
|
||
}
|
||
|
||
/// The cell under a pixel offset: the offset divided by the cell's width,
|
||
/// clamped at zero from below and then truncated toward it, which for a
|
||
/// non-negative quotient is the floor, which is the index of the cell the
|
||
/// pixel lies in, which is the whole of the question. Two floating-point
|
||
/// operations are committed here, a division and a `max`, and both are
|
||
/// forgiven: the operands are pixel measurements, a few hundred wide at
|
||
/// most, which an `f32` carries to a precision no pixel can resolve, and
|
||
/// the result is delivered into a `usize` the moment it is known. Nothing
|
||
/// temporal passes through this function. It is a ruler, and a ruler is
|
||
/// permitted to be made of wood.
|
||
#[allow(
|
||
clippy::cast_possible_truncation,
|
||
clippy::cast_sign_loss,
|
||
reason = "the floor of a non-negative quotient is the cell under the pixel; the cut selects it"
|
||
)]
|
||
fn cell_under(offset: f32, cell_width: f32) -> usize {
|
||
(offset / cell_width).max(0.0) as usize
|
||
}
|
||
|
||
/// A window onto the cosmic timeline: the contiguous stretch of
|
||
/// representable time, from `start` to `end` inclusive, that the bar is
|
||
/// currently showing. The full view is the whole domain, Creation to
|
||
/// Annihilation; every narrower view is one segment of some wider one,
|
||
/// reached by clicking, and the Observatory keeps the wider ones on a stack
|
||
/// so that the descent can be retraced one step at a time.
|
||
///
|
||
/// A view is a pair of instants and nothing else. Its width, its order of
|
||
/// magnitude, the position of any instant within it, and the label it
|
||
/// wears are all derived on demand from the two ends — in 512-bit
|
||
/// integers, with the one exception that `position_of` confesses to in
|
||
/// full. Nothing is cached, because nothing here is expensive enough to
|
||
/// deserve the risk of being stale.
|
||
#[derive(Clone)]
|
||
pub struct TimelineView {
|
||
/// The first instant the view shows, in Planck times since the Whisper:
|
||
/// negative for everything before 06:30:41 UTC on 30 October 1969,
|
||
/// which is most of the universe.
|
||
pub start: I512,
|
||
/// The last instant the view shows, in Planck times since the Whisper.
|
||
/// Never less than `start`; the difference between them is the view's
|
||
/// span, which for the full view does not fit in this type at all and
|
||
/// is computed unsigned for that reason.
|
||
pub end: I512,
|
||
}
|
||
|
||
impl TimelineView {
|
||
/// Manifest the widest view there is: `start` at Creation, the Big Bang
|
||
/// as the library locates it some 8.08 × 10^60 Planck times before the
|
||
/// Whisper, and `end` at Annihilation, the last coordinate 512 signed
|
||
/// bits can name, a little past 10^103 years hence. Between them lies
|
||
/// every instant the calendar can represent, which is the same as
|
||
/// saying every instant, since the calendar does not concede that there
|
||
/// are others. This is the view the Observatory opens on and the view
|
||
/// Escape returns it to.
|
||
pub fn full() -> Self {
|
||
Self {
|
||
start: *creation_planck(),
|
||
end: *annihilation_planck(),
|
||
}
|
||
}
|
||
|
||
/// The width of this view in Planck times, furnished as a `U512` because
|
||
/// the widest view is wider than a signed integer can say. Creation is
|
||
/// negative, Annihilation is `I512::MAX`, and their difference — the
|
||
/// library's `total_span`, some 6.7 × 10^153 — exceeds `I512::MAX` by
|
||
/// exactly the age of the universe, which is not a margin one waves
|
||
/// away. So the subtraction is performed on the unsigned bit patterns
|
||
/// and permitted to wrap, and for `end >= start` the wrapped difference
|
||
/// is the exact width at every zoom, including the one at which the
|
||
/// signed subtraction would have overflowed and, in a release build,
|
||
/// said nothing about it. Two's complement is a circle, and a circle has
|
||
/// no edge to fall off. The test `full_span_agrees_with_the_library`
|
||
/// holds this function to the library's own span, bit for bit.
|
||
pub fn span(&self) -> U512 {
|
||
self.end
|
||
.cast_unsigned()
|
||
.wrapping_sub(self.start.cast_unsigned())
|
||
}
|
||
|
||
/// The number of decimal digits in the span: `ilog10` plus one, an
|
||
/// integer stand-in for the view's order of magnitude that costs one
|
||
/// 512-bit logarithm and no floating-point at all. It is the coin in
|
||
/// which this module prices everything — which events are legible at
|
||
/// this zoom, which unit the span label speaks in, whether the
|
||
/// Observatory will consent to zoom further — because a threshold on a
|
||
/// digit count is a threshold on a power of ten, and a power of ten is
|
||
/// the only kind of "about" the calendar is prepared to be.
|
||
///
|
||
/// The full view has 154 digits. A span of zero, which no view has,
|
||
/// would be reported as one digit rather than as a panic; the guard
|
||
/// exists because `ilog10` is honest about the logarithm of nothing,
|
||
/// and this function prefers to be honest about something.
|
||
pub fn span_digits(&self) -> usize {
|
||
let span = self.span();
|
||
if span == U512_ZERO {
|
||
1
|
||
} else {
|
||
usize::try_from(span.ilog10()).map_or(1, |magnitude| magnitude + 1)
|
||
}
|
||
}
|
||
|
||
/// Whether this view is the full one: `start` at Creation and `end` at
|
||
/// Annihilation, both compared exactly, in all 512 bits, because a view
|
||
/// that is merely enormous is not the same thing as a view that is
|
||
/// everything. The Observatory asks this to decide whether the words
|
||
/// beneath the bar's ends read Creation and Annihilation or merely
|
||
/// Start and End, whether to offer the way back, and whether the
|
||
/// breadcrumb of fractions has anything yet to say.
|
||
pub fn is_full(&self) -> bool {
|
||
self.start == *creation_planck() && self.end == *annihilation_planck()
|
||
}
|
||
|
||
/// Descend into one segment of this view: divide the span into
|
||
/// `segment_count` equal parts and make the part numbered
|
||
/// `segment_index`, counting from zero at `start`, the whole of a new
|
||
/// view. Each descent narrows the window by the segment count — some
|
||
/// sixty at the Observatory's width — and the descents compose: forty
|
||
/// of them at fifty-eight segments apiece carry the full span's 154
|
||
/// digits down to about eighty-four, which is the walk the test
|
||
/// `position_is_exact_at_ends_and_midpoint_at_every_zoom` takes.
|
||
///
|
||
/// The division is integer division on the `U512` span, so the segments
|
||
/// are equal to the Planck time and the remainder falls past the last
|
||
/// of them, unshown; a view a few Planck times narrower than the bar
|
||
/// suggested is a smaller lie than a fractional Planck time would be,
|
||
/// and the calendar does not deal in those. The new view runs from
|
||
/// `start + index × segment` to `start + (index + 1) × segment`,
|
||
/// computed in `I512`, which the remark in the body explains is safe.
|
||
///
|
||
/// # Panics
|
||
///
|
||
/// Panics if `segment_index` does not fit an `i64` — a bar more than
|
||
/// nine quintillion cells wide, drawn on a monitor that does not exist —
|
||
/// or if `segment_count` is zero, which is a division by nothing and
|
||
/// the caller's confession that it drew no bar at all.
|
||
pub fn zoom_into(&self, segment_index: usize, segment_count: usize) -> Self {
|
||
let span = self.span();
|
||
let segment_span = span / (segment_count as u64).as_::<U512>();
|
||
|
||
// A segment is narrower than the span it divides, the widest span
|
||
// divided is the full one, and the full span divided by anything
|
||
// greater than one is below 2^511: so the segment's bit pattern,
|
||
// reinterpreted as signed, is its value, and the cast neither costs
|
||
// nor loses anything. Divided by one it would not be, and one
|
||
// segment is not a division but the view again; no bar asks for it.
|
||
let segment_span_i = segment_span.cast_signed();
|
||
|
||
let index = i64::try_from(segment_index).expect("no bar has more cells than an i64 holds");
|
||
let new_start = self.start + segment_span_i * index.as_::<I512>();
|
||
let new_end = new_start + segment_span_i;
|
||
|
||
Self {
|
||
start: new_start,
|
||
end: new_end,
|
||
}
|
||
}
|
||
|
||
/// Locate an instant within this view as a fraction of the way from
|
||
/// `start` to `end`: 0.0 at the first instant, 1.0 at the last, and
|
||
/// `None` for any instant the view does not contain, which is the
|
||
/// Observatory's cue to draw no caret rather than a caret in the
|
||
/// margin. The bar multiplies the fraction by its width in pixels and
|
||
/// places the caret there; the hit test reverses the same arithmetic to
|
||
/// find out which caret was struck.
|
||
///
|
||
/// # On the Reluctant Use of Floating-Point
|
||
///
|
||
/// This function commits the venial sin of `f64` arithmetic, but only in
|
||
/// its last line, and only for the conversion of one exact ratio into a
|
||
/// pixel ratio. The offset of the instant from `start` and the span of
|
||
/// the view are both computed in 512-bit integers, exactly; both are
|
||
/// then brought down to sixty-four bits by the same right shift, chosen
|
||
/// so that the span keeps its leading sixty-four bits and the offset
|
||
/// loses the same low bits; and only then is one divided by the other.
|
||
/// The ratio of the shifted quantities is exact to one part in 2^63,
|
||
/// which is finer than an `f64` can express, so the two conversions and
|
||
/// the one division are the only floating-point operations performed,
|
||
/// each faithful to one part in 2^53, and a pixel is one part in a few
|
||
/// thousand. The underlying temporal values remain in their integer
|
||
/// sanctity until the very moment we must speak to the GPU in its
|
||
/// native tongue, and the orders of magnitude discarded at that moment —
|
||
/// nearly all of them, in the full view — were never going to fit on a
|
||
/// monitor anyway; we shed no tears. The test
|
||
/// `position_is_exact_at_ends_and_midpoint_at_every_zoom` checks the
|
||
/// two ends and the midpoint of forty successive views against 0, 1 and
|
||
/// one half.
|
||
pub fn position_of(&self, planck_time: &I512) -> Option<f64> {
|
||
if planck_time < &self.start || planck_time > &self.end {
|
||
return None;
|
||
}
|
||
|
||
// `start <= planck_time` has just been established, so the wrapped
|
||
// unsigned difference is the exact offset — even in the full view,
|
||
// where the signed difference between an instant near Annihilation
|
||
// and Creation does not fit an I512 and a checked subtraction would
|
||
// have nothing honest to say. The same circle as `span`, walked for
|
||
// the same reason.
|
||
let offset = planck_time
|
||
.cast_unsigned()
|
||
.wrapping_sub(self.start.cast_unsigned());
|
||
let span = self.span();
|
||
if span == U512_ZERO {
|
||
return Some(0.0);
|
||
}
|
||
|
||
// Both quantities are brought down to 64 bits by the same shift: the
|
||
// span's bit length less sixty-four, saturating at zero for spans
|
||
// that already fit, so the span keeps its leading 64 bits and the
|
||
// offset — never the larger — loses the same low bits. Their ratio
|
||
// is then exact to one part in 2^63, which is more than an f64 can
|
||
// hold, and the division below is the only floating-point operation
|
||
// performed on the way to the pixel. The clamp is a courtesy to the
|
||
// rounding, not a correction of the arithmetic: the shifted offset
|
||
// cannot exceed the shifted span, so the quotient cannot exceed one.
|
||
let shift = (U512::BITS - span.leading_zeros()).saturating_sub(64);
|
||
let offset_top = (offset >> shift).as_::<u64>();
|
||
let span_top = (span >> shift).as_::<u64>();
|
||
Some((offset_top as f64 / span_top as f64).clamp(0.0, 1.0))
|
||
}
|
||
|
||
/// Pronounce the span of this view in a unit a human might recognise —
|
||
/// "~minutes", "~billions to trillions of years", "~10^-33 seconds" —
|
||
/// prefixed with a tilde, because the label is chosen by the span's
|
||
/// digit count and a digit count is honest only to within a factor of
|
||
/// ten. The Observatory prints it beneath the zoomed bar, centred, in
|
||
/// cyan. It is the one place the calendar utters the word "minutes",
|
||
/// and it does so under protest, with the tilde as its disclaimer.
|
||
///
|
||
/// The digit-count thresholds are derived from the Planck time constant
|
||
/// and from nothing else:
|
||
/// - `planck_per_second` = 10^50 / 5391247 = 1.855…×10^43 (44 digits)
|
||
/// - `planck_per_minute` = `planck_per_second` × 60 (46 digits)
|
||
/// - `planck_per_hour` = `planck_per_second` × 3600 (47 digits; the arm
|
||
/// below takes 47 and 48 together, hours to days)
|
||
/// - `planck_per_day` = `planck_per_second` × 86400 (49 digits)
|
||
/// - `planck_per_year` = `planck_per_second` × 31557600 (51 digits)
|
||
///
|
||
/// Therefore `span_digits` ≈ 44 + log10(seconds) for any span of a
|
||
/// second or more, and for a sub-second span the exponent of the seconds
|
||
/// is `span_digits` − 44, from which a prefix — micro, nano, pico,
|
||
/// femto, atto — is chosen by range and a power of ten from the table
|
||
/// is printed before it. Below 10^-30 seconds the function gives up on
|
||
/// prefixes and prints the exponent; below 10^10 Planck times it gives
|
||
/// up on seconds altogether and prints Planck times, which is the unit
|
||
/// the calendar wanted to use from the start.
|
||
pub fn format_span(&self) -> String {
|
||
let digits = self.span_digits();
|
||
|
||
// The thresholds, from `derive_constants.sh` at the workspace root.
|
||
// `planck_per_second` has 44 digits, so a span of:
|
||
// 44 digits is about a second;
|
||
// 46 digits about a minute, sixty of those;
|
||
// 47 to 48 digits an hour to a day;
|
||
// 49 to 50 digits days to months, a range the calendar labels
|
||
// "days to years" so as not to say the other word;
|
||
// 51 digits about a year, the Julian one;
|
||
// 60 digits about a billion years.
|
||
// The arms below are these numbers and nothing cleverer.
|
||
|
||
match digits {
|
||
0..=10 => format!("~10^{digits} Planck times"),
|
||
11..=43 => {
|
||
// The sub-second range: 11 to 43 digits is 10^-33 to 10^-1
|
||
// seconds, and the exponent of the seconds is the digit
|
||
// count less the forty-four digits of one second.
|
||
let seconds_exp = i32::try_from(digits).expect("a digit count beyond an i32") - 44;
|
||
if seconds_exp < -30 {
|
||
format!("~10^{seconds_exp} seconds")
|
||
} else if seconds_exp < -15 {
|
||
// Attoseconds, 10^-18 s: eighteen is added to the
|
||
// exponent to name the count of them.
|
||
format!("~{} attoseconds", pow10(seconds_exp + 18))
|
||
} else if seconds_exp < -9 {
|
||
// Femtoseconds, 10^-15 s: fifteen added.
|
||
format!("~{} femtoseconds", pow10(seconds_exp + 15))
|
||
} else if seconds_exp < -6 {
|
||
// Picoseconds, 10^-12 s: twelve added.
|
||
format!("~{} picoseconds", pow10(seconds_exp + 12))
|
||
} else if seconds_exp < -3 {
|
||
// Nanoseconds, 10^-9 s: nine added — the kernel's own
|
||
// unit, and the last one it can vouch for.
|
||
format!("~{} nanoseconds", pow10(seconds_exp + 9))
|
||
} else {
|
||
// Microseconds, 10^-6 s: six added.
|
||
format!("~{} microseconds", pow10(seconds_exp + 6))
|
||
}
|
||
}
|
||
44..=45 => "~seconds".to_string(),
|
||
46 => "~minutes".to_string(),
|
||
47..=48 => "~hours to days".to_string(),
|
||
49..=50 => "~days to years".to_string(),
|
||
51..=53 => "~years to millennia".to_string(),
|
||
54..=59 => "~millions to billions of years".to_string(),
|
||
60..=70 => "~billions to trillions of years".to_string(),
|
||
_ => format!("~10^{} years", digits - 51),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// One event of cosmic history as the timeline knows it: a name for the
|
||
/// list beneath the bar, a coordinate for the caret upon it, an identifier
|
||
/// for the passage that opens when the caret is clicked, and a threshold
|
||
/// that says how far in the observer must have zoomed to be shown it at
|
||
/// all. The table of them is `cosmic_events`. The events themselves are not
|
||
/// invented here, only placed, and the placing is done in the calendar's
|
||
/// own integers from the numbers the passages in `events` give.
|
||
pub struct CosmicEvent {
|
||
/// The identifier that ties this marker to its passage — the same
|
||
/// `EventId` that `events::get_passage` answers to, so that a caret and
|
||
/// the text behind it cannot come apart.
|
||
pub id: EventId,
|
||
/// The short name printed in the list beneath the bar, in amber, behind
|
||
/// a dash, at most eight of them at once.
|
||
pub name: &'static str,
|
||
/// The event's coordinate, in Planck times since the Whisper: negative
|
||
/// for everything that happened before 06:30:41 UTC on 30 October 1969,
|
||
/// which is nearly all of it, and positive for the handful of things
|
||
/// that have happened since and the many that have not happened yet.
|
||
pub planck_time: I512,
|
||
/// The widest view, measured in digits of its span, that shows this
|
||
/// event: it appears when `view.span_digits() <= max_span_digits` and
|
||
/// is withheld from anything wider. A small number is an event that
|
||
/// demands a close look; a large one is visible from far off. The
|
||
/// numbers are tiered by hand so that no view, at any zoom, lists more
|
||
/// events than the panel beneath the bar has lines, and a test holds
|
||
/// them to it.
|
||
pub max_span_digits: usize,
|
||
}
|
||
|
||
/// The most events any view lists, and the number of lines the panel
|
||
/// beneath the bar has for listing them: eight. The thresholds in
|
||
/// `cosmic_events` are tiered so that no view, at any zoom, has more to say
|
||
/// than it has lines to say it in, and the test
|
||
/// `no_view_lists_more_events_than_the_panel_holds` slides a window of
|
||
/// every width from 10^1 to 10^154 Planck times along the chronology to
|
||
/// make sure. The Observatory takes the first `EVENT_LINES` of whatever is
|
||
/// visible, which the thresholds guarantee is all of it; the take is a
|
||
/// statement, not a truncation.
|
||
pub const EVENT_LINES: usize = 8;
|
||
|
||
/// The chronology: every event the timeline can show, from the end of the
|
||
/// Planck epoch to the evaporation of the last black hole, in the order it
|
||
/// happened or is expected to, each placed in Planck times since the
|
||
/// Whisper by integer arithmetic on the numbers its passage gives. Built
|
||
/// on first request and kept for the life of the process by
|
||
/// `visible_events`, the universe's past not being expected to change
|
||
/// between frames.
|
||
///
|
||
/// # Integer Arithmetic for Temporal Constants
|
||
///
|
||
/// Every conversion herein is performed in `I512`, and the seductive
|
||
/// imprecision of floating-point is not merely avoided but never offered.
|
||
/// A time given in scientific notation — 10^-36 seconds, say — is split
|
||
/// into a mantissa and an exponent; the mantissa is multiplied by the
|
||
/// Planck times in a second, and the product is scaled by a power of ten
|
||
/// from the table, multiplying for a positive exponent and dividing for a
|
||
/// negative one. Years are multiplied by the Planck times in a Julian year.
|
||
/// Dates given as "so many years ago" are restated from the Whisper, which
|
||
/// is the present as far as the calendar is concerned; the helper
|
||
/// `years_before_present_to_planck` says why that matters. Civil dates —
|
||
/// the Julian calendar, ENIAC, the genesis block — are placed by their Unix
|
||
/// timestamps through the same offset the library applies to the kernel's
|
||
/// clock, so that a marker that names a day sits on that day, and the test
|
||
/// `civil_dates_land_on_their_days` holds it there.
|
||
///
|
||
/// # On the Thresholds
|
||
///
|
||
/// An event's `max_span_digits` says how far in you must have zoomed to be
|
||
/// shown it. The numbers are tiered by hand so that the whole past fits its
|
||
/// eight lines at the widest views (the first stars, the Solar System, the
|
||
/// Sun's death) and the rest surface as the view narrows, each in the
|
||
/// company of its own era: a window a billion years wide lists the Snowball
|
||
/// and the Cambrian, a window seventeen years wide lists the Whisper and
|
||
/// the Unix epoch. The test `no_view_lists_more_events_than_the_panel_holds`
|
||
/// is the proof that the tiers hold, and the reason the numbers are what
|
||
/// they are and not rounder.
|
||
///
|
||
/// # Panics
|
||
///
|
||
/// Panics if either decimal reliquary at the top of this module fails to
|
||
/// parse as an `I512`, which is to say if someone has edited forty-four or
|
||
/// fifty-one digits and introduced a letter; the table is built once, so
|
||
/// the panic would arrive with the first frame and not later. No entry in
|
||
/// the table can panic on its own: every exponent handed to the helpers is
|
||
/// a small literal, far inside the ranges their conversions guard.
|
||
#[allow(
|
||
clippy::too_many_lines,
|
||
reason = "a table of sixty events and their derivations is one table; cut into pieces it would stop reading as the chronology it is"
|
||
)]
|
||
pub fn cosmic_events() -> Vec<CosmicEvent> {
|
||
let planck_per_second = I512::from_str(PLANCK_PER_SECOND_DECIMAL).unwrap();
|
||
let planck_per_year = I512::from_str(PLANCK_PER_YEAR_DECIMAL).unwrap();
|
||
|
||
// The present, as the table understands it: the age of the universe in
|
||
// Julian years at the Whisper, 13,806,491,023. Read the digits — all
|
||
// eleven — and you have read the Boltzmann constant, 1.380649 × 10⁻²³
|
||
// J/K, mantissa then exponent, straight through with the sign dropped:
|
||
// the exchange rate between information and heat, buried in the age of
|
||
// everything (workspace README, section 2.1). The library carries it as
|
||
// `AGE_UNIVERSE_JULIAN_YEARS`; the derivation is `derive_constants.sh`
|
||
// at the workspace root. Every "years ago" in this table is subtracted
|
||
// from this number, not from the year you are reading it in.
|
||
let present_years: i64 = 13_806_491_023;
|
||
|
||
// Seconds after the Big Bang, given in scientific notation as
|
||
// mantissa × 10^exponent, to Planck times since the Whisper: the first
|
||
// instants of the universe are too brief for any other notation and too
|
||
// sacred for any other arithmetic. Integer throughout.
|
||
let seconds_sci_to_planck = |mantissa: i64, exponent: i32| -> I512 {
|
||
// The product is reassociated so that the scaling by the power of
|
||
// ten comes last, on the largest operand:
|
||
// planck = seconds × planck_per_second
|
||
// = mantissa × 10^exp × planck_per_second
|
||
// = mantissa × planck_per_second × 10^exp
|
||
// For the sub-second times this helper exists for, exp is negative
|
||
// and the last step is a division by 10^(−exp) of a number already
|
||
// forty-odd digits wide, which is why the multiplication precedes
|
||
// it: dividing first would leave nothing to multiply.
|
||
let base = planck_per_second * (mantissa).as_::<I512>();
|
||
let ten = 10i64.as_::<I512>();
|
||
|
||
// A positive exponent multiplies, a negative one divides; the table
|
||
// reaches 10^18, and anything beyond it — the Grand Unification
|
||
// epoch at 10^-36 seconds, the end of inflation at 10^-32 — is
|
||
// applied a power at a time by a fold, thirty-odd divisions by ten
|
||
// standing in for the one division by a number the table declines
|
||
// to hold.
|
||
let planck_offset = if let Ok(exp) = usize::try_from(exponent) {
|
||
POWERS_OF_10.get(exp).map_or_else(
|
||
|| (0..exp).fold(base, |acc, _| acc * ten),
|
||
|&power| base * power.as_::<I512>(),
|
||
)
|
||
} else {
|
||
let exp = usize::try_from(-exponent).expect("a negated negative exponent is positive");
|
||
POWERS_OF_10.get(exp).map_or_else(
|
||
|| (0..exp).fold(base, |acc, _| acc / ten),
|
||
|&power| base / power.as_::<I512>(),
|
||
)
|
||
};
|
||
|
||
creation_planck() + planck_offset
|
||
};
|
||
|
||
// Whole seconds after the Big Bang to Planck times since the Whisper:
|
||
// one multiplication, exact, which is the reason the second was
|
||
// transcribed to forty-four digits rather than approximated to two.
|
||
let seconds_to_planck =
|
||
|seconds: i64| -> I512 { creation_planck() + planck_per_second * (seconds).as_::<I512>() };
|
||
|
||
// Whole Julian years after the Big Bang, likewise: the year's Planck
|
||
// count, all fifty-one digits of it, times the count of years.
|
||
let years_to_planck =
|
||
|years: i64| -> I512 { creation_planck() + planck_per_year * (years).as_::<I512>() };
|
||
|
||
// Years after the Big Bang with a magnitude attached: mantissa × 10^exp,
|
||
// the ten applied one power at a time in `I512`, so that
|
||
// `years_exp_to_planck(1, 14)` is 10^14 years after the Big Bang — the
|
||
// death of the last stars — and `years_exp_to_planck(1, 100)` is the
|
||
// googol at which the last black holes go, a count of years no `i64`
|
||
// could have carried to this closure.
|
||
let years_exp_to_planck = |mantissa: i64, exponent: u32| -> I512 {
|
||
let mut years = (mantissa).as_::<I512>();
|
||
for _ in 0..exponent {
|
||
years *= (10i64).as_::<I512>();
|
||
}
|
||
*creation_planck() + planck_per_year * years
|
||
};
|
||
|
||
// Years before the Whisper to Planck times. "Present" is the epoch,
|
||
// 1969, not the year you are reading this in: the calendar has exactly
|
||
// one present and it has already happened. A date given as "N years
|
||
// ago" from a later vantage must be restated from the Whisper's; for
|
||
// anything older than a million years the restatement is invisible,
|
||
// and for anything younger it is not, which is why the civil dates
|
||
// below do not pass through this helper at all.
|
||
let years_before_present_to_planck =
|
||
|years_ago: i64| -> I512 { years_to_planck(present_years - years_ago) };
|
||
|
||
// Years after the Whisper to Planck times: the same arithmetic with the
|
||
// sign reversed, for the future the calendar has already scheduled.
|
||
let years_after_present_to_planck =
|
||
|years_future: i64| -> I512 { years_to_planck(present_years + years_future) };
|
||
|
||
// Years before the present in scientific notation, mantissa × 10^exp
|
||
// years ago, for the ages of the Earth and its early life, which are
|
||
// known to two or three figures and given here as exactly that many.
|
||
let years_ago_exp_to_planck = |mantissa: i64, exponent: u32| -> I512 {
|
||
let mut years_ago = (mantissa).as_::<I512>();
|
||
for _ in 0..exponent {
|
||
years_ago *= (10i64).as_::<I512>();
|
||
}
|
||
*creation_planck() + planck_per_year * ((present_years).as_::<I512>() - years_ago)
|
||
};
|
||
|
||
// Years after the present in scientific notation, mantissa × 10^exp
|
||
// years hence: the oceans, Andromeda, the red giant, given to the
|
||
// figures the models share and not one more.
|
||
let years_future_exp_to_planck = |mantissa: i64, exponent: u32| -> I512 {
|
||
let mut years_future = (mantissa).as_::<I512>();
|
||
for _ in 0..exponent {
|
||
years_future *= (10i64).as_::<I512>();
|
||
}
|
||
*creation_planck() + planck_per_year * ((present_years).as_::<I512>() + years_future)
|
||
};
|
||
|
||
// A civil instant, given as its Unix timestamp in UTC, placed from the
|
||
// Whisper by the calendar's own arithmetic — the timestamp's distance
|
||
// from `WHISPER_UNIX_SECONDS`, in seconds, times the Planck times in a
|
||
// second — which is exactly what `now()` does to the kernel's clock,
|
||
// and so exactly where `now()` would have placed the moment had it been
|
||
// running then. Exact to the second, as the dates in the passages are;
|
||
// a marker that names a day sits on that day, and one that names a
|
||
// second sits on the second.
|
||
let civil_to_planck = |unix_seconds: i64| -> I512 {
|
||
planck_per_second * (unix_seconds - WHISPER_UNIX_SECONDS).as_::<I512>()
|
||
};
|
||
|
||
vec![
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE PLANCK ERA: the first 10^-43 seconds, one Planck time, the unit the calendar counts in
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::PlanckEpochEnds,
|
||
name: "Planck Epoch ends",
|
||
planck_time: *creation_planck() + (1i64).as_::<I512>(),
|
||
max_span_digits: 5,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE GRAND UNIFICATION ERA: 10^-43 to 10^-36 seconds, three forces still on speaking terms
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::GrandUnificationEpoch,
|
||
name: "Grand Unification Epoch",
|
||
planck_time: seconds_sci_to_planck(1, -36), // 1e-36 seconds
|
||
max_span_digits: 15,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE ELECTROWEAK ERA: 10^-36 to 10^-12 seconds, inflation, and the Higgs field turning on
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::InflationEnds,
|
||
name: "Inflation ends",
|
||
planck_time: seconds_sci_to_planck(1, -32), // 1e-32 seconds
|
||
max_span_digits: 20,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::ElectroweakEpoch,
|
||
name: "Electroweak Epoch",
|
||
planck_time: seconds_sci_to_planck(1, -12), // 1e-12 seconds
|
||
max_span_digits: 35,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE QUARK ERA: 10^-12 to 10^-5 seconds, a plasma in which nothing has yet been confined
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::QuarkEpoch,
|
||
name: "Quark Epoch",
|
||
planck_time: seconds_sci_to_planck(1, -6), // 1e-6 seconds
|
||
max_span_digits: 40,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE HADRON ERA: 10^-5 seconds to one second, the quarks confined and the antimatter gone
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::HadronEpoch,
|
||
name: "Hadron Epoch",
|
||
planck_time: seconds_sci_to_planck(1, -4), // 1e-4 seconds
|
||
max_span_digits: 42,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::NeutrinoDecoupling,
|
||
name: "Neutrino decoupling",
|
||
planck_time: seconds_to_planck(1), // 1 second
|
||
max_span_digits: 46,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// NUCLEOSYNTHESIS: one second to twenty minutes, the whole universe run as one reactor
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::NucleosynthesisBegins,
|
||
name: "Nucleosynthesis begins",
|
||
planck_time: seconds_to_planck(10), // 10 seconds
|
||
max_span_digits: 47,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::NucleosynthesisEnds,
|
||
name: "Nucleosynthesis ends",
|
||
planck_time: seconds_to_planck(1200), // 20 minutes
|
||
max_span_digits: 49,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE PHOTON ERA: twenty minutes to 380,000 years, opaque, cooling, and nothing scheduled
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::CoolerThanTheSunsCore,
|
||
name: "Cooler than the Sun's core",
|
||
// In the radiation era the temperature falls as the inverse
|
||
// square root of the age, T ≈ 1.3 × 10^10 K / sqrt(t / 1 s); the
|
||
// Sun's core, 15.7 million K, is crossed at ~7 × 10^5 s, and the
|
||
// marker sits there, a week and a day in.
|
||
planck_time: seconds_to_planck(700_000), // ~8 days
|
||
max_span_digits: 51,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::MatterRadiationEquality,
|
||
name: "Matter-radiation equality",
|
||
planck_time: years_to_planck(47_000), // 47,000 years
|
||
max_span_digits: 56,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::Recombination,
|
||
name: "Recombination (CMB)",
|
||
planck_time: years_to_planck(380_000), // 380,000 years
|
||
max_span_digits: 58,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE DARK AGES: 380,000 to some 200 million years, and not one photon that was new
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::DarkAgesBegin,
|
||
name: "Dark Ages begin",
|
||
planck_time: years_to_planck(380_000), // 380,000 years
|
||
max_span_digits: 60,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// COSMIC DAWN: some 200 to 500 million years, the first stars and the first galaxies
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::FirstStarsIgnite,
|
||
name: "First stars ignite",
|
||
planck_time: years_to_planck(200_000_000), // 200 million years
|
||
max_span_digits: 63,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstGalaxiesForm,
|
||
name: "First galaxies form",
|
||
planck_time: years_to_planck(300_000_000), // 300 million years
|
||
max_span_digits: 60,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE STELLIFEROUS ERA: the age of stars, from reionization to the noon and the afternoon
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::ReionizationComplete,
|
||
name: "Reionization complete",
|
||
planck_time: years_to_planck(1_000_000_000), // 1 billion years
|
||
max_span_digits: 61,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::CosmicNoon,
|
||
name: "Cosmic noon",
|
||
planck_time: years_to_planck(3_500_000_000), // z ≈ 2, 3.5 billion years
|
||
max_span_digits: 62,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::ExpansionAccelerates,
|
||
name: "Expansion begins to accelerate",
|
||
planck_time: years_to_planck(7_500_000_000), // z ≈ 0.7, ~6 billion years ago
|
||
max_span_digits: 62,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE SOLAR SYSTEM: some 9.2 billion years after the Big Bang, an arm, a cloud, a star
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::SolarSystemForms,
|
||
name: "Solar System forms",
|
||
planck_time: years_ago_exp_to_planck(46, 8), // 4.6 billion years ago
|
||
max_span_digits: 63,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::EarthForms,
|
||
name: "Earth forms",
|
||
planck_time: years_ago_exp_to_planck(454, 7), // 4.54 billion years ago
|
||
max_span_digits: 61,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::MoonForms,
|
||
name: "Moon-forming impact",
|
||
planck_time: years_ago_exp_to_planck(45, 8), // 4.5 billion years ago
|
||
max_span_digits: 60,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// LIFE ON EARTH: from a chemistry nobody understands to the extinctions that made room
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::FirstLife,
|
||
name: "First life emerges",
|
||
planck_time: years_ago_exp_to_planck(38, 8), // 3.8 billion years ago
|
||
max_span_digits: 62,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::GreatOxidation,
|
||
name: "Great Oxidation Event",
|
||
planck_time: years_ago_exp_to_planck(243, 7), // 2.43 billion years ago
|
||
max_span_digits: 60,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstEukaryotes,
|
||
name: "First eukaryotes",
|
||
planck_time: years_ago_exp_to_planck(165, 7), // 1.65 billion years ago
|
||
max_span_digits: 60,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::SnowballEarth,
|
||
name: "Snowball Earth",
|
||
planck_time: years_before_present_to_planck(717_000_000), // Sturtian onset
|
||
max_span_digits: 60,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::CambrianExplosion,
|
||
name: "Cambrian Explosion",
|
||
planck_time: years_before_present_to_planck(539_000_000), // 538.8 million years ago
|
||
max_span_digits: 60,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::PermianExtinction,
|
||
name: "The Great Dying (Permian)",
|
||
planck_time: years_before_present_to_planck(252_000_000), // 251.9 million years ago
|
||
max_span_digits: 60,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::DinosaurExtinction,
|
||
name: "Dinosaur extinction",
|
||
planck_time: years_before_present_to_planck(66_000_000), // 66 million years ago
|
||
max_span_digits: 61,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// HUMAN HISTORY: three hundred thousand years, the last two percent of them written down
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::HomoSapiens,
|
||
name: "Homo sapiens",
|
||
planck_time: years_before_present_to_planck(300_000), // Jebel Irhoud
|
||
max_span_digits: 59,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstFarmers,
|
||
name: "First farmers",
|
||
planck_time: years_before_present_to_planck(11_500), // Fertile Crescent
|
||
max_span_digits: 57,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstWriting,
|
||
name: "First writing",
|
||
planck_time: years_before_present_to_planck(5400), // 5400 years ago
|
||
max_span_digits: 56,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::JulianCalendar,
|
||
name: "Julian calendar",
|
||
planck_time: civil_to_planck(-63_555_926_400), // 1 January 45 BC (Julian)
|
||
max_span_digits: 55,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::LeibnizBinary,
|
||
name: "Leibniz explains binary",
|
||
planck_time: civil_to_planck(-8_417_952_000), // April 1703
|
||
max_span_digits: 54,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::PlanckUnits,
|
||
name: "Planck units proposed",
|
||
planck_time: civil_to_planck(-2_228_688_000), // 18 May 1899
|
||
max_span_digits: 53,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstComputer,
|
||
name: "First electronic computer",
|
||
planck_time: civil_to_planck(-759_283_200), // 10 December 1945, ENIAC's first run
|
||
max_span_digits: 52,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstTransistor,
|
||
name: "First transistor",
|
||
planck_time: civil_to_planck(-695_692_800), // 16 December 1947
|
||
max_span_digits: 52,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE WHISPER AND BEYOND: around epoch zero, where the count changes sign
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::ArpanetWhisper,
|
||
name: "The Whisper (ARPANET)",
|
||
planck_time: I512_ZERO, // the epoch itself: zero, by definition and by decree
|
||
max_span_digits: 54,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::UnixEpoch,
|
||
name: "Unix epoch",
|
||
planck_time: civil_to_planck(0), // 1 January 1970 00:00:00 UTC
|
||
max_span_digits: 52,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstEmail,
|
||
name: "First email",
|
||
planck_time: years_after_present_to_planck(2), // late 1971; the month is not recorded
|
||
max_span_digits: 52,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstLeapSecond,
|
||
name: "First leap second",
|
||
planck_time: civil_to_planck(78_796_800), // 1972-06-30 23:59:60 ends here
|
||
max_span_digits: 52,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstWebsite,
|
||
name: "First website",
|
||
planck_time: civil_to_planck(681_436_800), // 6 August 1991
|
||
max_span_digits: 52,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstSmartphone,
|
||
name: "iPhone released",
|
||
planck_time: civil_to_planck(1_183_075_200), // 29 June 2007
|
||
max_span_digits: 52,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::FirstBitcoinBlock,
|
||
name: "Bitcoin genesis block",
|
||
planck_time: civil_to_planck(1_231_006_505), // 3 January 2009 18:15:05 UTC
|
||
max_span_digits: 52,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::UnixTimeOverflows,
|
||
name: "32-bit time overflows",
|
||
planck_time: civil_to_planck(2_147_483_648), // 19 January 2038 03:14:08 UTC
|
||
max_span_digits: 52,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE FAR FUTURE: Voyager, the continents, the oceans, Andromeda, the Sun, the last stars
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::VoyagerPassesGliese445,
|
||
name: "Voyager 1 passes Gliese 445",
|
||
planck_time: years_after_present_to_planck(40_000), // ~40,000 years from now
|
||
max_span_digits: 57,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::PangaeaProxima,
|
||
name: "Pangaea Proxima",
|
||
planck_time: years_future_exp_to_planck(25, 7), // 250 million years from now
|
||
max_span_digits: 60,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::OceansEvaporate,
|
||
name: "Oceans evaporate",
|
||
planck_time: years_future_exp_to_planck(1, 9), // ~1 billion years from now
|
||
max_span_digits: 60,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::AndromedaArrives,
|
||
name: "Andromeda arrives (even odds)",
|
||
planck_time: years_future_exp_to_planck(45, 8), // 4.5 billion years from now
|
||
max_span_digits: 62,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::SunRedGiant,
|
||
name: "Sun becomes red giant",
|
||
planck_time: years_future_exp_to_planck(5, 9), // 5 billion years from now
|
||
max_span_digits: 65,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::LocalGroupAlone,
|
||
name: "Local Group alone",
|
||
planck_time: years_exp_to_planck(1, 11), // 10^11 years after Big Bang
|
||
max_span_digits: 64,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::LastStarsDie,
|
||
name: "Last stars die",
|
||
planck_time: years_exp_to_planck(1, 14), // 10^14 years after Big Bang
|
||
max_span_digits: 70,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE DEGENERATE ERA: the remnants cooling, the planets stripped, the galaxies unbound
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::DegenerateEra,
|
||
name: "Degenerate Era",
|
||
planck_time: years_exp_to_planck(1, 15), // 10^15 years
|
||
max_span_digits: 75,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::GalaxiesDisperse,
|
||
name: "Galaxies disperse",
|
||
planck_time: years_exp_to_planck(1, 19), // 10^19 years
|
||
max_span_digits: 80,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE BLACK HOLE ERA: if the protons decay, only holes are left; if not, the rocks go round
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::ProtonDecayComplete,
|
||
name: "Proton decay (if any) complete",
|
||
planck_time: years_exp_to_planck(1, 40), // 10^40 years
|
||
max_span_digits: 100,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::BlackHoleEraBegins,
|
||
name: "Black Hole Era begins",
|
||
planck_time: years_exp_to_planck(1, 41), // 10^41 years (after proton decay)
|
||
max_span_digits: 102,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::RocksBecomeSpheres,
|
||
name: "Rocks flow into spheres",
|
||
planck_time: years_exp_to_planck(1, 65), // 10^65 years, if protons persist
|
||
max_span_digits: 118,
|
||
},
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// THE DARK ERA: the holes evaporate, the dwarfs leak away, the bits run out
|
||
// ════════════════════════════════════════════════════════════════════
|
||
CosmicEvent {
|
||
id: EventId::StellarBlackHolesEvaporate,
|
||
name: "Stellar black holes evaporate",
|
||
planck_time: years_exp_to_planck(1, 67), // 10^67 years
|
||
max_span_digits: 120,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::WhiteDwarfsErode,
|
||
name: "White dwarfs erode away",
|
||
planck_time: years_exp_to_planck(1, 78), // 10^78 years
|
||
max_span_digits: 130,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::SagittariusAEvaporates,
|
||
name: "Sagittarius A* evaporates",
|
||
planck_time: years_exp_to_planck(1, 87), // ~10^87 years at its present mass
|
||
max_span_digits: 140,
|
||
},
|
||
CosmicEvent {
|
||
id: EventId::SupermassiveBlackHolesEvaporate,
|
||
name: "Supermassive black holes evaporate",
|
||
planck_time: years_exp_to_planck(1, 100), // 10^100 years
|
||
max_span_digits: 150,
|
||
},
|
||
]
|
||
}
|
||
|
||
/// The events this view is entitled to show: those whose threshold admits
|
||
/// the view's digit count and whose coordinate lies within its two ends,
|
||
/// in the table's order, which is chronological, so that the list beneath
|
||
/// the bar reads in the order things happened. Furnished as references
|
||
/// into a table built once and kept for the life of the process — the
|
||
/// Observatory asks this once per frame, the frames are not throttled, and
|
||
/// the past does not change often enough to justify placing sixty events
|
||
/// in `I512` again each time. What changes is the view, and the view is
|
||
/// the only argument.
|
||
pub fn visible_events(view: &TimelineView) -> Vec<&'static CosmicEvent> {
|
||
// The table, built on first request and never again: sixty events,
|
||
// sixty 512-bit placements, one `LazyLock`, and a `'static` lifetime
|
||
// that is the truth, since the process cannot outlive its own past.
|
||
use std::sync::LazyLock;
|
||
static EVENTS: LazyLock<Vec<CosmicEvent>> = LazyLock::new(cosmic_events);
|
||
|
||
let span_digits = view.span_digits();
|
||
|
||
EVENTS
|
||
.iter()
|
||
.filter(|event| {
|
||
// An event is shown when the view has narrowed enough to admit
|
||
// it — its threshold at or above the span's digit count — and
|
||
// when it lies between the view's two ends, inclusive. The first
|
||
// test is about the observer; the second is about the event.
|
||
span_digits <= event.max_span_digits
|
||
&& event.planck_time >= view.start
|
||
&& event.planck_time <= view.end
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// The outcome of a click on the bar, reduced to the two numbers the zoom
|
||
/// needs: which segment was struck and how many there were to strike. The
|
||
/// pair is what `TimelineView::zoom_into` consumes, and what the breadcrumb
|
||
/// above the bar prints, one-indexed, as the fraction the observer chose.
|
||
#[derive(Clone, Copy)]
|
||
pub struct TimelineHit {
|
||
/// The segment struck, counting from zero at the bar's left end and
|
||
/// never reaching `segment_count`: the bar's right edge is folded into
|
||
/// the last segment rather than into a segment that does not exist.
|
||
pub segment_index: usize,
|
||
/// How many segments the bar was divided into when it was struck — one
|
||
/// per glyph cell between the brackets, at the Observatory's width.
|
||
pub segment_count: usize,
|
||
}
|
||
|
||
impl TimelineHit {
|
||
/// Resolve a click into the segment it landed in. `bar_x` is the pixel
|
||
/// at which the bar's first segment begins, just inside the opening
|
||
/// bracket; `bar_width` is the width in pixels of all the segments
|
||
/// together; `click_x` is where the pointer was released; and
|
||
/// `segment_count` is how many equal segments the caller drew, which
|
||
/// the Observatory takes to be the number of glyph cells between the
|
||
/// brackets. A click outside the bar's horizontal extent yields `None`
|
||
/// — the vertical test belongs to the caller, which knows where the
|
||
/// bar is and this function does not. Inside it, the offset from the
|
||
/// bar's start is divided by the width of one segment and floored, and
|
||
/// the result is clamped to the last segment, so that the bar's right
|
||
/// edge, which the division would assign to a segment past the end,
|
||
/// belongs to the segment it borders.
|
||
///
|
||
/// # Panics
|
||
///
|
||
/// In a debug build, panics if `segment_count` is zero, since
|
||
/// `segment_count - 1` then underflows a `usize`; a release build wraps
|
||
/// instead and answers segment zero of no segments. A bar with no
|
||
/// segments has nothing to be clicked, and the caller that drew one has
|
||
/// already asked a question this function cannot frame.
|
||
pub fn from_click(
|
||
bar_x: f32,
|
||
bar_width: f32,
|
||
click_x: f32,
|
||
segment_count: usize,
|
||
) -> Option<Self> {
|
||
if click_x < bar_x || click_x > bar_x + bar_width {
|
||
return None;
|
||
}
|
||
|
||
let relative_x = click_x - bar_x;
|
||
let segment_width = bar_width / segment_count as f32;
|
||
let segment_index = cell_under(relative_x, segment_width);
|
||
|
||
Some(Self {
|
||
segment_index: segment_index.min(segment_count - 1),
|
||
segment_count,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// The full view knows itself: constructed from Creation and
|
||
/// Annihilation, it answers `is_full`, which is the least a view can do.
|
||
#[test]
|
||
fn test_full_view() {
|
||
let view = TimelineView::full();
|
||
assert!(view.is_full());
|
||
}
|
||
|
||
/// One descent into the first of ten segments is no longer the full
|
||
/// view and is narrower than it — narrower by the factor of ten the
|
||
/// segment count promised, though the test asks only for narrower.
|
||
#[test]
|
||
fn test_zoom_into() {
|
||
let view = TimelineView::full();
|
||
let zoomed = view.zoom_into(0, 10);
|
||
assert!(!zoomed.is_full());
|
||
assert!(zoomed.span() < view.span());
|
||
}
|
||
|
||
/// The full view lists nothing: every event on the table demands a
|
||
/// closer look than 154 digits of span affords.
|
||
#[test]
|
||
fn test_event_visibility() {
|
||
let full = TimelineView::full();
|
||
let visible = visible_events(&full);
|
||
// In the full view, 154 digits wide, nothing is legible: the most
|
||
// patient event on the table, the last black hole, waits for 150.
|
||
// The assertion is looser than the fact, and the fact is empty.
|
||
assert!(visible.is_empty() || visible.len() < 5);
|
||
}
|
||
|
||
/// The full view's width is the library's own total span, bit for bit,
|
||
/// even though no I512 can hold the difference of its two ends: the
|
||
/// wrapped unsigned subtraction here and the unsigned addition in the
|
||
/// library arrive at the same 154 digits by different roads.
|
||
#[test]
|
||
fn full_span_agrees_with_the_library() {
|
||
use loic::total_span;
|
||
assert_eq!(TimelineView::full().span(), *total_span());
|
||
}
|
||
|
||
/// The position of an instant is exact at both ends and at the midpoint
|
||
/// of every view, from the full span down forty zoom levels: the ratio
|
||
/// is taken on the leading bits of both quantities, never on their
|
||
/// decimal strings, so no zoom level is quietly off by a power of ten,
|
||
/// and the midpoint — the span halved by a shift and added to the start
|
||
/// — lands within a millionth of a millionth of one half at every depth.
|
||
#[test]
|
||
fn position_is_exact_at_ends_and_midpoint_at_every_zoom() {
|
||
let mut view = TimelineView::full();
|
||
for _ in 0..40 {
|
||
let half = (view.span() >> 1u32).cast_signed();
|
||
let midpoint = view.start + half;
|
||
assert_eq!(view.position_of(&view.start), Some(0.0));
|
||
assert_eq!(view.position_of(&view.end), Some(1.0));
|
||
let position = view
|
||
.position_of(&midpoint)
|
||
.expect("a midpoint that is not inside its view is not a midpoint");
|
||
assert!(
|
||
(position - 0.5).abs() < 1e-12,
|
||
"midpoint at {position}, not one half"
|
||
);
|
||
view = view.zoom_into(17, 58);
|
||
}
|
||
}
|
||
|
||
/// The table is a chronology: every event is at or after the one before
|
||
/// it, so the panel's list, which takes the table's order, reads in the
|
||
/// order things happened. A table that placed the Cambrian after the
|
||
/// dinosaurs would still render; it would merely be wrong, in the way
|
||
/// this test exists to refuse.
|
||
#[test]
|
||
fn events_are_chronological() {
|
||
let events = cosmic_events();
|
||
for pair in events.windows(2) {
|
||
assert!(
|
||
pair[0].planck_time <= pair[1].planck_time,
|
||
"{} is listed before {} in the table and happens after it in the universe",
|
||
pair[0].name,
|
||
pair[1].name
|
||
);
|
||
}
|
||
}
|
||
|
||
/// The panel lists `EVENT_LINES` events and no more. A view with `d`
|
||
/// span digits is narrower than 10^d Planck times and lists the events
|
||
/// whose threshold admits `d`; so for every `d` from 1 to the full
|
||
/// span's 154, no window of width 10^d may contain more than
|
||
/// `EVENT_LINES` such events. This is the sliding window over the
|
||
/// chronology, in the calendar's own integers — offsets from Creation
|
||
/// as `U512`, widths as exact powers of ten — and it is the reason the
|
||
/// thresholds are what they are and not rounder.
|
||
#[test]
|
||
fn no_view_lists_more_events_than_the_panel_holds() {
|
||
let events = cosmic_events();
|
||
let creation = creation_planck().cast_unsigned();
|
||
let ten = 10u8.as_::<U512>();
|
||
for digits in 1..=154u32 {
|
||
let width = ten.pow(digits);
|
||
let level = usize::try_from(digits).expect("one hundred and fifty-four fits a usize");
|
||
let offsets: Vec<U512> = events
|
||
.iter()
|
||
.filter(|event| event.max_span_digits >= level)
|
||
.map(|event| event.planck_time.cast_unsigned().wrapping_sub(creation))
|
||
.collect();
|
||
let mut first = 0;
|
||
for (last, &offset) in offsets.iter().enumerate() {
|
||
while offset - offsets[first] > width {
|
||
first += 1;
|
||
}
|
||
assert!(
|
||
last - first < EVENT_LINES,
|
||
"a {digits}-digit view lists {} events; the panel has {EVENT_LINES} lines",
|
||
last - first + 1
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Civil dates sit exactly where the calendar's own arithmetic puts
|
||
/// them: the Unix epoch is 5,419,759 seconds after the Whisper, the
|
||
/// genesis block is its timestamp plus that, and the transistor, which
|
||
/// is dated before the epoch, lands twenty-two years before it —
|
||
/// twenty-one and most of another, which integer division calls
|
||
/// twenty-one — and not, as "years before present" would have it from
|
||
/// a later present, somewhere in the nineteenth century.
|
||
#[test]
|
||
fn civil_dates_land_on_their_days() {
|
||
let events = cosmic_events();
|
||
let planck_per_second = I512::from_str(PLANCK_PER_SECOND_DECIMAL).unwrap();
|
||
let seconds_from_whisper = |id: EventId| -> I512 {
|
||
let event = events
|
||
.iter()
|
||
.find(|event| event.id == id)
|
||
.expect("an event with a passage and no place in the table");
|
||
event.planck_time / planck_per_second
|
||
};
|
||
assert_eq!(
|
||
seconds_from_whisper(EventId::UnixEpoch),
|
||
5_419_759i64.as_::<I512>()
|
||
);
|
||
assert_eq!(
|
||
seconds_from_whisper(EventId::FirstBitcoinBlock),
|
||
(1_231_006_505i64 + 5_419_759).as_::<I512>()
|
||
);
|
||
assert_eq!(
|
||
seconds_from_whisper(EventId::FirstTransistor),
|
||
(-695_692_800i64 + 5_419_759).as_::<I512>()
|
||
);
|
||
let seconds_per_julian_year = 31_557_600i64.as_::<I512>();
|
||
assert_eq!(
|
||
seconds_from_whisper(EventId::FirstTransistor) / seconds_per_julian_year,
|
||
(-21i64).as_::<I512>()
|
||
);
|
||
}
|
||
}
|