Skip to main content

somatize_core/
fact.rs

1//! What happened while a plan ran: the vocabulary of level 1, the engine's.
2//!
3//! A fact and not a judgement. Whether 400 ms is slow or a gradient is dying is
4//! an opinion about the record, and the invariant is that the opinion has to be
5//! reproducible from the record without running again.
6//!
7//! An enum because the set is closed and the engine knows it. What the original
8//! got wrong was not the number of variants but putting three vocabularies in
9//! one — a fact beside an opinion about facts. Here each level keeps its own:
10//! this is the engine's, a training run's is Python's, a study's is a record on
11//! disk. **They do not meet in Rust, they meet in the record**, and
12//! [`Fact::flattened`] is that meeting: a name and text-to-text pairs.
13//!
14//! Every measurement is a [`Duration`] and never an instant. A duration from
15//! another machine is worth reading; two wall clocks disagree. **When** it was
16//! written down is the store's business, and it stamps it.
17
18use crate::{Device, Host, Key, NodeId};
19use std::time::Duration;
20
21/// One thing the engine saw.
22#[derive(Debug, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Fact {
25    /// A node was advanced, and answered.
26    Ran {
27        /// Which one.
28        node: NodeId,
29        /// How long after this run started it began. An offset into a slice
30        /// is a fact about the slice, so one that ran elsewhere counts from its
31        /// own start and a timeline adds the [`Fact::Left`] it arrived under.
32        began: Duration,
33        /// How long its `forward` took. Whatever it did in there is inside
34        /// that number: the engine does not look inside a node.
35        took: Duration,
36        /// Where it was told to run, if it was told.
37        device: Option<Device>,
38    },
39    /// A node was advanced and did not answer. Emitted **before** the run
40    /// stops, so a watcher learns which node while it is happening.
41    Failed {
42        /// Which one.
43        node: NodeId,
44        /// What it said.
45        why: String,
46    },
47    /// A node was not run because nobody needed what it makes.
48    ///
49    /// A fact and not an absence: a node missing from a record cannot be told
50    /// from one that was never in the graph.
51    Spared {
52        /// Which one.
53        node: NodeId,
54    },
55    /// A node was not advanced at all: what it would have produced was already
56    /// kept under that name.
57    Recalled {
58        /// Which one.
59        node: NodeId,
60        /// The name it was found under.
61        key: Key,
62    },
63    /// A node ran and what it produced was written down.
64    Kept {
65        /// Which one.
66        node: NodeId,
67        /// The name it was written under.
68        key: Key,
69    },
70    /// A node that maps over its items, item by item — it runs the new ones
71    /// and reads the rest back, so one number would not say what happened.
72    Items {
73        /// Which one.
74        node: NodeId,
75        /// How many items it was given.
76        of: usize,
77        /// How many of them did not have to be computed.
78        recalled: usize,
79    },
80    /// A slice of the plan crossed to another machine, and came back. `took`
81    /// is the whole round trip, which is not the sum of what happened there.
82    Left {
83        /// Whose machine.
84        host: Host,
85        /// How long after this run started it left.
86        began: Duration,
87        /// How long the round trip took.
88        took: Duration,
89    },
90    /// And this is what happened over there. Recursive, so a slice that
91    /// carried on to a third host still says where each thing happened and
92    /// nothing that travelled is rewritten; flattening turns it into a `host`.
93    Elsewhere {
94        /// Whose machine.
95        host: Host,
96        /// What it saw there.
97        saw: Box<Fact>,
98    },
99    /// A level that is **not** the engine had something to say, already flat.
100    ///
101    /// The carrier and not the vocabulary: the core does not learn what a load
102    /// average is, only that other levels exist and one may be speaking from
103    /// another machine. Not for level 2, whose loss is computed where the
104    /// notebook is and goes straight into the record.
105    Said {
106        /// What kind of thing it is, which is what it will be written down as.
107        kind: String,
108        /// And its fields, text to text, already in the written form.
109        pairs: Vec<(String, String)>,
110    },
111    /// The whole thing is over. Emitted by [`Executor::run`](crate::Executor::run)
112    /// and not by [`resume`](crate::Executor::resume): a slice is not a
113    /// `forward`. It is what tells a writer where one record ends.
114    Finished {
115        /// How long all of it took.
116        took: Duration,
117    },
118    /// ...or it is over because of this: the other terminal fact, so a record
119    /// is closed either way. A node that failed said so as [`Fact::Failed`].
120    Broke {
121        /// What stopped it.
122        why: String,
123    },
124}
125
126impl Fact {
127    /// This fact as a name and text-to-text fields: **how it is written down**,
128    /// which is not how it is emitted.
129    ///
130    /// [`Fact::Elsewhere`] does not survive as a name — it becomes a `host`
131    /// field on whatever it wrapped, so a reader gets columns and not a tree.
132    pub fn flattened(&self) -> (&str, Vec<(String, String)>) {
133        match self {
134            Self::Ran {
135                node,
136                began,
137                took,
138                device,
139            } => {
140                let mut said = vec![
141                    ("node".into(), node.to_string()),
142                    began_us(began),
143                    took_us(took),
144                ];
145                if let Some(device) = device {
146                    said.push(("device".into(), device.to_string()));
147                }
148                ("ran", said)
149            }
150            Self::Failed { node, why } => (
151                "failed",
152                vec![
153                    ("node".into(), node.to_string()),
154                    ("why".into(), why.clone()),
155                ],
156            ),
157            Self::Spared { node } => ("spared", vec![("node".into(), node.to_string())]),
158            Self::Recalled { node, key } => (
159                "recalled",
160                vec![
161                    ("node".into(), node.to_string()),
162                    ("key".into(), key.to_string()),
163                ],
164            ),
165            Self::Kept { node, key } => (
166                "kept",
167                vec![
168                    ("node".into(), node.to_string()),
169                    ("key".into(), key.to_string()),
170                ],
171            ),
172            Self::Items { node, of, recalled } => (
173                "items",
174                vec![
175                    ("node".into(), node.to_string()),
176                    ("of".into(), of.to_string()),
177                    ("recalled".into(), recalled.to_string()),
178                ],
179            ),
180            Self::Left { host, began, took } => (
181                "left",
182                vec![
183                    ("host".into(), host.to_string()),
184                    began_us(began),
185                    took_us(took),
186                ],
187            ),
188            // Already flat, and reshaping it here would be this crate deciding
189            // something about a vocabulary it does not know.
190            Self::Said { kind, pairs } => (kind.as_str(), pairs.clone()),
191            Self::Elsewhere { host, saw } => {
192                let (kind, mut said) = saw.flattened();
193                // Last, so that a fact which crossed two machines keeps the
194                // nearest host last and the reader sees the route in order.
195                said.push(("host".into(), host.to_string()));
196                (kind, said)
197            }
198            Self::Finished { took } => ("finished", vec![took_us(took)]),
199            Self::Broke { why } => ("broke", vec![("why".into(), why.clone())]),
200        }
201    }
202
203    /// Whether this fact ends a run, whichever way. Asked by whoever writes
204    /// records so it does not have to know the vocabulary.
205    pub fn ends_a_run(&self) -> bool {
206        matches!(self, Self::Finished { .. } | Self::Broke { .. })
207    }
208}
209
210/// A duration as whole microseconds — an integer, because this is text somebody
211/// reads with `cat` and something else parses.
212fn took_us(took: &Duration) -> (String, String) {
213    ("took_us".into(), took.as_micros().to_string())
214}
215
216/// And where it sat on the run's own timeline, which is what makes a picture of
217/// *what ran when* possible at all.
218fn began_us(began: &Duration) -> (String, String) {
219    ("began_us".into(), began.as_micros().to_string())
220}