mod.rs
17.3 kB · rust · 541 lines
1/// The markdown page the ledger renders.2pub mod markdown;3/// The measures and their cost classes.4pub mod measure;5/// The curated OEIS records and the identification against them.6pub mod records;7/// The term generators and the closed forms.8pub mod terms;910pub use markdown::markdown;11pub use measure::{Cost, Measure};12pub use records::{identify, Record, RECORDS};13pub use terms::{closed, fill_polynomial, terms};1415use mrlycore::errors::{value_error, Result};16use mrlymath::bang::{baseq, Code};17use mrlymath::name::{Bang, Named, Sequence as SequenceName};18use std::collections::BTreeMap;19use std::sync::{Mutex, OnceLock};2021type Walked = BTreeMap<(usize, usize), &'static [Code]>;2223/// The dimension and base pairs the ledger walks.24pub const SPACES: [(usize, usize); 9] = [25 (1, 2),26 (2, 2),27 (3, 2),28 (4, 2),29 (1, 3),30 (2, 3),31 (1, 4),32 (2, 4),33 (1, 5),34];3536/// The cells a catalog row may render for one term.37pub const BUDGET: u128 = 500_000;3839/// The terms a catalog row holds.40pub const TERMS: usize = 8;4142/// The index a sequence runs along.43#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]44pub enum Axis {45 /// The fractal level from 1, at side `max(base, 3)`.46 Level,47 /// The odd side `2k - 1` from `k = 2`, at level 1.48 Side,49}5051impl Axis {52 /// Both axes, level first.53 pub const ALL: [Axis; 2] = [Axis::Level, Axis::Side];54 /// Returns the axis's one-word name.55 pub fn slug(self) -> &'static str {56 match self {57 Axis::Level => "level",58 Axis::Side => "side",59 }60 }61 /// Parses a one-word name back into its axis, or an error for any other word.62 pub fn parse(slug: &str) -> Result<Axis> {63 match slug {64 "level" => Ok(Axis::Level),65 "side" => Ok(Axis::Side),66 other => value_error(format!("unknown axis {other:?}.")),67 }68 }69 /// Returns the ledger index of the first term: the level 1 or the `k` of side 3.70 pub fn start(self) -> i32 {71 match self {72 Axis::Level => 1,73 Axis::Side => 2,74 }75 }76 /// Returns the side number and level of the term at the index.77 pub fn place(self, index: usize, number: usize) -> (usize, u32) {78 match self {79 Axis::Level => (number, index as u32 + 1),80 Axis::Side => (2 * index + 3, 1),81 }82 }83}8485/// The status a claim carries.86#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]87pub enum Tag {88 /// Proved on this tree.89 Proved,90 /// Checked against a record or a second generator.91 Verified,92 /// Stated and not yet checked.93 Conjecture,94 /// Checked and found false.95 Refuted,96}9798impl Tag {99 /// Returns the tag's capitalised word.100 pub fn text(self) -> &'static str {101 match self {102 Tag::Proved => "Proved",103 Tag::Verified => "Verified",104 Tag::Conjecture => "Conjecture",105 Tag::Refuted => "Refuted",106 }107 }108}109110/// A design sequence's address: the design, the measure and the axis.111#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]112pub struct Key {113 /// The design's code.114 pub code: Code,115 /// The design's dimension.116 pub dimension: usize,117 /// The numeral base of the corners.118 pub base: usize,119 /// The reading taken.120 pub measure: Measure,121 /// The index the reading runs along.122 pub axis: Axis,123}124125impl Key {126 /// Pins a design sequence to its address.127 pub const fn new(128 code: Code,129 dimension: usize,130 base: usize,131 measure: Measure,132 axis: Axis,133 ) -> Key {134 Key {135 code,136 dimension,137 base,138 measure,139 axis,140 }141 }142 /// Returns the sequence name this key addresses.143 pub fn named(&self) -> SequenceName {144 SequenceName::new(145 self.code,146 self.dimension,147 self.base,148 self.measure.slug(),149 self.axis.slug(),150 )151 }152 /// Returns the sequence's file name.153 ///154 /// ```155 /// use mrlylab::ledger::{Axis, Key, Measure};156 /// let key = Key::new(23, 3, 2, Measure::Surface, Axis::Level);157 /// assert_eq!(key.name(), "sequence_dim=3_code=23_measure=surface_axis=level");158 /// assert_eq!(key.id(), "753b6b49");159 /// ```160 pub fn name(&self) -> String {161 self.named().to_file()162 }163 /// Returns the first eight hex digits of the sha256 of the sequence's canonical JSON.164 pub fn id(&self) -> String {165 self.named().to_id()166 }167 /// Returns the design pinned to its dimension and base.168 pub fn design(&self) -> Bang {169 Bang::new(self.code, self.dimension, self.base)170 }171 /// Returns the side number the level axis runs at.172 pub fn number(&self) -> usize {173 self.base.max(3)174 }175}176177/// A closed form of a sequence.178#[derive(Clone, Debug, PartialEq, Eq)]179pub enum Closed {180 /// `fill^level`.181 Power(u128),182 /// `cells^level - fill^level`.183 Difference(u128, u128),184 /// A polynomial in `k` by rising power, at side `2k - 1`.185 Polynomial(Vec<i128>),186 /// `a(level) = c[0] a(level-1) + c[1] a(level-2) + ...`.187 Recurrence(Vec<i128>),188}189190fn signed(text: &mut String, coefficient: i128, first: bool) {191 if first {192 if coefficient < 0 {193 text.push('-');194 }195 } else {196 text.push_str(if coefficient < 0 { " - " } else { " + " });197 }198}199200impl Closed {201 /// Spells the closed form.202 pub fn text(&self) -> String {203 match self {204 Closed::Power(f) => format!("{f}^level"),205 Closed::Difference(g, f) => format!("{g}^level - {f}^level"),206 Closed::Polynomial(coefficients) => {207 let mut text = String::new();208 for (power, &c) in coefficients.iter().enumerate().rev() {209 if c == 0 {210 continue;211 }212 let first = text.is_empty();213 signed(&mut text, c, first);214 if c.abs() != 1 || power == 0 {215 text.push_str(&c.abs().to_string());216 }217 match power {218 0 => {}219 1 => text.push('k'),220 _ => text.push_str(&format!("k^{power}")),221 }222 }223 if text.is_empty() {224 text.push('0');225 }226 text227 }228 Closed::Recurrence(coefficients) => {229 let mut text = String::new();230 for (back, &c) in coefficients.iter().enumerate() {231 if c == 0 {232 continue;233 }234 let first = text.is_empty();235 signed(&mut text, c, first);236 if c.abs() != 1 {237 text.push_str(&c.abs().to_string());238 text.push(' ');239 }240 text.push_str(&format!("a(level-{})", back + 1));241 }242 if text.is_empty() {243 text.push('0');244 }245 format!("a(level) = {text}")246 }247 }248 }249}250251/// A cost tier of the catalog.252#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]253pub enum Tier {254 /// The closed measures on both axes.255 Closed,256 /// The profile measures on both axes.257 Convolved,258 /// The grid measures on the side axis.259 SideGrid,260 /// The grid measures on the level axis.261 LevelGrid,262}263264impl Tier {265 /// Every tier, cheapest first.266 pub const ALL: [Tier; 4] = [267 Tier::Closed,268 Tier::Convolved,269 Tier::SideGrid,270 Tier::LevelGrid,271 ];272 /// Returns the tier's one-word name.273 pub fn slug(self) -> &'static str {274 match self {275 Tier::Closed => "closed",276 Tier::Convolved => "convolved",277 Tier::SideGrid => "side",278 Tier::LevelGrid => "level",279 }280 }281 /// Parses a one-word name back into its tier, or an error for any other word.282 pub fn parse(slug: &str) -> Result<Tier> {283 Tier::ALL284 .into_iter()285 .find(|tier| tier.slug() == slug)286 .map_or_else(|| value_error(format!("unknown tier {slug:?}.")), Ok)287 }288 fn cost(self) -> Cost {289 match self {290 Tier::Closed => Cost::Closed,291 Tier::Convolved => Cost::Convolved,292 _ => Cost::Grid,293 }294 }295 fn axes(self) -> &'static [Axis] {296 match self {297 Tier::SideGrid => &[Axis::Side],298 Tier::LevelGrid => &[Axis::Level],299 _ => &Axis::ALL,300 }301 }302}303304/// One row of the catalog: a design sequence with its terms, its closed form and its record.305#[derive(Clone, Debug, PartialEq, Eq)]306pub struct Sequence {307 /// The sequence's address.308 pub key: Key,309 /// The first terms.310 pub terms: Vec<i128>,311 /// Whether the cell budget or a u128 stopped the terms short.312 pub capped: bool,313 /// The closed form, when one is known.314 pub closed: Option<Closed>,315 /// The record the terms match, with the record's index less the ledger's.316 pub record: Option<(&'static Record, i32)>,317 /// The status of the match: the record's when the record names this key, else a collision to explain.318 pub tag: Option<Tag>,319}320321impl Sequence {322 fn mentions(&self, needle: &str) -> bool {323 self.key.name().contains(needle)324 || self.record.is_some_and(|(record, _)| {325 record.id.to_lowercase().contains(needle)326 || record.name.to_lowercase().contains(needle)327 })328 }329}330331/// Returns the designs of a dimension and base, the least code of every orbit, walked once and cached, or an error past the walk limit.332///333/// ```334/// assert_eq!(mrlylab::ledger::designs(2, 2).unwrap(), [0, 1, 3, 6, 7, 15]);335/// assert_eq!(mrlylab::ledger::designs(2, 3).unwrap().len(), 26);336/// ```337pub fn designs(dimension: usize, base: usize) -> Result<&'static [Code]> {338 static CACHE: OnceLock<Mutex<Walked>> = OnceLock::new();339 let cache = CACHE.get_or_init(|| Mutex::new(BTreeMap::new()));340 let mut guard = cache.lock().expect("the design cache is not poisoned");341 if let Some(codes) = guard.get(&(dimension, base)) {342 return Ok(codes);343 }344 let codes: Vec<Code> = baseq::representatives(base, dimension)?345 .into_iter()346 .map(|(code, _)| code)347 .collect();348 let leaked: &'static [Code] = Box::leak(codes.into_boxed_slice());349 guard.insert((dimension, base), leaked);350 Ok(leaked)351}352353/// Lists every key of a tier: the designs of every space, the measures of the tier's cost that apply, on the tier's axes.354pub fn keys(tier: Tier) -> Vec<Key> {355 let mut out = Vec::new();356 for (dimension, base) in SPACES {357 let codes = designs(dimension, base).expect("the ledger spaces are walkable");358 for &code in codes {359 for measure in Measure::ALL {360 if measure.cost() != tier.cost() || !measure.applies(dimension, base) {361 continue;362 }363 for &axis in tier.axes() {364 out.push(Key::new(code, dimension, base, measure, axis));365 }366 }367 }368 }369 out370}371372fn attach(key: &Key, terms: &[i128]) -> Option<(&'static Record, i32)> {373 if terms.len() < 4 || terms.iter().all(|&term| term == terms[0]) {374 return None;375 }376 let found = identify(terms);377 found378 .iter()379 .find(|(record, _)| record.key == Some(*key))380 .or(found.first())381 .map(|&(record, shift)| (record, shift - key.axis.start()))382}383384/// Reads one design sequence: its terms within the budget, its closed form and the record it matches.385pub fn sequence(key: &Key, count: usize, cells: u128) -> Result<Sequence> {386 let (terms, capped) = terms(key, count, cells)?;387 let closed = closed(key)?;388 let record = attach(key, &terms);389 let tag = record.map(|(record, _)| {390 if record.key == Some(*key) {391 record.status392 } else {393 Tag::Conjecture394 }395 });396 Ok(Sequence {397 key: *key,398 terms,399 capped,400 closed,401 record,402 tag,403 })404}405406/// Reads every sequence of a tier at the count of terms, within the standing cell budget.407pub fn catalog(tier: Tier, count: usize) -> Vec<Sequence> {408 keys(tier)409 .iter()410 .filter_map(|key| sequence(key, count, BUDGET).ok())411 .collect()412}413414/// Parses integers separated by commas or spaces, or none when a token is not one.415pub fn numbers(text: &str) -> Option<Vec<i128>> {416 let tokens: Vec<&str> = text417 .split(|c: char| c == ',' || c.is_whitespace())418 .filter(|token| !token.is_empty())419 .collect();420 if tokens.is_empty() {421 return None;422 }423 tokens.iter().map(|token| token.parse().ok()).collect()424}425426/// Finds the catalog rows a query names: the rows holding the typed terms as a window, or the rows whose name or record holds the typed fragment.427pub fn search(catalog: &[Sequence], query: &str) -> Vec<usize> {428 let query = query.trim();429 if query.is_empty() {430 return (0..catalog.len()).collect();431 }432 if let Some(window) = numbers(query) {433 return catalog434 .iter()435 .enumerate()436 .filter(|(_, row)| {437 row.terms438 .windows(window.len())439 .any(|w| w == window.as_slice())440 })441 .map(|(index, _)| index)442 .collect();443 }444 let needle = query.to_lowercase();445 catalog446 .iter()447 .enumerate()448 .filter(|(_, row)| row.mentions(&needle))449 .map(|(index, _)| index)450 .collect()451}452453#[cfg(test)]454mod tests {455 use super::*;456457 #[test]458 fn the_classics_read_their_records() {459 let carpet = sequence(&Key::new(7, 2, 2, Measure::Fills, Axis::Side), 4, BUDGET).unwrap();460 assert_eq!(carpet.terms, [8, 21, 40, 65]);461 assert_eq!(carpet.record.map(|(r, s)| (r.id, s)), Some(("A000567", 0)));462 assert_eq!(carpet.tag, Some(Tag::Proved));463 assert_eq!(carpet.closed.unwrap().text(), "3k^2 - 2k");464 let tree = sequence(&Key::new(3, 2, 2, Measure::Fills, Axis::Side), 4, BUDGET).unwrap();465 assert_eq!(tree.terms[..3], [6, 15, 28]);466 assert_eq!(tree.record.map(|(r, s)| (r.id, s)), Some(("A000384", 0)));467 let sponge = sequence(468 &Key::new(23, 3, 2, Measure::Surface, Axis::Level),469 3,470 BUDGET,471 )472 .unwrap();473 assert_eq!(sponge.terms, [72, 1056, 18048]);474 assert_eq!(475 sponge.closed.unwrap().text(),476 "a(level) = 28 a(level-1) - 160 a(level-2)"477 );478 let slice = sequence(479 &Key::new(23, 3, 2, Measure::Triangles, Axis::Level),480 8,481 BUDGET,482 )483 .unwrap();484 assert_eq!(slice.terms, [42, 306, 2250, 16578]);485 assert!(slice.capped);486 assert_eq!(slice.record.map(|(r, s)| (r.id, s)), Some(("A299916", 1)));487 let void = sequence(&Key::new(9, 2, 2, Measure::Voids, Axis::Side), 3, BUDGET).unwrap();488 assert_eq!(void.closed.unwrap().text(), "2k^2 - 2k");489 assert_eq!(void.terms, [4, 12, 24]);490 }491492 #[test]493 fn the_keys_of_the_closed_tier_cover_every_space() {494 let closed = keys(Tier::Closed);495 assert_eq!(closed.len(), 1282 * 3 * 2);496 assert_eq!(keys(Tier::Convolved).len(), (1282 - 3 - 4 - 6 - 8) * 2 * 2);497 assert!(closed.iter().all(|key| key.measure.cost() == Cost::Closed));498 assert!(designs(3, 3).is_err());499 }500501 #[test]502 fn the_search_finds_terms_and_names() {503 let rows: Vec<Sequence> = [504 Key::new(7, 2, 2, Measure::Fills, Axis::Level),505 Key::new(7, 2, 2, Measure::Surface, Axis::Level),506 Key::new(23, 3, 2, Measure::Fills, Axis::Level),507 ]508 .iter()509 .map(|key| sequence(key, 5, BUDGET).unwrap())510 .collect();511 assert_eq!(search(&rows, "64, 512"), [0]);512 assert_eq!(search(&rows, "80 496"), [1]);513 assert_eq!(search(&rows, "dim=3_code=23"), [2]);514 assert_eq!(search(&rows, "A381517"), [1]);515 assert_eq!(search(&rows, "surface"), [1]);516 assert_eq!(search(&rows, ""), [0, 1, 2]);517 assert!(search(&rows, "5, 6, 7").is_empty());518 assert_eq!(numbers("1, 2 3"), Some(vec![1, 2, 3]));519 assert_eq!(numbers("1, x"), None);520 }521522 #[test]523 fn the_closed_forms_spell_themselves() {524 assert_eq!(Closed::Power(8).text(), "8^level");525 assert_eq!(Closed::Difference(9, 8).text(), "9^level - 8^level");526 assert_eq!(Closed::Polynomial(vec![1, -2, 2]).text(), "2k^2 - 2k + 1");527 assert_eq!(Closed::Polynomial(vec![0, 0, 1]).text(), "k^2");528 assert_eq!(Closed::Polynomial(vec![-1, 0, -1]).text(), "-k^2 - 1");529 assert_eq!(Closed::Polynomial(vec![0]).text(), "0");530 assert_eq!(531 Closed::Recurrence(vec![11, -24]).text(),532 "a(level) = 11 a(level-1) - 24 a(level-2)"533 );534 assert_eq!(Closed::Recurrence(vec![1]).text(), "a(level) = a(level-1)");535 assert_eq!(Closed::Recurrence(vec![0]).text(), "a(level) = 0");536 assert_eq!(Axis::Side.place(0, 3), (3, 1));537 assert_eq!(Axis::Level.place(2, 3), (3, 3));538 assert_eq!(Tier::parse("side").unwrap(), Tier::SideGrid);539 assert!(Axis::parse("depth").is_err());540 }541}