ltl.md (10979B)
1 # Linear Temporal Logic 2 3 ## What is LTL? 4 > [!NOTE] 5 > Github's inline $\LaTeX$ is somewhat limited in what it allows you to write, 6 > so some of this notation is really just abusing similar-looking symbols. 7 8 Linear temporal logic is an extension of propositional logic that allows for 9 reasoning over arbitrary time streams. The two core operators it provides are 10 the _next_ operator $\circ \varphi$ that is true iff $\varphi$ is true at the 11 next state and the _until_ operator $\varphi \cup \psi$ that is true iff $\psi$ 12 is true at some future or current state and $\varphi$ is true at all states 13 preceding it. 14 15 The two main operators then allow you to build up more complex operators: 16 - The _eventually_ operator $\diamondsuit$ which is defined as 17 $\diamondsuit \varphi = true \cup \varphi$ 18 - The _henceforth_ operator $\square$ defined as $\lnot \diamondsuit \lnot \varphi$ 19 - The _weak until_ operator $\varphi \text{ W } \psi$ defined as 20 $(\varphi \cup \psi) \lor \square \varphi$ which relaxes the constraint of 21 the until operator that the second condition must be satisfied at some point. 22 - The _release_ operator $\varphi \text{ R } \psi$ is defined as 23 $\lnot(\lnot\varphi \cup \lnot \psi)$ which is true iff $\psi$ always 24 holds until "released" by $\varphi$ being true. 25 26 (the names for these operators can vary - what I've written here is the verbiage 27 `minisql` uses) 28 29 LTL is used primarily for formal verification of systems (which you could 30 imagine in many cases would need to encode time-dependent constraints) and also 31 is partially what the TLA language is built on top of! Probably the most 32 interesting application I saw for it was encoding invariants about parallelism in 33 [Michigan State's slides](https://www.cse.msu.edu/~cse814/Lectures/14_introLTL.pdf) 34 on the topic. Say for example you have a critical section of code guarded by a 35 mutex. Let $\text{inCS}_X$ denote a process $X$ being in the critical section: 36 37 - Mutual exclusion is expressed by: 38 $\square(\lnot \text{inCS}_A \lor \lnot\text{inCS}_B)$ 39 (in English: it is always true that either A is or B is not in the critical 40 section) 41 42 - You can also express that $A$ doesn't monopolize the lock: 43 $\square(\text{inCS}_A \implies \diamondsuit \lnot\text{inCS}_A)$ 44 (in English: it is always true that $A$ holding the lock implies it will 45 eventually not hold the lock). 46 47 Those slides also include some cool formalizations of fairness guarantees for the 48 dining philosophers problem! 49 50 ## Adding Support (this can be skipped) 51 Adding support for the core of LTL is actually not that hard: `minisql` does 52 predicate filtering via the higher-order function `filter`, which normally looks 53 something like this in SML: 54 ```sml 55 fun filter f [] = [] 56 | filter f (x::xs) = if f x then x::(filter f xs) else (filter f xs) 57 ``` 58 59 Since SML has singly-linked lists ("cons cells") as the first class list construct, 60 it becomes very easy to pass in the rest of the list to our predicate function `f`: 61 62 ```sml 63 fun filterWith f [] = [] 64 | filterWith f (x::xs) = if f(x, xs) then x::(filter f xs) else (filter f xs) 65 ``` 66 67 The parser is also implemented in a fashion where adding more unary and binary 68 constructs isn't a big ask. The execution engine now has to track where it "started" 69 (the row that is actually being checked against the predicate), but otherwise these 70 future-oriented predicates can be computed pretty easily by recursing on the rest of 71 the list. 72 73 Once we implement support for next and until, we can just have the parser 74 automatically translate the more complex queries to be in terms of the two basic 75 primitives via "artificial" constructors like: 76 ```sml 77 fun Eventually x = Until(True, x) 78 fun Henceforth x = Not(Eventually(Not x)) 79 fun WeakUntil (x, y) = Or(Until(x, y), Henceforth x) 80 fun Release (x, y) = Not(Until(Not x, Not y)) 81 fun StrongRelease (x, y) = Not(WeakUntil(Not x, Not y)) 82 ``` 83 84 ## Queries 85 Now we can mess with some queries! Note that all of this data can be generated via 86 `randgen.py`: calling `python randgen.py cities` will generate the cities dataset, 87 `python randgen.py sequsers` the sequential user dataset, etc. 88 89 ### Simple Queries 90 Let's start with a simple dataset: say we have a simple business with one product 91 that can meet with up to one client businesses a day. These clients have a limited 92 number of actions as described by the following FSM: 93 94 <p align="center"> 95 <img src="./fsm.png"> 96 </p> 97 98 "New" is short for a client coming in contact with our business, "buy" is them buying 99 our product, "com" is them complaining about it, "ret" is them returning it, and 100 "leave" is them vowing to never work with us again. 101 102 `sequsers.json` is a table where each row represents what happened in a day and the 103 rows are laid out chronologically (e.g. first day is the first row). 104 105 Let's try the simplest possible LTL query: maybe we're interested in predicting what 106 events could indicate that a client is going to buy our product. To do that we'd want 107 all events that _precede_ a buy order: 108 ``` 109 $ ./minisql 110 SELECT * FROM sequsers WHERE NEXT action = 'buy' LIMIT 10; 111 action | name | headcount | 112 new | Sanctologist Corp. | 15221 | 113 buy | Sanctologist Corp. | 43864 | 114 new | Unskilful Corp. | 56921 | 115 new | Wiredancing Corp. | 68559 | 116 new | Dasylirion Corp. | 78640 | 117 new | Unoxidated Corp. | 17631 | 118 buy | Overtures Corp. | 59852 | 119 buy | Clamative Corp. | 75337 | 120 new | Macrospore Corp. | 86109 | 121 buy | Attendant Corp. | 18739 | 122 ``` 123 124 Is this correct? Let's peek at the first 15 rows of the actual data using `viewer.py`: 125 126 ```python 127 $ python viewer.py sequsers.json 15 128 {'action': 'new', 'name': 'Irrevocablaginable Corp.', 'headcount': 65498} 129 {'action': 'new', 'name': 'Psychopathologist Corp.', 'headcount': 43556} 130 {'action': 'new', 'name': 'Ararao Corp.', 'headcount': 39872} 131 {'action': 'new', 'name': 'Avant Corp.', 'headcount': 10441} 132 {'action': 'new', 'name': 'Obverts Corp.', 'headcount': 21201} 133 {'action': 'new', 'name': 'Unstaggering Corp.', 'headcount': 37987} 134 {'action': 'new', 'name': 'Overnobly Corp.', 'headcount': 12356} 135 {'action': 'new', 'name': 'Souterrain Corp.', 'headcount': 29849} 136 {'action': 'new', 'name': 'Upclimbed Corp.', 'headcount': 59171} 137 {'action': 'new', 'name': 'Ungibbet Corp.', 'headcount': 60191} 138 {'action': 'new', 'name': 'Sanctologist Corp.', 'headcount': 15221} 139 {'action': 'buy', 'name': 'Sanctologist Corp.', 'headcount': 43864} 140 {'action': 'buy', 'name': 'Ungibbet Corp.', 'headcount': 91063} 141 {'action': 'new', 'name': 'Dampnesses Corp.', 'headcount': 50746} 142 {'action': 'new', 'name': 'Protopectinase Corp.', 'headcount': 55577} 143 ``` 144 145 The first two buy actions line up with our output! 146 147 Let's try some more sample queries: 148 - Get all returns that are followed by a buy order. 149 ```sql 150 SELECT name FROM sequsers WHERE action = 'return' AND EVENTUALLY action = 'buy'; 151 ``` 152 - Get all chains of new users followed by a buy order: 153 ```sql 154 SELECT name, headcount FROM sequsers WHERE action = 'new' UNTIL action = 'buy'; 155 ``` 156 - Get all events preceding large clients leaving us: 157 ```sql 158 SELECT * FROM sequsers WHERE NEXT (action = 'leave' AND headcount > 10000); 159 ``` 160 161 Okay, let's try actually doing the query mentioned in the challenge ("get all 162 customers who returned product within 2 weeks"). The best we can do with plain old 163 linear temporal logic is something like: 164 ```sql 165 SELECT * FROM sequsers WHERE action = 'buy' AND (action = 'leave' WITHIN 14); 166 ``` 167 Here the `WITHIN` operator is syntactic sugar over nested `Or` and `Next` clauses: 168 ```sml 169 fun Within (x, 0) = x 170 | Within (x, i) = Or(Next(x), Next(Within(x, i-1))) 171 ``` 172 173 Yet this isn't really what we want: all this results in is all events that are within 174 14 events of some client returning our product. We just want the event where said 175 client bought it (if it is within 14 events!). Unfortunately vanilla LTL doesn't 176 support this as it is very much future-oriented and applies predicates independent of 177 time to several states across a time stream. 178 179 ### Temporally Dependent Queries 180 Some variants of LTL solve the problem we had earlier by introducing operators that 181 let you refer to _past_ values. This is a bit overkill for our concerns and it also 182 would make the code a bit less pleasant. Instead, `minisql` introduces the notion of 183 the "current" row for use in predicates. 184 185 For example, we can query all users who returned our product with: 186 ```sql 187 SELECT * FROM sequsers WHERE action = 'buy' AND EVENTUALLY 188 (name = cur.name AND action = 'return'); 189 ``` 190 While `name` and `action` columns will refer to the columns of the state currently 191 being checked at any point in the timestream, `cur.name` refers to the column of the 192 current state. 193 194 We can also write the earlier query now: 195 ```sql 196 SELECT * FROM sequsers WHERE action = 'buy' AND 197 ((name = cur.name AND action = 'return') WITHIN 14); 198 ``` 199 200 In general this expands the number of actually meaningful predicates we can make. Say 201 we're interested in all the complaints that come from companies who have shrunk their 202 headcounts since they bought our product (maybe they're just haggling to cut 203 expenses...) 204 ```sql 205 SELECT name FROM sequsers WHERE action = 'buy' AND EVENTUALLY 206 (name = cur.name AND (action = 'return' AND headcount < cur.headcount)); 207 ``` 208 209 ### The Query You Asked For 210 Okay, but there's still a big constraint on our data here: only one event can happen 211 per day (or alternatively our only notion of time is the event number). Let's now 212 consider `users.json` which is exactly the same as `sequsers.json` except for the 213 fact that there is now a `time` column with the time of the event as a Unix 214 timestamp. Combining the very limited time operations provided by `minisql` with the 215 temporally dependent queries lets us finally do this the _right_ way. 216 217 To get all clients who bought and returned their product within two weeks, write 218 ```sql 219 SELECT name, headcount FROM users WHERE action = 'buy' AND EVENTUALLY 220 (name = cur.name AND (action = 'return' AND time - cur.time < 2 weeks)); 221 ``` 222 On my randomly generated copy, this returns 223 ``` 224 name | headcount | 225 Mathurin Corp. | 75192 | 226 Ferrelling Corp. | 76831 | 227 Preoperating Corp. | 20880 | 228 ``` 229 230 Sure enough, `grep`ping for "Mathurin" yields they did buy and return within about a week: 231 ```python 232 $ python viewer.py users.json 1000 | grep Mathurin 233 {'time': 1725139942, 'action': 'new', 'name': 'Mathurin Corp.', 'headcount': 96156} 234 {'time': 1755384672, 'action': 'buy', 'name': 'Mathurin Corp.', 'headcount': 75192} 235 {'time': 1755982565, 'action': 'return', 'name': 'Mathurin Corp.', 'headcount': 5771} 236 {'time': 1789821027, 'action': 'leave', 'name': 'Mathurin Corp.', 'headcount': 20720} 237 ``` 238 239 This is not hardcoded: the parser will recognize any of "seconds", "minutes", 240 "hours", "days", or "weeks" and the query engine prevents you from mixing up integers 241 and timestamps, but beyond that the time system is not fully realized.