minisql

small tool to query json files with ltl-flavored sql queries
Log | Files | Refs | README

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.