lib.rs
17.5 kB · rust · 544 lines
1//! The sequence registry: every measure of every design as a sequence, the curated records, and the page they render.2#![deny(missing_docs)]34/// The markdown page the ledger renders.5pub mod markdown;6/// The measures and their cost classes.7pub mod measure;8/// The curated OEIS records and the identification against them.9pub mod records;10/// The term generators and the closed forms.11pub mod terms;1213pub use markdown::markdown;14pub use measure::{Cost, Measure};15pub use records::{identify, Record, RECORDS};16pub use terms::{closed, fill_polynomial, terms};1718use mrlyrs::core::error::{value_error, Result};19use mrlyrs::math::bang::baseq;20use mrlyrs::math::name::{Bang, Named, Sequence as SequenceName};21use std::collections::BTreeMap;22use std::sync::{Mutex, OnceLock};2324type Walked = BTreeMap<(usize, usize), &'static [u128]>;2526/// The dimension and base pairs the ledger walks.27pub const SPACES: [(usize, usize); 9] = [28 (1, 2),29 (2, 2),30 (3, 2),31 (4, 2),32 (1, 3),33 (2, 3),34 (1, 4),35 (2, 4),36 (1, 5),37];3839/// The cells a catalog row may render for one term.40pub const BUDGET: u128 = 500_000;4142/// The terms a catalog row holds.43pub const TERMS: usize = 8;4445/// The index a sequence runs along.46#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]47pub enum Axis {48 /// The fractal level from 1, at side `max(base, 3)`.49 Level,50 /// The odd side `2k - 1` from `k = 2`, at level 1.51 Side,52}5354impl Axis {55 /// Both axes, level first.56 pub const ALL: [Axis; 2] = [Axis::Level, Axis::Side];57 /// Returns the axis's one-word name.58 pub fn slug(self) -> &'static str {59 match self {60 Axis::Level => "level",61 Axis::Side => "side",62 }63 }64 /// Parses a one-word name back into its axis, or an error for any other word.65 pub fn parse(slug: &str) -> Result<Axis> {66 match slug {67 "level" => Ok(Axis::Level),68 "side" => Ok(Axis::Side),69 other => value_error(format!("unknown axis {other:?}.")),70 }71 }72 /// Returns the ledger index of the first term: the level 1 or the `k` of side 3.73 pub fn start(self) -> i32 {74 match self {75 Axis::Level => 1,76 Axis::Side => 2,77 }78 }79 /// Returns the side number and level of the term at the index.80 pub fn place(self, index: usize, number: usize) -> (usize, u32) {81 match self {82 Axis::Level => (number, index as u32 + 1),83 Axis::Side => (2 * index + 3, 1),84 }85 }86}8788/// The status a claim carries.89#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]90pub enum Tag {91 /// Proved on this tree.92 Proved,93 /// Checked against a record or a second generator.94 Verified,95 /// Stated and not yet checked.96 Conjecture,97 /// Checked and found false.98 Refuted,99}100101impl Tag {102 /// Returns the tag's capitalised word.103 pub fn text(self) -> &'static str {104 match self {105 Tag::Proved => "Proved",106 Tag::Verified => "Verified",107 Tag::Conjecture => "Conjecture",108 Tag::Refuted => "Refuted",109 }110 }111}112113/// A design sequence's address: the design, the measure and the axis.114#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]115pub struct Key {116 /// The design's code.117 pub code: u128,118 /// The design's dimension.119 pub dimension: usize,120 /// The numeral base of the corners.121 pub base: usize,122 /// The reading taken.123 pub measure: Measure,124 /// The index the reading runs along.125 pub axis: Axis,126}127128impl Key {129 /// Pins a design sequence to its address.130 pub const fn new(131 code: u128,132 dimension: usize,133 base: usize,134 measure: Measure,135 axis: Axis,136 ) -> Key {137 Key {138 code,139 dimension,140 base,141 measure,142 axis,143 }144 }145 /// Returns the sequence name this key addresses.146 pub fn named(&self) -> SequenceName {147 SequenceName::new(148 self.code,149 self.dimension,150 self.base,151 self.measure.slug(),152 self.axis.slug(),153 )154 }155 /// Returns the sequence's file name.156 ///157 /// ```158 /// use ledger::{Axis, Key, Measure};159 /// let key = Key::new(23, 3, 2, Measure::Surface, Axis::Level);160 /// assert_eq!(key.name(), "sequence_dim=3_code=23_measure=surface_axis=level");161 /// assert_eq!(key.id(), "753b6b49");162 /// ```163 pub fn name(&self) -> String {164 self.named().to_file().unwrap_or_default()165 }166 /// Returns the first eight hex digits of the sha256 of the sequence's canonical JSON.167 pub fn id(&self) -> String {168 self.named().to_id()169 }170 /// Returns the design pinned to its dimension and base.171 pub fn design(&self) -> Bang {172 Bang::new(self.code, self.dimension, self.base)173 }174 /// Returns the side number the level axis runs at.175 pub fn number(&self) -> usize {176 self.base.max(3)177 }178}179180/// A closed form of a sequence.181#[derive(Clone, Debug, PartialEq, Eq)]182pub enum Closed {183 /// `fill^level`.184 Power(u128),185 /// `cells^level - fill^level`.186 Difference(u128, u128),187 /// A polynomial in `k` by rising power, at side `2k - 1`.188 Polynomial(Vec<i128>),189 /// `a(level) = c[0] a(level-1) + c[1] a(level-2) + ...`.190 Recurrence(Vec<i128>),191}192193fn signed(text: &mut String, coefficient: i128, first: bool) {194 if first {195 if coefficient < 0 {196 text.push('-');197 }198 } else {199 text.push_str(if coefficient < 0 { " - " } else { " + " });200 }201}202203impl Closed {204 /// Spells the closed form.205 pub fn text(&self) -> String {206 match self {207 Closed::Power(f) => format!("{f}^level"),208 Closed::Difference(g, f) => format!("{g}^level - {f}^level"),209 Closed::Polynomial(coefficients) => {210 let mut text = String::new();211 for (power, &c) in coefficients.iter().enumerate().rev() {212 if c == 0 {213 continue;214 }215 let first = text.is_empty();216 signed(&mut text, c, first);217 if c.abs() != 1 || power == 0 {218 text.push_str(&c.abs().to_string());219 }220 match power {221 0 => {}222 1 => text.push('k'),223 _ => text.push_str(&format!("k^{power}")),224 }225 }226 if text.is_empty() {227 text.push('0');228 }229 text230 }231 Closed::Recurrence(coefficients) => {232 let mut text = String::new();233 for (back, &c) in coefficients.iter().enumerate() {234 if c == 0 {235 continue;236 }237 let first = text.is_empty();238 signed(&mut text, c, first);239 if c.abs() != 1 {240 text.push_str(&c.abs().to_string());241 text.push(' ');242 }243 text.push_str(&format!("a(level-{})", back + 1));244 }245 if text.is_empty() {246 text.push('0');247 }248 format!("a(level) = {text}")249 }250 }251 }252}253254/// A cost tier of the catalog.255#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]256pub enum Tier {257 /// The closed measures on both axes.258 Closed,259 /// The profile measures on both axes.260 Convolved,261 /// The grid measures on the side axis.262 SideGrid,263 /// The grid measures on the level axis.264 LevelGrid,265}266267impl Tier {268 /// Every tier, cheapest first.269 pub const ALL: [Tier; 4] = [270 Tier::Closed,271 Tier::Convolved,272 Tier::SideGrid,273 Tier::LevelGrid,274 ];275 /// Returns the tier's one-word name.276 pub fn slug(self) -> &'static str {277 match self {278 Tier::Closed => "closed",279 Tier::Convolved => "convolved",280 Tier::SideGrid => "side",281 Tier::LevelGrid => "level",282 }283 }284 /// Parses a one-word name back into its tier, or an error for any other word.285 pub fn parse(slug: &str) -> Result<Tier> {286 Tier::ALL287 .into_iter()288 .find(|tier| tier.slug() == slug)289 .map_or_else(|| value_error(format!("unknown tier {slug:?}.")), Ok)290 }291 fn cost(self) -> Cost {292 match self {293 Tier::Closed => Cost::Closed,294 Tier::Convolved => Cost::Convolved,295 _ => Cost::Grid,296 }297 }298 fn axes(self) -> &'static [Axis] {299 match self {300 Tier::SideGrid => &[Axis::Side],301 Tier::LevelGrid => &[Axis::Level],302 _ => &Axis::ALL,303 }304 }305}306307/// One row of the catalog: a design sequence with its terms, its closed form and its record.308#[derive(Clone, Debug, PartialEq, Eq)]309pub struct Sequence {310 /// The sequence's address.311 pub key: Key,312 /// The first terms.313 pub terms: Vec<i128>,314 /// Whether the cell budget or a u128 stopped the terms short.315 pub capped: bool,316 /// The closed form, when one is known.317 pub closed: Option<Closed>,318 /// The record the terms match, with the record's index less the ledger's.319 pub record: Option<(&'static Record, i32)>,320 /// The status of the match: the record's when the record names this key, else a collision to explain.321 pub tag: Option<Tag>,322}323324impl Sequence {325 fn mentions(&self, needle: &str) -> bool {326 self.key.name().contains(needle)327 || self.record.is_some_and(|(record, _)| {328 record.id.to_lowercase().contains(needle)329 || record.name.to_lowercase().contains(needle)330 })331 }332}333334/// 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.335///336/// ```337/// assert_eq!(ledger::designs(2, 2).unwrap(), [0, 1, 3, 6, 7, 15]);338/// assert_eq!(ledger::designs(2, 3).unwrap().len(), 26);339/// ```340pub fn designs(dimension: usize, base: usize) -> Result<&'static [u128]> {341 static CACHE: OnceLock<Mutex<Walked>> = OnceLock::new();342 let cache = CACHE.get_or_init(|| Mutex::new(BTreeMap::new()));343 let mut guard = cache.lock().expect("the design cache is not poisoned");344 if let Some(codes) = guard.get(&(dimension, base)) {345 return Ok(codes);346 }347 let codes: Vec<u128> = baseq::representatives(base, dimension)?348 .into_iter()349 .map(|(code, _)| code.get())350 .collect();351 let leaked: &'static [u128] = Box::leak(codes.into_boxed_slice());352 guard.insert((dimension, base), leaked);353 Ok(leaked)354}355356/// 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.357pub fn keys(tier: Tier) -> Vec<Key> {358 let mut out = Vec::new();359 for (dimension, base) in SPACES {360 let codes = designs(dimension, base).expect("the ledger spaces are walkable");361 for &code in codes {362 for measure in Measure::ALL {363 if measure.cost() != tier.cost() || !measure.applies(dimension, base) {364 continue;365 }366 for &axis in tier.axes() {367 out.push(Key::new(code, dimension, base, measure, axis));368 }369 }370 }371 }372 out373}374375fn attach(key: &Key, terms: &[i128]) -> Option<(&'static Record, i32)> {376 if terms.len() < 4 || terms.iter().all(|&term| term == terms[0]) {377 return None;378 }379 let found = identify(terms);380 found381 .iter()382 .find(|(record, _)| record.key == Some(*key))383 .or(found.first())384 .map(|&(record, shift)| (record, shift - key.axis.start()))385}386387/// Reads one design sequence: its terms within the budget, its closed form and the record it matches.388pub fn sequence(key: &Key, count: usize, cells: u128) -> Result<Sequence> {389 let (terms, capped) = terms(key, count, cells)?;390 let closed = closed(key)?;391 let record = attach(key, &terms);392 let tag = record.map(|(record, _)| {393 if record.key == Some(*key) {394 record.status395 } else {396 Tag::Conjecture397 }398 });399 Ok(Sequence {400 key: *key,401 terms,402 capped,403 closed,404 record,405 tag,406 })407}408409/// Reads every sequence of a tier at the count of terms, within the standing cell budget.410pub fn catalog(tier: Tier, count: usize) -> Vec<Sequence> {411 keys(tier)412 .iter()413 .filter_map(|key| sequence(key, count, BUDGET).ok())414 .collect()415}416417/// Parses integers separated by commas or spaces, or none when a token is not one.418pub fn numbers(text: &str) -> Option<Vec<i128>> {419 let tokens: Vec<&str> = text420 .split(|c: char| c == ',' || c.is_whitespace())421 .filter(|token| !token.is_empty())422 .collect();423 if tokens.is_empty() {424 return None;425 }426 tokens.iter().map(|token| token.parse().ok()).collect()427}428429/// 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.430pub fn search(catalog: &[Sequence], query: &str) -> Vec<usize> {431 let query = query.trim();432 if query.is_empty() {433 return (0..catalog.len()).collect();434 }435 if let Some(window) = numbers(query) {436 return catalog437 .iter()438 .enumerate()439 .filter(|(_, row)| {440 row.terms441 .windows(window.len())442 .any(|w| w == window.as_slice())443 })444 .map(|(index, _)| index)445 .collect();446 }447 let needle = query.to_lowercase();448 catalog449 .iter()450 .enumerate()451 .filter(|(_, row)| row.mentions(&needle))452 .map(|(index, _)| index)453 .collect()454}455456#[cfg(test)]457mod tests {458 use super::*;459460 #[test]461 fn the_classics_read_their_records() {462 let carpet = sequence(&Key::new(7, 2, 2, Measure::Fills, Axis::Side), 4, BUDGET).unwrap();463 assert_eq!(carpet.terms, [8, 21, 40, 65]);464 assert_eq!(carpet.record.map(|(r, s)| (r.id, s)), Some(("A000567", 0)));465 assert_eq!(carpet.tag, Some(Tag::Proved));466 assert_eq!(carpet.closed.unwrap().text(), "3k^2 - 2k");467 let tree = sequence(&Key::new(3, 2, 2, Measure::Fills, Axis::Side), 4, BUDGET).unwrap();468 assert_eq!(tree.terms[..3], [6, 15, 28]);469 assert_eq!(tree.record.map(|(r, s)| (r.id, s)), Some(("A000384", 0)));470 let sponge = sequence(471 &Key::new(23, 3, 2, Measure::Surface, Axis::Level),472 3,473 BUDGET,474 )475 .unwrap();476 assert_eq!(sponge.terms, [72, 1056, 18048]);477 assert_eq!(478 sponge.closed.unwrap().text(),479 "a(level) = 28 a(level-1) - 160 a(level-2)"480 );481 let slice = sequence(482 &Key::new(23, 3, 2, Measure::Triangles, Axis::Level),483 8,484 BUDGET,485 )486 .unwrap();487 assert_eq!(slice.terms, [42, 306, 2250, 16578]);488 assert!(slice.capped);489 assert_eq!(slice.record.map(|(r, s)| (r.id, s)), Some(("A299916", 1)));490 let void = sequence(&Key::new(9, 2, 2, Measure::Voids, Axis::Side), 3, BUDGET).unwrap();491 assert_eq!(void.closed.unwrap().text(), "2k^2 - 2k");492 assert_eq!(void.terms, [4, 12, 24]);493 }494495 #[test]496 fn the_keys_of_the_closed_tier_cover_every_space() {497 let closed = keys(Tier::Closed);498 assert_eq!(closed.len(), 1282 * 3 * 2);499 assert_eq!(keys(Tier::Convolved).len(), (1282 - 3 - 4 - 6 - 8) * 2 * 2);500 assert!(closed.iter().all(|key| key.measure.cost() == Cost::Closed));501 assert!(designs(3, 3).is_err());502 }503504 #[test]505 fn the_search_finds_terms_and_names() {506 let rows: Vec<Sequence> = [507 Key::new(7, 2, 2, Measure::Fills, Axis::Level),508 Key::new(7, 2, 2, Measure::Surface, Axis::Level),509 Key::new(23, 3, 2, Measure::Fills, Axis::Level),510 ]511 .iter()512 .map(|key| sequence(key, 5, BUDGET).unwrap())513 .collect();514 assert_eq!(search(&rows, "64, 512"), [0]);515 assert_eq!(search(&rows, "80 496"), [1]);516 assert_eq!(search(&rows, "dim=3_code=23"), [2]);517 assert_eq!(search(&rows, "A381517"), [1]);518 assert_eq!(search(&rows, "surface"), [1]);519 assert_eq!(search(&rows, ""), [0, 1, 2]);520 assert!(search(&rows, "5, 6, 7").is_empty());521 assert_eq!(numbers("1, 2 3"), Some(vec![1, 2, 3]));522 assert_eq!(numbers("1, x"), None);523 }524525 #[test]526 fn the_closed_forms_spell_themselves() {527 assert_eq!(Closed::Power(8).text(), "8^level");528 assert_eq!(Closed::Difference(9, 8).text(), "9^level - 8^level");529 assert_eq!(Closed::Polynomial(vec![1, -2, 2]).text(), "2k^2 - 2k + 1");530 assert_eq!(Closed::Polynomial(vec![0, 0, 1]).text(), "k^2");531 assert_eq!(Closed::Polynomial(vec![-1, 0, -1]).text(), "-k^2 - 1");532 assert_eq!(Closed::Polynomial(vec![0]).text(), "0");533 assert_eq!(534 Closed::Recurrence(vec![11, -24]).text(),535 "a(level) = 11 a(level-1) - 24 a(level-2)"536 );537 assert_eq!(Closed::Recurrence(vec![1]).text(), "a(level) = a(level-1)");538 assert_eq!(Closed::Recurrence(vec![0]).text(), "a(level) = 0");539 assert_eq!(Axis::Side.place(0, 3), (3, 1));540 assert_eq!(Axis::Level.place(2, 3), (3, 3));541 assert_eq!(Tier::parse("side").unwrap(), Tier::SideGrid);542 assert!(Axis::parse("depth").is_err());543 }544}