Fork me on GitHub
#missionary
<
2023-10-05
>
nivekuil00:10:05

writing a little pid controller thing for animations; this works but it would constantly burn up cycles every tick of the clock. how would you pause this when the error is below a certain threshold, and resume it when setpoint changes?

(let [curr (atom 0)
           setpoint 100
           force 30
           clock (y/seed (repeat 10 5))]
       (->> (y/ap (let [dt (y/?> clock)
                        err (- setpoint @curr)
                        delta (* err dt 0.01 force)]
                    (println dt err)
                    {:val (swap! curr + delta)
                     :delta delta}))
            (y/reduce (fn [_ _] nil) nil)
            y/?))

leonoel09:10:54

The idea is to build a feedback loop and apply the correction only after checking the error threshold. If the control logic is stable, the propagation will eventually stop due to cp's work-skipping optimization : if an input emits the same value twice, the body is not rerun and the previous value is reused. It would not work with ap because this optimization doesn't make sense in discrete time (edit : not sure about that actually).

(require '[missionary.core :as m])

(defn update-after [x d f & args]
  (m/ap (m/amb x (m/? (m/sleep d (apply f x args))))))

(def dt 5)
(def eps 0.001)

(defn pid [k > (pid 0.001 (m/watch !setpoint) (m/watch !force))
     (m/reduce (fn [_ x] (prn x)) nil))
   prn prn))

(swap! !setpoint + 10)
(cancel)

❤️ 1
Dustin Getz12:10:27

@U797MAJ8M as you are an Electric user i think this can/should be done in Electric, e.g.

nivekuil20:10:07

@U09K620SG I want to learn missionary more and i think this is generally useful outside of electric too, also it's a bit more complex than your example since that does all the work eagerly instead of triggering on a clock

👍 1
nivekuil20:10:30

I think this is what I want. so it's necessary to call reset! on !curr even when it's a no-op? (edit: doesn't seem like it matters)

(def eps 0.1)
  (def !clock (atom 5))

  (defn pid [k 

nivekuil20:10:11

or does doing it this way not get the optimization? not sure what "if an input emits the same value twice" means actually...

nivekuil20:10:26

i mean it seems to stop printing "ERROR:" when it hits the threshold but I have no idea why it works 😛

nivekuil22:10:26

@U09K620SG another benefit of doing it in missionary is you can pre-compile animations to waapi keyframes to offload them for dom

nivekuil22:10:36

don't think you can parameterize electric as easily

Dustin Getz22:10:33

continuous operators that fmap clojure fns, like m/latest and m/cp, will “work skip” if the input flows notify but with unchanged values, and decline to run the clojure fn, instead reusing and propagating the cached prior value forward

nivekuil22:10:38

ah, so my implementation still wastes cycles on the clock. back to the drawing board

nivekuil22:10:46

I guess I could sample setpoint , that only triggers on a clock in practice or just debounce it

Dustin Getz22:10:20

yes looks so, though if the cpu is idle it may not be a big deal

nivekuil22:10:18

I don't like it, I want basically every value to be animated. just need to get setpoint to open some kind of gate that gets closed when the controller stabilizes

Dustin Getz22:10:53

makes sense to me

nivekuil18:10:10

?< + reset!+ watch is basically a goto right? feels weird

👍 1
leonoel18:10:10

that feels imperative and wrong, there must be a better way

nivekuil18:10:08

sadly it still used up my last brain cell

leonoel13:10:22

I think this pattern is correct, I added another animation example with an easing function

nivekuil17:10:10

how long did that take to write? I think I still don't understand when signal is needed

Dustin Getz18:10:38

signal is for creating dataflow diamond topology, it allocates a memo buffer so that the signal (flow) value can be shared across multiple consumers. It’s a DAG builder primitive, without it you have supervision trees not DAGs. Without signal, the flow, eg <time, will be booted N times for N consumers. So you’d have N clock instances. Missionary flows are deliberately RT so that you can reuse them and boot them many times if you want to. This is a feature not a bug, it gives you the ability to opt in to sharing and do so at explicit points, thereby allowing you to express either topology. That means without m.signal a flow’s computation and effects will recompute and rerun for each instance of the flow. m/signal ensures that a continuous flow is booted once and the result is saved and shared across all consumers. (implying allocation of the memo buffer.) each Electric let compiles to m/signal. Leo please tell me if I have made a mistake

👍 1
leonoel08:10:39

> how long did that take to write? It took me a while to grok the idea of cyclic reaction but now I'm used to it and I find it rather straightforward. It is relatively close to EE, you just need to make sure the system is stable

nivekuil08:10:23

certainly prettier than verilog. I wonder if the overhead of missionary here is small enough that this is a viable signal processing dsl

👍 1
leonoel09:10:02

good question indeed

Dustin Getz10:10:45

we are using electric to author a robotics control system in a client engagement (highly experimental, the compile time DAG structure with cycles appears to be exactly right but the runtime execution model probably ought to be discrete)

Dustin Getz10:10:31

things like how to compile the simulation model to gpu optimized math array operators is an open question as well (unclear at what point this is actually needed as we are not, like, training LLMs or anything like that)

Dustin Getz10:10:23

& yes, reset! is basically reactive goto though note there is still the key structure- the process supervision tracks through it

bahulneel16:10:46

Has anyone had any recent experience using missionary in clerk notebooks? I was hoping to use it to make notes as I explore the api but because of the execution model I think I’m hitting some rough edges which is just confusing my understanding of the model.

Dustin Getz16:10:51

i've used nextjournal, make sure to use RCF for async examples https://nextjournal.com/dustingetz/missionary-relieve-backpressure . Can you say more about how clerk is different? I did use it once last year

bahulneel16:10:06

I’m not sure it is any different since you last used it, I guess I was being hopeful.

bahulneel16:10:03

I think I’m going to stick with the repl until I have a better understanding of the missionary primatives

Dustin Getz16:10:35

Ok, even still i highly recommend RCF for learning missionary at the repl. Here are more examples https://github.com/hyperfiddle/electric/blob/master/src-docs/user/missionary/missionary_tutorial_object.cljc (siblings to this file)

Dustin Getz16:10:00

they dont run in CI so let us know of any failures

bahulneel16:10:42

That’s really helpful, thanks.

bahulneel16:10:34

RCF looks like exactly what I’m looking for

🙂 1
bahulneel13:10:41

Quick question, do I have to do anything different to see the test output of RCF in a clerk notebook?

bahulneel13:10:48

Right now I’m just seeing the return value

Dustin Getz14:10:03

the green check is printing to stdout and in the console. I don't know what Clerk does with stdout. NextJournal captures the stdout and prints it in the notebook as you can see in my notebook linked above

bahulneel14:10:33

ok, that explains the difference. thanks

Dustin Getz14:10:20

surely you are not the first to want Clerk to do this, I would ask

bahulneel14:10:35

Surely, but I’m not in a position to wait for an answer right now. Once I have more free time I’ll go back and get some answers 😁

bahulneel14:10:15

I’m thankful for your fast responses, you’ve got me unstuck a few times now

🙂 1
Dustin Getz18:10:53

this is just liveview right?

Ben Sless19:10:41

I'm not familiar with liveview so I can't say

nivekuil23:10:04

is there a way to shut down leaked processes that you forgot to cancel, like when you lose the reference to a running flow by defing over it, without restarting the repl?