HPInc / HP-Digital-Microfluidics

HP Digital Microfluidics Software Platform and Libraries
MIT License
2 stars 0 forks source link

Add clock support to macro language #221

Open EvanKirshenbaum opened 5 months ago

EvanKirshenbaum commented 5 months ago

Mike pointed out that it would be useful to be able to control the clock from the macro language. (He suggested it in the context of implementing pausing for user input #219, but it's a good idea in general.)

The easiest way is probably to add a clock type (Type.CLOCK) as a Type.BINARY_COMPONENT. I'm not sure why the internal Clock class isn't a BinaryComponent. I guess it may be that BinaryComponent is specifically a BoardComponent (I forget why, the board attribute doesn't appear to be used) and Clock isn't. But I could probably split out BinaryBoardComponent or just make the current BinaryComponents inherit from both.

In any case, doing this and giving myself a **CLOCK** special variable would allow the user to talk about clock or the clock.

Making it a binary component would allow the user to say

clock : ON
clock : OFF

We could also give the clock attributes such as [update] interval, [update] rate (or speed), next tick, and current tick. Also properties such as paused and running

It would probably be worthwhile to add a pause [the] clock expression, which would be equivalent to turning the clock off and resuming once it was turned on (either by other code running in parallel or by the user hitting the RUN button). This could be done by turning it off and then delaying the future post to after the next tick.

To handle the update rate properly, I'll want to add a frequency type and Hz/hertz units, with the ability to divide numbers by time and frequency to interchange. To be able to handle forms like 5/sec, I'll want to put an optional (but checkable) DIV in the unit_expr rule:

  | amount=expr divp=DIV? dim_unit             # unit_expr

On the other hand, if I want to be able to support things like 2 ticks/second, I may want to simply have a second division operator that takes a unit as its rhs.

For completeness, I also jotted down on my board

[n [ticks]] every 100 ms
n [ticks] per second

but those are probably not that important. Simple division should be sufficient.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Jan 20, 2023 at 2:36 PM PST.
EvanKirshenbaum commented 5 months ago

This issue was referenced by the following commits before migration:

EvanKirshenbaum commented 5 months ago

DML now has a CLOCK type, which is a BINARY_COMPONENT, so you can say

the clock : on;
if the clock is on { ... }
s = the clock's current state;

You can also specifically say

   the board's clock;
   the clock is running;  // and paused
   pause the clock;  // and resume, start, restart
   the clock's update interval = 100ms;
   the clock's update rate = 20 Hz;
   the clock's update rate = 5/second;
Migrated from internal repository. Originally created by @EvanKirshenbaum on Aug 15, 2023 at 1:44 PM PDT.
EvanKirshenbaum commented 5 months ago

I spent a little bit of time trying to figure out how to say

   the clock's update interval = 20 ms/tick;
   the clock's rate = 5 ticks/second;

but it turned out to be surprisingly tricky, so I'm going to postpone that for later (assuming later comes).

Just to get it down, what I was trying was to get rid of the unit_recip_expr rule and generalize dim_unit to possibly take divisions:

dim_unit returns[tuple[Optional[PhysUnit], Optional[Unit]] units]
  : ('per' | DIV) denom=simple_dim_unit {$ctx.unit=(None, $denom.unit)}
  | non_recip_unit {$ctx.units=$non_recip_unit.units}
  ;

non_recip_unit returns[tuple[PhysUnit, Optional[Unit]] units]
  : num=maybe_env_unit ('per' | DIV) denom=simple_dim_unit {$ctx.unit=($num.unit, $denom.unit)}
  | num=maybe_env_unit {$ctx.unit=($num.unit, None)}
  ;

maybe_env_unit returns[PhysUnit unit]
  : ('drop' | 'drops') {$ctx.unit=EnvRelativeUnit.DROP}
  | simple_dim_unit{$ctx.unit=$simple_dim_unit.unit}
  ;
simple_dim_unit returns[Unit unit]
  : ('s' | 'sec' | 'secs' | 'second' | 'seconds') {$ctx.unit=SI.sec}
  | ('ms' | 'millisecond' | 'milliseconds') {$ctx.unit=SI.ms}
  | ('minute' | 'minutes' | 'min' | 'mins') {$ctx.unit=SI.minutes}
  | ('hour' | 'hours' | 'hr' | 'hrs') {$ctx.unit=SI.hours}
  | ('uL' | 'ul' | 'microliter' | 'microlitre' | 'microliters' | 'microlitres') {$ctx.unit=SI.uL}
  | ('mL' | 'ml' | 'milliliter' | 'millilitre' | 'milliliters' | 'millilitres') {$ctx.unit=SI.mL}
  | ('tick' | 'ticks') {$ctx.unit=ticks}
  | ('V' | 'volt' | 'volts') {$ctx.unit=SI.volts}
  | ('mV' | 'millivolt' | 'millivolts') {$ctx.unit=SI.millivolts}
  | ('Hz' | 'hz') {$ctx.unit=SI.hertz}
  ;

But getting that to work not only for 5/second and 5 ticks per second, but also 5 drops per second (since drops needs to come from the environment) and the clock's update interval in ms/tick and the clock's update rate's magnitude in ticks/second quickly got hairy. Not to mention that allowing this makes it seem like you should be able to say things like 5 ticks every 3 seconds.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Aug 15, 2023 at 1:48 PM PDT.
EvanKirshenbaum commented 5 months ago

Okay, it turns out that making Clock a BinaryComponent breaks the hybrid Bilby/Yaminon model. I've reorganized things a bit,

  1. adding a (non-exposed) Config.owns_externals that the model can set to False when building its internal Yaminon board.
  2. Only adding external components (including the pipettor and clock) when it's true.
  3. Making Joey wells and extraction points use their board's pipettor rather than needing one on creation.
  4. Explicitly patching the hidden board's pipettor to match the outer board (since the wells and extraction points are done there).

This seems to work, but unfortunately, the resulting board is just a Wombat board, not a Yaminon board. Looking into it, it appears that

        with (wombat.Config.is_yaminon >> True
              and device.Config.owns_externals >> False
              ):
            self._yaminon = wombat.Board()

is somehow making Config.is_yaminon read as False. I'll look into what's happening with conjoined overrides to see whether (and why) it's taking the second override instead of the first.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Aug 16, 2023 at 11:38 AM PDT.