API Reference
adwin
CONTEXTS__SPECIAL = {'ADwin_LowInit': -2, 'ADwin_Init': -1, 'ADwin_Finish': 2 ** 31 - 1}
module-attribute
Used for passing information to the ADwin controller
CYCLES__RUN = (0, 2 ** 31 - 2)
module-attribute
The first and last cycle at which a row outside the special contexts can be played.
The real-time program's cyclecount is a 32-bit long that starts at 0 and is
incremented once past the last row, so that row can be at most 2**31 - 2. Everything
outside the range belongs to the sentinels of CONTEXTS__SPECIAL, which share the
column with the time axis (D19). See validate.cycles for what each way out does.
DATA__FINISH__DIGITAL = 41
module-attribute
Where upload leaves the final state (B11): the number of analogue and digital rows in
Par_15 and Par_16, the analogue rows as module, channel and digits in data_31..33, the
digital ones as module, channel and value in data_41..43. The sequencer's finish: plays
them, and the console reads them back to show what the apparatus holds after a run.
MODULES__DIGITAL__MAX = 16
module-attribute
The digital modules, which the sequencer's lowinit: programs as outputs: their number in
Par_19, and the modules in data_44, at most digitalModulesMaxDim of them. upload writes
them from the machine specifications, so a second digital module is programmed like the first,
and its rows reach it through their own module number, as analogue rows do (D18, #133).
PAR__PROCESSDELAY__EXPECTED = 9
module-attribute
processdelayExpected in the sequencer: the Processdelay upload built the arrays for.
PAR__PROCESSDELAY__REPORTED = 14
module-attribute
processdelayReported in the sequencer: the Processdelay its event loop runs at, written at
the end of init:. upload clears it, so a 0 after a run means that the loaded program does
not report it. That is a program older than the check, which is refused rather than trusted.
Where the two differ, the sequencer has refused to play the run (#128).
PAR__SEQUENCES__FINISHED = 18
module-attribute
sequencesFinished in the sequencer: how many sequences have finished since the machine was
booted, incremented at the end of every finish:. The console uses it to tell a restart after a
run, when the apparatus holds the run's final state, from a restart by hand.
PAR__SEQUENCE__OWNER = 17
module-attribute
sequenceOwner in the sequencer: the number of the process playing the arrays, from the start
of its lowinit: to the end of its finish:, and 0 otherwise. The data arrays are shared by
every process on the machine, so a write for one process can land under a run of another; this
says which one to wait for. Read as a name, not as proof: whether the named process is still
running is asked of the process itself, so that a value left behind by one that did not finish
cannot hold anything up.
PROCESSDELAY__RATE = {'T12': 1000000000.0}
module-attribute
How fast Processdelay counts on each processor type, in units per second, keyed by what
ADwin.ADwin.Processor_Type() returns. A process's cycle period is its Processdelay
divided by this.
The one hardware constant the package keeps, and only for the processors it has run on:
both laboratories use the T12, at 1 ns per unit. Another type -- including "T12.1",
which the driver reports separately -- is refused by name rather than guessed at, and
belongs here only once its rate is confirmed. Kept as a rate rather than a tick so that
5000 / 1e9 gives exactly the float 5e-6.
ROWS__MAX = {'analogue': 10000000, 'digital': 10000, 'analogue__finish': 256, 'digital__finish': 256}
module-attribute
How many rows each array of the sequencer holds: analogMaxArrayDim, digitalMaxArrayDim
and finishMaxArrayDim in both .bas programs, which must agree. upload refuses more
rather than letting the transfer fail partway, with some arrays written and others not.
adc
Recording with the ADC variant of the sequencer, resources/ADwin/WignerTimeADwinADC.bas.
That program plays an upload exactly as WignerTimeADwin.bas does, and in addition records
one channel of an ADC card in burst mode, over a window of the run. The window is stated here
in seconds of the timeline, and written to the program in cycles of the run, at the period
core.upload read off the machine. The program therefore assumes no period of its own. It
used to convert seconds to cycles at a fixed 5 us, so at a period of 2 us, Lab2's, a window
asked for at 1 s would have opened at 0.4 s (#94, step 10).
A window is armed for one run: the program's finish: disarms it. A run nobody armed
records nothing, rather than a window left over from an earlier run, and read says so.
The contract with the program, whose #Defines must agree with the numbers below:
Par_42andPar_43, written byarm: the cycle at which the burst starts, and the cycle by which it is over. The program starts the burst only if0 <= Par_42 < Par_43 <= Par_1, and itsfinish:resetsPar_43to 0, which is what disarms.FPar_61, written byarm: the window's duration in seconds. The program turns it into a number of samples at its own sample period, and caps it at its buffer.Par_41andPar_44, reported by the program, and cleared byarm: how many samples it recorded, and their period in whole nanoseconds.Par_41is 0 unless the burst had its whole window. The period is a Par rather than an FPar because an FPar reaches Python in single precision, which over two million samples would shift the last one by some 30 ns.Data_1: the samples, as the card's digits.
FPar_62, the start in seconds, is no longer read.
PROCESS = 4
module-attribute
The process number in the header of WignerTimeADwinADC.bas.
Samples
Bases: NamedTuple
One recording: time in seconds of the timeline, one entry per sample, and digits as
the card delivered them, with the window that was armed and the run that played it.
Source code in src/wignertime/adwin/adc.py
72 73 74 75 76 77 78 79 80 81 | |
Window
Bases: NamedTuple
What arm wrote: a window of the run of upload, in cycles of that run.
cycle__start is the start rounded to the nearest cycle, by the same rule as every row
of the timeline, and time__start is that instant in seconds, which is when the first
sample is taken. cycle__end is the first cycle by which the whole duration has
passed.
Source code in src/wignertime/adwin/adc.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
time__start
property
The start of the window as armed, in seconds of the timeline.
arm(upload, t, duration)
Arms the next run of upload to record from t for duration seconds, and returns the
Window as written.
upload must be one made for the ADC variant, process 4. The window must lie within the
run: the samples are read out in finish:, and a burst still under way there would give
a partly stale buffer, so a window that ends after the run is refused here. If process 4
is running, this waits for it first, since its finish: would disarm the new window.
Source code in src/wignertime/adwin/adc.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
read(window, run)
Reads out what the run recorded in the window armed for it, and returns the Samples.
Refuses a run of another upload, and one not yet waited for. Raises if the program recorded nothing, which it does when the run was not armed or did not last until the window closed, and if it recorded less than the window, which it does when the window is longer than its buffer. A program that reports no sample period is older than this contract, and is refused, since it would have placed the window by its own assumed period.
Source code in src/wignertime/adwin/adc.py
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
run(upload, t, duration)
Records from t for duration seconds in one run of upload, and returns the
Samples: arm, then core.run, then read. Arms afresh each time, so it can be
called repeatedly on the same upload, to average over runs.
Source code in src/wignertime/adwin/adc.py
189 190 191 192 193 194 195 196 | |
connection
The choice made here is to model our ADwin connections as little more than tables of data (DataFrames). This allows for the least amount of coupling with other implementations an convenience of interaction with timelines etc.
What follows are simply convenience functions to make the creation easier.
new(*variable_module_channel)
Convenience for creating a table with 'variable', 'module' and 'channel' columns.
'variable's have the form 'context_equipment__unit' or 'context_equipment'. In the latter case, the 'variable' is taken to be digital (unitless).
vmcs: e.g. "AOM__MOT__V", 1, 1 or ["shutter__MOT", 1, 11], ["shutter__repump", 1, 12], ["shutter__imaging", 1, 13],
Source code in src/wignertime/adwin/connection.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
remove_unconnected_variables(timeline, connections)
Purges the given timeline of any variables that do not have a matching connection.
NOTE: Assumes timeline and connections are both pd.DataFrame-like things
Source code in src/wignertime/adwin/connection.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
console
The manual console: direct access to the apparatus's channels from a notebook, for alignment
and debugging, through resources/ADwin/WignerTimeConsole.bas loaded as process 10.
It works from the tables the timelines use. panel joins connections to devices, so a
channel is named, converted and bounded in one place only (D22). An analogue value goes
through its device's to_V, a function where it is one, and then through
conversion.to_digits with its module's range, width and gain from the machine
specifications, the conversion the sequencer's arrays are made with (D11). The console
has no notion of a timeline beyond the one it takes its starting values from.
Shadow state, not a mailbox. configure writes the panel into the program as a list of
entries, one per variable. set_value writes one word, the digits wanted for one entry, and
returns without waiting. The program's sweep writes to the hardware wherever the wanted digits
differ from the ones last written, so repeated values coalesce and nothing on either side waits
on the other. The mailbox this replaced made Python poll Par_73 until the program had served
the previous request, and both explanations kept for the fault in which the processor saturated
for seconds at a time involved that loop (D22).
The contract with the program, whose #defines must agree with the numbers below:
data_51[i]/data_52[i]: the digits wanted for entryi, written here, and the digits last written to the hardware, written by the program. -1 indata_52means write again; -2 in either means unknown.data_53[i]/data_54[i]: the module and channel of entryi, in the panel's row order.data_56[i]: 1 where entryiis a digital line, which takes 0 or 1, and 0 where it is analogue. The program programs the module of every digital entry as outputs (D18).data_55[i]: 1 where the program has written entryisince it started, which is what makes its value one set on the console rather than one a run left there.jumpsreads it.Par_75: the number of entries.configuresets it to 0 while it rewrites them.Par_78: a tokenconfiguredraws, so that a panel configured again elsewhere makes the old one refuse, rather than write its values into another panel's entries.Par_74,Par_77: hardware writes and sweeps since the program started, forhealth.Par_76: the value ofadwin.PAR__SEQUENCES__FINISHEDthe program last saw.
After a run. A sequence stops the console for its run, and starts it again afterwards if it
was running. The program then adopts the run's final state (maintainer's policy, 2026-09-27):
an entry the final state names takes its digits, and one it does not name becomes unknown,
because it holds whatever the run left it at. Nothing is written to either, and anything wanted
before or during the run is discarded. set_value refuses while a sequence owns the outputs. A
start by hand, with no run since, writes every entry again from what was wanted.
Before a run. adwin.core.upload warns about each analogue channel set on the console that
the run will jump from its value, whether in the initial state or at the first row that commands
it (jumps). The MOT coils are the case in point: the lab leaves them out of the initial state,
so that the MOT stays in its steady state between runs, and the jump from a value set by hand
comes with the first stage that sets them.
ENTRIES__MAX = 512
module-attribute
consoleMaxEntries in the program: 16 modules of 32 channels.
PROCESS = 10
module-attribute
The process number in the header of WignerTimeConsole.bas.
Console
Bases: NamedTuple
A console configured with a panel: the machine, the panel table whose rows are its
entries, and the token that configure drew for it.
Source code in src/wignertime/adwin/console.py
105 106 107 108 109 110 111 112 113 | |
Health
Bases: NamedTuple
A snapshot of the machine as the console sees it. workload is ADwin's processor workload
in percent, sweeps the program's heartbeat and writes its hardware writes since it
started. They are for the fault reported in the lab, the processor saturating for seconds
at a time while the console is in use: sampled over a session, they date an episode, and
show whether the console was writing while it lasted.
Source code in src/wignertime/adwin/console.py
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 | |
Jump
Bases: NamedTuple
An analogue channel that a run will move, at once, from a value set on the console: the
variable, the value it held, the value the run first commanded, both in its own unit,
and the cycle at which the run does so, negative for the initial state.
Source code in src/wignertime/adwin/console.py
414 415 416 417 418 419 420 421 422 423 424 | |
OutputsOwned
Bases: RuntimeError
A sequence owns the outputs, so a value is refused: after the run the apparatus holds the run's final state, and the console adopts it.
Source code in src/wignertime/adwin/console.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
configure(machine, table)
Writes the panel into the console program as its entries, each wanting its
default_value, and returns the Console. The program writes them to the hardware, except
the variables without a default, which are unknown until they are set.
The entries are rewritten with their count at 0, so that the program never sweeps a half-written list, and a fresh token is drawn. A run finished before this does not count as one the program has yet to adopt, since these values are newer.
Source code in src/wignertime/adwin/console.py
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | |
create_UI(machine, table, continuous_update=True)
Configures the console with the panel and returns its widgets: a vertical slider per
analogue variable, a toggle per digital line, a button that shows what the program holds,
and a line for messages. Configuring writes every default to the apparatus; a variable
without one is marked as unknown and left as it is.
A slider sends its value while it is dragged, since a write costs one driver call and
repeated values coalesce in the program. continuous_update=False sends it on release.
A value that is refused, because a sequence owns the outputs or the console is not running, puts the widget back and says why, so that the widgets show what was asked for. After a run the widgets are brought up to the adopted final state at the next move, or with the button. A variable the final state does not name is marked, since what it holds is unknown until it is set.
Source code in src/wignertime/adwin/console.py
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 | |
final_state(machine, table)
The final state of the last upload, as {variable: value} for the variables of the
panel it sets, read from the arrays upload writes for the sequencer's finish:.
Source code in src/wignertime/adwin/console.py
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 | |
from_digits(row, digits)
The value of the panel's row whose digits are digits. The digits give the voltage
exactly, as conversion.to_digits is linear. A linear to_V then gives the value; a
calibration function, which cannot in general be inverted, is inverted by interpolation
over the row's range, which assumes that it is monotonic there.
Source code in src/wignertime/adwin/console.py
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
health(machine)
The console's Health now.
Source code in src/wignertime/adwin/console.py
491 492 493 494 495 496 497 498 499 500 | |
jumps(machine, analogue, connections, devices, machine_specifications=None)
The Jumps of a run whose analogue rows are analogue, as adwin.core.convert gives them:
each analogue channel that holds a value written by the console since it last started, and
whose first value in the run differs from it by more than one DAC step.
Only values the console wrote count. After a run the console holds the run's final state, and a jump from that is part of the sequence, not a surprise. Nothing is reported when the console has not yet seen the last run that finished, because its record is then older than what the apparatus holds.
Source code in src/wignertime/adwin/console.py
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 | |
panel(connections, devices, timeline__defaults, machine_specifications=None)
The table the console works from: connections joined to devices, with a
default_value per variable, the last value timeline__defaults gives it (normally the
lab's experiment.init()), and what _conversions adds from machine_specifications
(adwin.internal.SPECIFICATIONS__DEFAULT when not given), as the pipeline reads them.
The two tables must describe the same apparatus (device.check_correspondence). Here
that matters more than anywhere: a variable with no device is taken for a digital line,
so an analogue channel whose device name was mistyped would be switched between digits
0 and 1, that is, to -10 V. Every analogue variable needs finite bounds, which are its
slider's range.
A variable the defaults do not mention keeps NaN as its default_value, is reported, and is
left as it is: configure writes nothing to it until it is set. The lab leaves the MOT coils
and the dispenser out of its initial state so that the MOT stays in its steady state between
runs, and building the console should not undo that.
Source code in src/wignertime/adwin/console.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
readback(console)
What the console program holds for each variable of the panel: a frame of variable,
value (NaN where unknown), and pending, true where the value wanted has not yet been
written to the hardware. That should last a sweep or two at most.
After a run, this is the final state the program adopted, and NaN for the variables the final state does not name.
Source code in src/wignertime/adwin/console.py
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | |
set_value(console, name, value)
Sets the variable name to value, in its own unit, and returns without waiting: the
program writes it to the hardware within a sweep or two.
Refuses while a sequence owns the outputs (OutputsOwned), when the console process is
not running, and when the console has been configured again since console was, from this
notebook or another, since its entries are then another panel's.
Source code in src/wignertime/adwin/console.py
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
to_digits(row, value)
The digits for value of the panel's analogue row, as the sequencer gets them.
Source code in src/wignertime/adwin/console.py
201 202 203 204 205 206 207 208 209 210 | |
core
POLL__PERIOD = 0.1
module-attribute
How often a process is asked whether it has stopped, in seconds: the lab's own rate.
LostEvents
Bases: RuntimeError
A run lost events: ADwin's term for event cycles that came due while the previous one was still executing.
Nothing is skipped when that happens -- cyclecount advances once per executed
event, so every row is still played, in order, at its cycle -- but the cycles
themselves stretch, and the run takes longer than its timeline says by run.slip.
The shot is not the experiment that was described, so it is refused rather than
reported. run carries the record.
Source code in src/wignertime/adwin/core.py
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 | |
PeriodRefused
Bases: RuntimeError
The sequencer refused to play a run, because the Processdelay its event loop runs at is not the one the arrays were built for (#128). It played the initial state and nothing after.
upload reads the period off the machine, but before the start. A program may still set its
own Processdelay once started, and a different process from the one the upload was made for
may have been started. Either way, playing the arrays would rescale every time in them.
Source code in src/wignertime/adwin/core.py
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 | |
Run
dataclass
The record of one run of an upload, filled in as it goes: start sets the first two
fields and wait the rest.
time__start is wall-clock time (time.time()), so the record can be filed with the
data the shot produced; duration is how long the run took, as seen from here, in
seconds; lost_events counts ADwin's lost events during it (see LostEvents);
processdelay__reported is the Processdelay the sequencer reported running at (see
PeriodRefused).
Source code in src/wignertime/adwin/core.py
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 | |
slip
property
How much longer the run took than described, in seconds.
Upload
Bases: NamedTuple
The log of one upload: where the timeline went, at what period, and what was written.
The first two fields are the machine and the process, in the order of the
(machine, process) pair that routines starting the controller take -- the camera
routines of sec:parameter_scan among them -- so an Upload can be passed wherever
such a pair is expected.
cycle__last is what Par_1 was set to, the cycle of the last row outside the
special contexts, and time__last is the same instant in seconds. analogue and
digital are the rows played, (cycle, module, channel, digits); analogue__finish and
digital__finish are the final state, (module, channel, digits), which the sequencer
applies however the run ends.
Source code in src/wignertime/adwin/core.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
convert(timeline, connections, devices, cycle_period, machine_specifications=None, time_resolution=None)
Convenience for converting a Wigner timeline (DataFrame) to an ADbasic-compatible list of tuples.
This takes an operation-layer timeline, adds the columns necessary for an ADwin conversion, based on the supplied or default specifications, and then converts the relevant columns according to adwin.to_tuples, i.e. [[(cycle, module, channel, value), ...],
[(cycle, module, channel, value), ...]].
cycle_period is the period of the controller's event loop, in seconds, and has no
default: it belongs to the program loaded on the machine, not to the package, and
the two laboratories this was written for run at 5 us and 2 us. A wrong value is a
uniform rescaling of every time in the experiment, which reads as physics rather
than as an error, so it is asked for rather than assumed.
time_resolution is the step at which the ramps that bind none are sampled, the
cycle period when not given -- the finest step the hardware can act on. A coarser
one thins them without moving anything in time, since cycles are computed from the
times themselves. A ramp that binds its own keeps it, e.g.
function=functools.partial(tanh, time_resolution=1e-4) (#65): this is a default,
never an override (timeline.expand).
machine_specifications describes the installed modules, and is
internal.SPECIFICATIONS__DEFAULT when not given, read at call time.
Source code in src/wignertime/adwin/core.py
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
link_device(DeviceNo=1, raiseExceptions=1, useNumpyArrays=0)
A Wrapper around ADwin.ADwin. Returns a new (stateful) ADwin machine object that is digitally connected to a physical ADwin machine.
Source code in src/wignertime/adwin/core.py
158 159 160 161 162 163 164 165 166 | |
read_cycle_period(machine, process)
The cycle period of process on machine, in seconds: its Processdelay, read off the
machine, divided by the rate at which the processor counts it.
Read, never set. An ADbasic program can overwrite its own Processdelay, so a value
written from here is not one the machine is bound to keep. For the same reason this
shows the value before any the program sets for itself once started.
Source code in src/wignertime/adwin/core.py
146 147 148 149 150 151 152 153 154 155 | |
run(upload)
Runs an upload with nothing to collect alongside, and returns its Run: the plain
case, as for a sequence whose results are read off the machine afterwards.
Source code in src/wignertime/adwin/core.py
588 589 590 591 592 593 594 595 | |
running(upload)
Brackets one run of an upload: starts it on entry, and on leaving waits for it to end
and checks it (wait). The block is where whatever the run triggers is collected:
camera.arm() # before the start
with adwin.running(log) as run:
frames = camera.capture(4) # triggered by the timeline
# here the run is over, and lost no events
If the block raises, the run is still waited out before the error goes on, but not
checked, and not stopped. A stop plays the final state since B11 was fixed, but that a
stop from the PC reaches finish: is not yet confirmed on the rig.
Source code in src/wignertime/adwin/core.py
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 | |
start(upload)
Starts the process an upload was made for, and returns its Run.
If the process is still running -- a replay of the same upload, which does not go
through upload and its wait -- this waits for it first, saying so once. Everything
that must be ready before the sequence begins, such as a camera waiting for its
trigger, has to be armed before this is called.
Source code in src/wignertime/adwin/core.py
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 | |
upload(timeline, connections, devices, machine, process, machine_specifications=None, time_resolution=None)
Converts a timeline for process on machine and writes it into the machine's memory,
ready for the process to be started. Returns the Upload log of what was written.
The cycle period is read off the machine (read_cycle_period), never given: a wrong
one rescales every time in the experiment, which reads as physics rather than as an
error. That is why the machine and the process are both required. The process is the
one whose Processdelay sets the period, so it must be the one started afterwards;
the arrays themselves are shared by every process on the machine.
machine_specifications and time_resolution are passed on to convert.
If the process is still running, upload converts first and then waits for it to stop
before writing, saying so once. The machine accepts writes mid-run and the running
sequence reads them, so writing at once would rewrite the arrays under a run that is
still playing (A15/#151). A parameter scan that uploads shot N+1 while shot N plays its
tail is the case in point. The process that owns the arrays (Par_17) is waited for
too, when it is another one, such as the ADC variant.
Then it warns about each analogue channel set on the console that the run will jump from
its value (console.jumps), once the previous run is over and the console has seen it. A
jump from a value set by hand is often intended, so it is reported, not refused.
NOTE: Stateful. It writes Par_1..3 and the data arrays, and starts nothing.
Source code in src/wignertime/adwin/core.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 | |
wait(run)
Waits for a Run to end, fills in its record, and returns it.
Raises PeriodRefused if the sequencer refused to play the run, and refuses a program that
did not report its Processdelay at all: one older than the check, whose run nothing vouches
for. Then raises LostEvents if the run lost any.
The count is ADwin's counter as the run ends, which the ADbasic manual (6.00, p. 79) describes as the cycles lost "since process start": the run's own. UNVERIFIED on the rig. If the counter instead accumulated from the moment the program was loaded, every run after one that lost events would be refused as well, which is loud. The difference across the run, read before this, was right only under that reading, and would have passed, without a word, a run that lost as many events as the one before it.
Source code in src/wignertime/adwin/core.py
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 | |
display
MARGIN = 0.05
module-attribute
The width of each margin, as a fraction of the run's duration.
quantities(timeline, variables=None, do_context=True, do_show=True, symbol_quantities=SYMBOL_QUANTITY, cmap__context='magma', range__x=None)
Displays the given timeline, filtered by variable, in terms of different quantites, i.e. by common unit. The mapping between unit and 'quantity' can be provided as a dictionary.
NOTE: Unit and quantity terminology taken from SI conventions.
Source code in src/wignertime/adwin/display.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | |
internal
For 'lower-level' manipulations of ADwin-specifc timeline informaton.
In general, the user shouldn't need to use these functions and there is no guarantee that the API will not change.
add(timeline, connections, devices, cycle_period, machine_specifications=None)
Takes an 'operational' layer timeline and inserts ADwin-specific columns, e.g. cycles and numbers for the module and channel etc.
Which modules are digital, and how each analogue module converts to digits, are read
from machine_specifications (D11, D18).
cycle_period is the period of the controller's event loop, in seconds; see
core.convert.
Source code in src/wignertime/adwin/internal.py
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | |
add_cycle(timeline, cycle_period, special_contexts=None)
Inserts a new cycle column into the timeline as a conversion of the time column into 'number of cycles'.
Parameters:
- timeline: DataFrame containing the experimental data.
- cycle_period: The period of the controller's event loop, in seconds.
- special_contexts: Dictionary with context-specific overrides for cycle values;
wt_adwin.CONTEXTS__SPECIAL when not given.
The column is 64-bit here, although the machine's counter is 32-bit: a row too late
for the counter has to survive long enough for validate.cycles to refuse it, and
casting first would wrap it into an ordinary-looking number instead. validate.types
narrows it once that check has run.
Raises:
- ValueError if the time column is missing, or the cycle period is not a positive
number of seconds.
Source code in src/wignertime/adwin/internal.py
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | |
check_module_kinds(connections, modules__digital)
Checks that each connected variable is of the kind of the module it is connected to.
A variable's kind is stated twice: by its name, since a variable without a __unit
suffix is a digital line, and by its module, whose width says what the port is. Nothing
checked that the two agree (A16). An analogue variable on the digital module was rounded
to an integer and switched as a digital line: 1.5 A on a coil became a 2 written to a
digital output, without a word. A digital line on an analogue module did fail, but later,
in a cast, with a message that named neither the variable nor the cause.
Source code in src/wignertime/adwin/internal.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
check_modules_described(connections, machine_specifications)
Checks that every module a connection names is one the specifications describe.
Everything about a module – whether it is digital, and how its values become digits – is read from its entry in the specifications, so a connection to a module without one has no right answer. It used to be taken for analogue and converted as ±10 V, 16 bits.
Source code in src/wignertime/adwin/internal.py
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
conversion__module(machine_specifications, module)
How the values of an analogue module become digits, as the keyword arguments of
conversion.to_digits: the module's voltage_range, its width in bits, and its gain,
from its entry in the specifications.
Each module is converted with its own entry (D11, #125). Every analogue module the
package had run on is ±10 V and 16 bits, which is why converting all of them with
conversion.SPECIFICATIONS__DEFAULT went unnoticed: a module of any other range was
converted wrongly, without a word. An entry missing one of the three raises, rather
than being completed with those values.
Source code in src/wignertime/adwin/internal.py
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
modules__digital(machine_specifications)
The module numbers carrying digital connections, derived from the specifications.
A digital line is one bit wide, so a module of one-bit channels is a digital module.
This is a derivation rather than a declaration, deliberately: a kind field beside
bits would be a second source of truth, and a module declared
{"kind": "digital", "bits": 16} would have no right answer. The same reasoning
keeps module and channel numbers out of the device conversions.
Until 2026-09-22 the test read m.get("bits", False) == True, which picks out the
digital module only because 1 == True in Python (D12/#126). Note what that did and
did not cost: x == True and x == 1 agree for every number, so the answer was
never wrong -- what was wrong was that "is one bit wide" was written as a comparison
against a boolean, leaving the intent unrecoverable from the code.
A module that declares no bits at all now raises rather than being taken for
analogue, which is the one behaviour that changed. Silently reading an
under-specified module as analogue is a guess about hardware, and it would put a
16-bit conversion on a digital line.
NOTE: Modules are numbered from 1 (unlike Python lists).
Any number of modules may be digital, and the real-time program honours each: upload
sends it the list, which its lowinit: programs as outputs, and the module of every
digital row, as it does for the analogue ones (D18, #133).
Source code in src/wignertime/adwin/internal.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
specifications(machine_specifications=None)
The machine specifications to convert against: those given, or else
SPECIFICATIONS__DEFAULT as it stands when this is called.
Read here rather than bound as a default argument, so that rebinding the global reaches every function that uses it (D23).
An entry for cycle_period is refused rather than ignored. The period used to live
here, and a specification written then would otherwise be accepted with its period
silently unused -- the failure D15 describes, in a new place.
Source code in src/wignertime/adwin/internal.py
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
to_tuples(timeline, machine_specifications=None)
Takes a full, ADwin-compatible, dataframe of the experimental run and converts the result to an output format that can be processed by ADwin (tuples), separating analogue and digital values.
return [[(cycle, module, channel, value), ...], [(cycle, module, channel, value), ...]]
Source code in src/wignertime/adwin/internal.py
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | |
to_tuples__raw(timeline, cols=['cycle', 'module', 'channel', 'value__digits'])
A raw extraction of ADwin-relevant values from a timeline, regardless of whether or not the module is digital or not.
NOTE: No validation is done here.
Source code in src/wignertime/adwin/internal.py
333 334 335 336 337 338 339 | |
validate
all(timeline, do_drop_repeats=True)
Includes ADwin-specific methods ontop of the basic timeline sanitization for removing unnecessary points and raising errors on illogical input.
do_drop_repeats can be turned off for debugging, or to inspect the unfiltered cost of an expansion.
Source code in src/wignertime/adwin/validate.py
184 185 186 187 188 189 190 191 192 193 194 195 196 | |
ascending(output)
Refuses converted arrays whose cycles are not in ascending order (D20).
output is what internal.to_tuples returns, [analogue, digital], each a list of
(cycle, module, channel, digits). The real-time program walks each array with an
index that only moves forward, so a row out of order is not skipped but played
late: at the cycle of the row before it. Nothing on the machine can tell.
to_tuples sorts, so this holds by construction today. It is checked on the arrays
themselves because they are the contract with the machine, and nothing between here
and the upload would notice if it stopped holding.
Source code in src/wignertime/adwin/validate.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
cycles(timeline, special_contexts=None)
Refuses a row, outside the special contexts, at a cycle the real-time program cannot play.
Such a row belongs in wt_adwin.CYCLES__RUN. Everything outside that range is the
territory of the special contexts' sentinels, and each way of leaving it fails
silently on the machine rather than here (D19):
- at cycle -1 or -2, the row is played with the init or lowinit rows;
- below -2, it sorts ahead of the lowinit rows, and since the backend plays only the row its index points at, and only when the count reaches it, the index never moves: no row of that array is played at all -- not the run, not the initial state, not the final one;
- past the last cycle, the machine's 32-bit counter wraps negative, into the sentinels, so the run replays its initialisation and does not end.
A row earlier than half a cycle before zero is therefore an error, however it came
about, rather than something to shift or drop. Must run before types, which narrows
the column to 32 bits and would wrap the late rows into plausible ones.
Source code in src/wignertime/adwin/validate.py
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
drop_duplicates(timeline, subset=['variable', 'cycle'], unless_context=list(wt_adwin.CONTEXTS__SPECIAL.keys()))
An alternative to that in timeline, to deal with ADwin-specific cases.
Drop rows where the columns specified in subset are both duplicated, except for in the specific contexts listed.
This removes temporal collisions: two entries for the same variable that round to the same cycle. The later entry wins, as wt_frame.duplicated keeps the last occurrence.
Source code in src/wignertime/adwin/validate.py
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
drop_repeats(timeline, subset=['module', 'channel'], column__value='value__digits', column__order='cycle', unless_context=list(wt_adwin.CONTEXTS__SPECIAL.keys()))
Drops rows that would command a channel to the value it already holds.
Where drop_duplicates removes temporal collisions, this removes value redundancy: rows whose digitized value is unchanged from the previous row on the same physical channel. The ADwin program only acts when a value differs from the preceding one, so these rows occupy memory and transfer bandwidth without ever producing an output change.
The effect is largest on expanded ramps. A ramp sampled at the cycle period produces one row per cycle, but a device can only express as many distinct outputs as it has digits across the ramp's span; the surplus rows repeat. Since the sampling grid is the hardware's own time grid and the value grid is the converter's least significant bit, the rows that survive are exactly the transitions the hardware is capable of making. Filtering here is therefore not an approximation to bit-flip-timed expansion – on this grid it is equivalent to it, and it needs no inverse of the ramp function, which would be unavailable for interpolated calibrations in any case.
Grouping is by physical channel rather than by variable, since the channel is the state the real-time program compares against.
Rows in the special contexts are excluded from the comparison entirely and always kept: they carry the safe-state guarantee and have no position in time.
The first and last row of every channel are always kept. Retaining the last matters beyond making the final commanded state explicit: adwin.core.upload takes the run length from the highest non-special cycle, and the tail of a hyperbolic-tangent ramp is flat, so dropping trailing repeats would silently shorten the experiment.
Source code in src/wignertime/adwin/validate.py
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
special_contexts(timeline, special_contexts=wt_adwin.CONTEXTS__SPECIAL)
Ensures that a variable has at most one entry before the run, across
ADwin_LowInit and ADwin_Init together, and at most one after it, in
ADwin_Finish. There is no concept of 'time' inside the special contexts.
The contexts before the run are counted together (#154, (b1), settled by the
maintainer 2026-09-29): both are at −∞, so nothing in the timeline orders them, and a
value lookup would otherwise see two candidates there. They used to be counted per
context. What is lost is changing a variable between lowinit: and init:, which
nothing did. A context's side is the sign of its sentinel.
Similarly, the time values are adjusted to avoid automatic removal later on.
Source code in src/wignertime/adwin/validate.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
config
CONTEXT__INFER = INFER
module-attribute
INFER, under the name the context signatures use.
INFER = _Infer()
module-attribute
The default of origin in update, anchor and ramp, and of context in create,
update, anchor and ramp (#142). It is there so that a signature shows that the
default does something: the origin is inferred from the timeline being extended (see
ORIGIN__DEFAULTS and ORIGIN__DEFAULTS__RAMP), and an unstated context is inherited
from it, and help(tl.update) reads origin=INFER.
None means exactly the same, both as the whole argument and in either slot of an
origin pair. That is deliberate. A stage that takes origin=None or context=None and
passes it on -- the usual way of writing "no opinion of my own" -- then gets the
library's default without having to know that this object exists, and a pair such as
[None, 0.0] reads the same as [INFER, 0.0]. Absolute placement is a number,
origin=0.0, and reads the same in update, anchor and ramp: a ramp always starts
where its variable is (#142), so its value slot is never an origin question. There is
no way to switch context inheritance off: every row has a context, stated or inherited
(#156).
It is an object rather than a string so that it cannot be mistaken for a name:
context="INFER" is an ordinary context.
ORIGIN__DEFAULTS = [[ANCHOR, None], [LAST, None]]
module-attribute
For update and anchor, whose values are absolute -- hence None in every value slot.
ORIGIN__DEFAULTS__RAMP = [[ANCHOR, VARIABLE], [LAST, VARIABLE]]
module-attribute
For ramp, which is the one core function that needs a value origin: a ramp runs from
wherever the variable currently sits to the target, so its start value has to be looked
up. Hence VARIABLE in the value slot -- the variable's own previous value, bounded by
the time origin. It is the only thing that slot can hold: a ramp always starts where its
variable is (#142), and ramp refuses anything else written there.
The LAST step is not decoration. Without it (this was a single-entry list until
2026-09-18) a ramp onto a timeline holding no anchor fell off the end of the chain and
landed in absolute time, so it could be placed before the rows it was appended to, with
no warning. See KNOWN_ISSUES A4.
ORIGIN__INFER = INFER
module-attribute
INFER, under the name the origin signatures use.
VARIABLE__REGEX = re.compile('^([^_]+)__([^_]+(?:_[^_]+)*)(?:__([^_]+))?$')
module-attribute
The naming convention for a variable, as <device>__<UID>(__<unit>), e.g.
coil__MOT_lower__A and shutter__MOT; the three groups are what variable.parse
returns. A missing __<unit> is what marks a line as digital. <device> and <unit>
contain no _; the <UID> may contain single ones (D7, #121, settled by the
maintainer 2026-09-29).
Keeping _ out of <device> is what makes the grammar safe to migrate to: a name in the
old <device>_<UID>(__<unit>) form has a _ before its first __, so it is refused
rather than read with its unit taken for a UID, i.e. as digital. It also refuses a
device of several words, power_supply__X__V, which the old grammar split silently;
such a device is supply__power_X__V.
This lives here, rather than in variable, because the convention is a default
rather than a law: a site that consistently applies a different one can rebind this
name before building its connections. variable.parse and
adwin.connection.is_valid_name read it at call time, so an override takes effect
without reimporting.
Origin
Bases: Enum
The reserved origin words, as tags rather than strings (#158, 2026-09-29).
A string in an origin is always a name -- of a variable or of a context -- and a
tag is always a rule. While the rules were the strings "anchor", "last" and
"variable", a string was sometimes one and sometimes the other, so a context or a
variable named after one of the words could not be referred to, and had to be refused
where it was written (A9). Tags are compared with is, print as their bare names, as
INFER does, and never reach the data, since an origin is resolved while the
timeline is built.
ANCHOR: the most recent anchor;LAST: the latest entry of the timeline;VARIABLE: each variable relative to its own most recent entry, in time or in value -- the one tag admissible in either slot.
Source code in src/wignertime/config.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
conversion
add(timeline, specifications=SPECIFICATIONS__DEFAULT, column__conversion='to_V', column__new='value__digits')
Performs a conversion, according to the associated factor or function, adds the resulting values as another column, value__digits, and returns the result.
Source code in src/wignertime/conversion.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
function_from_file(path, method='cubic', fill_value='extrapolate', indices__column=[0, 1], **read_csv__args)
An interpolation function drawn from two columns of a CSV-like calibration file.
NOTE: If you would like to invert the interpolation then just specify the columns backwards, e.g. indices__column=[1,0]
e.g.
function_from_file(
"resources/calibration/aom_calibration.dat",
names=["voltage", "transparency"],
sep=r"\s+",
),
Source code in src/wignertime/conversion.py
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
to_digits(voltage, voltage_range=[-10.0, 10.0], num_bits=16, gain=1)
Transforms any voltage linearly to analogue-digital-converter(ADC) digits.
Source code in src/wignertime/conversion.py
14 15 16 17 18 19 20 21 22 23 | |
demo
full_experiment
An example implementation of a real experiment, using 'Wigner Time' timelines.
As well as providing conveniences, the functions can be used to document the intention and meaning of each stage.
connections = adcon.new(['shutter__MOT', 1, 11], ['shutter__repump', 1, 12], ['shutter__OP1', 1, 14], ['shutter__OP2', 1, 15], ['shutter__science', 1, 10], ['shutter__transverse_pump', 1, 9], ['AOM__MOT', 1, 1], ['AOM__repump', 1, 2], ['AOM__OP_aux', 1, 30], ['AOM__OP', 1, 31], ['coil__compensation_X__A', 4, 7], ['coil__compensation_Y__A', 3, 2], ['coil__MOT_lower__A', 4, 1], ['coil__MOT_upper__A', 4, 3], ['coil__MOT_lower_plus__A', 4, 2], ['coil__MOT_upper_plus__A', 4, 4], ['lockbox__MOT__MHz', 3, 8], ['trigger__TC__V', 3, 1], ['AOM__science', 1, 4], ['AOM__science__trans', 4, 8], ['trigger__camera', 1, 0])
module-attribute
devices stores how to map our physical quantities to an implementation voltage, as well as specifying the range of values that should be allowed for this variable. A linear conversion is the factor taking the unit to volts, so a device whose permitted range spans the controller's full output is 10 / limit — 10 / 5.0 for a coil driven over +/-5 A by a +/-10 V DAC.
These specifications are deliberately separated from connections because they represent physical properties and conversions that are independent of the particular DAC wiring.
devices = device.new(['coil__compensation_X__A', 10 / 3.0, -3, 3], ['coil__compensation_Y__A', 10 / 3.0, -3, 3], ['coil__MOT_lower__A', 10 / 5.0, -5, 5], ['coil__MOT_upper__A', 10 / 5.0, -5, 5], ['coil__MOT_lower_plus__A', 10 / 5.0, -5, 5], ['coil__MOT_upper_plus__A', 10 / 5.0, -5, 5], ['lockbox__MOT__MHz', 0.05, -200, 200], ['trigger__TC__V', 1.0, -10, 10], ['AOM__science__trans', conv.function_from_file('resources/calibration/aom_calibration.dat', sep='\\s+'), 0.0, 1.0])
module-attribute
'constants' allow us to store site-specific details that help define our exeriment.
MOT(duration=15, lower_current=-1.0, upper_current=-0.98)
Creates a Magneto-Optical Trap.
Source code in src/wignertime/demo/full_experiment.py
182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
MOT_detuned_growth(duration=0.1, duration_ramp=0.01, detuning__MHz=-5)
Final stage of MOT collection with detuned MOT beams for increased capture range.
Source code in src/wignertime/demo/full_experiment.py
202 203 204 205 206 207 208 209 210 211 212 213 214 | |
default_state(MOT_ON=True, **kwargs)
Starts/leaves the system in a sane state that is appropriate for creating a new timeline
As a general rule, AOMs are kept on as long as possible to keep them in thermal equilibrium. When needed, we turn them off before the opening of the shutter. The same holds for the coils.
The same stage at both ends of the experiment: init and finish differ only in the
time, the context and MOT_ON.
Source code in src/wignertime/demo/full_experiment.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
finish(wait=1, lower_current=-1.0, upper_current=-0.98, MOT_ON=True, **kwargs)
Safely winds down the system, 'ramping' the analog variables to the “default state” in a given duration by the default ramp_function.
The anchor function is used to specify a key time instant, around which other times can be specified.
The ADwin_Finish environment means that the “default state” will be actuated even when the process is interrupted.
Source code in src/wignertime/demo/full_experiment.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
magnetic_trapping(duration_initial=5e-05, lower_current_initial=-1.8, upper_current_initial=-1.7, duration_strengthen=0.003, lower_current_strengthen=-4.8, upper_current_strengthen=-4.7)
Does what it says on the tin.
Source code in src/wignertime/demo/full_experiment.py
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | |
molasses(duration=0.005, duration_coil_ramp=0.0009, duration_lockbox_ramp=0.001, to__MHz=-90, delay=0)
For slowing down the atoms by creating an optical density.
Source code in src/wignertime/demo/full_experiment.py
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | |
optical_pumping(duration_exposition=8e-05, duration_coil_ramp=5e-05, i=-0.12, delay1=0, delay2=0, delay_repump=0, delay_shutter_reinitialization=0.1)
Creates an experimental timeline for optical pumping.
NOTE: The AOM is switched off close to, but before, the opening of the first shutter
WARNING: Shutters are reinitialized so that additional optical pumping stages can be added later.
Source code in src/wignertime/demo/full_experiment.py
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
pull_coils(duration, lower_current, upper_current, lower_plus_current=0, upper_plus_current=0, pt=3, time=None)
Controls the concentric coil pairs responsible for 'pulling' the atoms.
It names no context: it inherits the one of the stage it is part of, as
magnetic_trapping's two calls do.
Source code in src/wignertime/demo/full_experiment.py
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
trigger_camera(time, exposure, context, origin)
Opens the camera for exposure, starting time after origin.
origin has no default, because this stage is placed rather than chained: with one,
a call that forgot it would fall back on the latest anchor and land wherever the
timeline happened to end, without an error (maintainer, 2026-10-06).
context is required rather than inherited: a trigger placed into a finished timeline would otherwise adopt the context of its last row at an instant, finalRamps, and a camera trigger is not part of the final ramps.
The camera is not part of the default state, so the timeline it is placed into should set it initially and finally, e.g. init(trigger__camera=0) and finish(trigger__camera=0).
Source code in src/wignertime/demo/full_experiment.py
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | |
device
A device is represented by a dataframe that contains a variable (with a given unit) and a means, scalar or function, to convert this quantity to a voltage. It also specifies a minimum and maximum value for the variable, to be used in validation.
The unit range is used for conversion and the saftey range is for sanity checking the output.
add(timeline, devices)
For connecting device information to a timeline.
Source code in src/wignertime/device.py
164 165 166 167 168 | |
check_correspondence(connections, devices)
Checks that the device and connection tables describe the same apparatus.
The two are deliberately separate — recalibrating a device and rewiring the apparatus
are independent operations — but they are not independent vocabularies, and nothing
used to notice when they drifted apart. A single transposed letter in a device name
left the variable with no bounds at all, and check_within_range reads absent bounds
as "a digital line" and skips it: 500 A passed on a coil declared +/-5 A (A14).
Both directions raise, because each catches a different half of that:
- an analogue variable with a connection but no device will be driven with neither a calibration nor limits;
- a device with no connection is the other end of the same typo. It is harmless in itself, since nothing reads it, but tolerating it is what let the first case happen quietly.
A digital line has no unit and needs no device, so it is not expected to have one.
Source code in src/wignertime/device.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
check_within_range(timeline, columns__bounds=['value__min', 'value__max'])
Considers whether the timeline values fall inside device safety ranges (see SCHEMA). Raises an error if not, naming every variable that offends rather than only the first.
A variable with no bounds at all is one that has no entry in devices – a digital line – and is skipped. A variable with only one bound is checked against that bound alone.
"Typically" until 2026-09-21: an analogue variable could reach here unbounded through a mistyped device name, and this function could not tell that from a digital line, so it passed it. check_correspondence, called from adwin.internal.add, now rules that out on the path to hardware (A14).
ASSUMES: That a value column is present, and that the timeline has already been joined to devices.
Source code in src/wignertime/device.py
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | |
new(*variable_toV_min_max)
Convenience for creating a table that specifies the conversion and range of device values ('variable', 'to_V', 'value__min' and 'value__max' columns). 'to_V' specifies the conversion and can either be a floating point factor or a function that takes the output from the givin units to a Voltage. The min-max limits outline the range that a user is allowed to specify (and will be validated before output). This allows for error-checking before passing values to real devices.
variables are used as the means by which connections and devices can be later connected.
If the 'value__min/max' columns are not specified, then they will be taken as +/- infinity.
vfmm:
e.g.
"coil__compensation_X__A", 3/10., -3.0, 3.0,
or
["coil__compensation_Y__A", 0.333, -3.0],
["coil__MOT_lower__A",
Source code in src/wignertime/device.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
file
For PC-level loading and saving of stored timelines.
load(path)
Reads the given file into memory.
Source code in src/wignertime/file.py
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
save(df, path=None)
Writes the given timeline to file, according to convenient features.
--------
- If `path` has a recognized suffix, save in that format if the required
dependency is available.
- If `path` has no suffix:
- save as parquet if possible
- otherwise save as pickle
- If `path` is a directory (or looks like a directory), generate a filename
from the caller's variable name for `df`, falling back to "timeline".
- If the target already exists, append __002, __003, ... before the suffix.
Returns
Returns
Path
The actual path written.
Supported suffixes
Supported suffixes
.parquet, .feather, .pickle, .csv, .json
Source code in src/wignertime/file.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |
national_instruments
This is currently not implemented, but if it is of interest to you then please don't hesitate to get in touch: thomas.clark@wigner.hun-ren.hu .
ramp_function
ATTRIBUTE__POINTS = '__wigner_time_points__'
module-attribute
How many time-value pairs a ramp function interpolates between.
This belongs to the function, not to the caller expanding it: tanh is defined by two
points, and an interpolation wanting interior control points would be defined by more.
expand used to take the number as an argument instead (num__bounds), which meant it
could be given a value the data did not match, and could not be told apart from the
caller's other keywords. See KNOWN_ISSUES B6.
The name it replaces was wrong as well as misplaced: for two points "bounds" is exact, since start and end are the boundaries -- but that is the one case where the number need not be stated at all. A third point is an interior control point, not a bound.
POINTS__DEFAULT = 2
module-attribute
Assumed of a ramp function that does not say, which is every hand-written one.
linear(origin, terminus, time_resolution=None)
A series of [time, value] pairs according to the line defined by two points and the time resolution.
time_resolution left None is filled by whoever expands the ramp; see
_require_resolution.
Source code in src/wignertime/ramp_function.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
points(f)
How many time-value pairs f interpolates between. See ATTRIBUTE__POINTS.
Source code in src/wignertime/ramp_function.py
44 45 46 47 48 | |
tanh(origin, terminus, time_resolution=None, sharpness=3)
Hyperbolic tan, with a call signature adapted for practical timeline population.
origin/terminus are time-value pairs
time_resolution left None is filled by whoever expands the ramp: bind one with
functools.partial(tanh, time_resolution=...) for a ramp of its own resolution (#65).
See _require_resolution.
sharpness is a measure of how linear the 'slope' of the function is around the halfway point and in practice is used for easing transitions between the end-points. For example, sharpness ~0 (!=0) gives a linear ramp between origin and terminus, whereas large values approximate a step-function at the half-way point. In-between these values, the ramps returned will start and end gradually, with a linear movement in the middle.
Source code in src/wignertime/ramp_function.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | |
with_points(number)
Declare how many time-value pairs a ramp function interpolates between.
Decorating is optional: an undeclared function is taken to want POINTS__DEFAULT,
which keeps a user's lambda origin, terminus, time_resolution: ... working without
ceremony.
Source code in src/wignertime/ramp_function.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
timeline
Multiple layers of abstraction: - operational (time sequence: probe-on, probe-off etc.) – this should go to notebooks / experiment specific packages - variable (time sequence of independent degrees of freedom: AOM_probe_power 5V) - ADwin (ADwin-specific details)
It is a goal to be able to go up and down through the layers of abstraction.
ANCHOR = wt_config.ANCHOR
module-attribute
An origin at the most recent anchor. See config.Origin.
LAST = wt_config.LAST
module-attribute
An origin at the latest entry of the timeline. See config.Origin.
VARIABLE = wt_config.VARIABLE
module-attribute
An origin at each variable's own most recent entry, in time or value. See config.Origin.
as_deferred = wt_util.mark_deferred
module-attribute
Mark a user-written function as a deferred timeline function, so that stack will
accept it as a constituent. See stack.
noop = wt_util.mark_deferred(lambda timeline, **kwargs: timeline)
module-attribute
A stack constituent that contributes nothing, for the branch of a conditional that
should add no rows.
Not funcy.identity: it has to carry the deferred tag, and tagging a shared library
function would mark it for every other user of funcy. Taking **kwargs also means it
survives a stack that forwards keywords -- identity did not, and raised
TypeError: identity() got an unexpected keyword argument 'context'.
__getattr__(name)
Say what replaced a public name that is gone, rather than only that it is missing.
create initialised a timeline, and was the one core function a stack could not
begin with anything but. Since #85 a stack composes stages only, and the first rows
of a timeline are an update applied to an empty timeline by to_timeline.
Source code in src/wignertime/timeline.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
anchor(time, *, context=wt_config.CONTEXT__INFER, origin=wt_config.ORIGIN__INFER)
Creates a special, non-physical variable (will never have a matching connection), that can be used for time references, particularly within individual contexts.
This can be very convenient in the context of ramps, where the starting and ending times are often built around a hypothetical point in time, due to physical switching speeds.
time is required. There is no sensible default: it is a displacement from whatever
the origin resolves to, and the two readings a default would have to choose
between are genuinely different instants (see below).
Where the anchor lands
By default the origin is the most recent anchor where one exists and the last
entry otherwise, so time is normally a duration measured from the end of the
preceding stage. That is what makes stages chain: a stage may write rows past its
own closing anchor -- optical_pumping reinitialises shutters 0.1 s later -- without
dragging the next stage along with them.
To mark the end of everything written so far instead, ask for it explicitly:
anchor(0.0, origin=LAST) # here, at the last entry in the timeline
anchor(0.0) # here, at the most recent anchor
The two coincide until some stage writes past its own anchor, and then they do not:
in the shipped demo they differ by ~0.1 s from optical_pumping onwards. Which one
is meant is therefore worth stating at the call site rather than defaulting.
NB. - Anchors are automatically numbered, for 'global' referencing, but these numbers are not necessary in normal use.
Source code in src/wignertime/timeline.py
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 | |
cascade(*fs, **kws)
Similarly to stack, a convenience that combines an arbitrary chain of functions with an arbitrary selection of associated keywords.
kws are routed to the associated functions by prefixing, e.g. cascade(MOT, molasses, MOT_duration=1.0) creates a stack of MOT and molasses, with duration=1.0 passed into the MOT function before evaluation.
The motivation for this feature is that different experimental contexts should be built modularly, but, at final composition, the user often just wants a single point of contact to add/change nested variables.
How this differs from stack, which is easy to get wrong
stack takes stages that have already been called; cascade takes the stage
functions themselves and calls them::
stack(MOT(duration=15), molasses()) # called here
cascade(MOT, molasses, MOT_duration=15) # called by cascade
In the first, MOT(duration=15) has had every argument but timeline supplied, and
what it returns is a function of the timeline alone -- partial application, not
currying, since the remaining argument is supplied in one call rather than one at a
time. stack then threads the timeline through that chain.
In the second, nothing has been called: cascade routes MOT_duration=15 to MOT,
calls it, and hands the results to stack. So a stage reaches cascade bare and
reaches stack applied, and the two are not interchangeable -- writing
stack(MOT) raises (see _ensure_stackable), and cascade(MOT(...)) fails because
the result takes no keywords to route.
What it returns
A stage, like stack: to_timeline(cascade(...)) is the experiment's timeline, and
a cascade is itself stackable, so one beginning mid-experiment composes like any
other stage.
Routing is strict: a keyword that names no stage, or that names one but is not a
parameter of it, raises rather than being dropped. The alternative -- letting an
unrouted keyword broadcast to every stage -- was considered and rejected, because
every stage would turn it into rows, and into different kinds of row depending on
whether the stage ends in an update or a ramp.
Strictness is not configured but derived, from whether the target has **kwargs.
That is only safe because the operation layer confines the open namespace to
default_state and the two functions that wrap it; a stage that collects **kwargs
is, correctly, still permissive here.
Source code in src/wignertime/timeline.py
986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 | |
context_info(timeline)
Useful data (currently 'variables' and 'times') concerning every context. The result is a dictionary, indexed by context.
e.g. To get the start and end times of the 'MOT' context, call context_info(timeline)['MOT']['times].
Source code in src/wignertime/timeline.py
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | |
expand(timeline, **function_args)
Converts the functions marked in the timeline into individual rows, i.e. applies the functions to the given data.
This is generally a 'one-way' operation and so should only be carried out before the timeline is implemented on a device.
It takes a timeline and returns one, and is not a stage (#85, C7 item 6). Unlike
update, ramp and anchor, which add rows, it transforms the whole timeline it is
given, so inside a stack it expanded every ramp built so far and dropped function,
and the expand in adwin.core.convert then did nothing.
A keyword given to expand is a default, never an override (#65, C7 item 7). It
reaches only the ramp functions that leave it unstated (util.function__defaults),
so expand(timeline, time_resolution=...) samples the ramps that bind no resolution,
and a ramp that binds one keeps it: function=functools.partial(tanh,
time_resolution=1e-3) is a coarse ramp in a timeline expanded at the cycle period.
That binding used to be overridden silently, since a partial still declares the
keyword. A ramp binding none, expanded with none given, raises.
How many rows make up one ramp is read from the ramp function itself
(ramp_function.points), not passed in. It used to be the num__bounds argument,
which could be given a number the data did not match and was named for the two-point
case in which it need not have been given at all -- start and end are the bounds
only while there is nothing between them (B6).
Source code in src/wignertime/timeline.py
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 | |
ramp(*, duration=None, time=None, time2=None, context=wt_config.CONTEXT__INFER, origin=wt_config.ORIGIN__INFER, origin2=[wt_config.VARIABLE, 0.0], function=wt_ramp_function.tanh, **vtvc_dict)
Convenient ways of defining pairs of points and a function!
A ramp defines ranges of values for each variable across time from a beginning time-value pair to an ending time-value pair. Primarily, for the sake of switching analogue devices on and off in a controllable way. Although a ramp can be as simple as a linear value progression from start to end, the default function for a timeline is hyperbolic tan. This allows the user to soften the value gradient at the beginning and end of the function.
ramp has a slightly different interface to update. **vtvc_dict follows that of update; to specify the points manually (in big lists), use update.
A ramp is two points and a function between them: its start, which is always where the variable is at that instant, and its end, which is written.
Examples of calling ramp Normally, it will look something like ` tl.stack( tl.ramp( coil__compensation_X__A=0.0, coil__compensation_Y__A=0.0, coil__MOT_lower_plus__A=0.0, coil__MOT_upper_plus__A=0.0,
duration=duration,
context="final_ramps"))
` The variables are given end values independently and other options collectively. By default, the starting time is also inferred from the previous timeline and so chains of operations can be built up conveniently.
For simpler ramps, it can still be easier, like in update, to supply everything in a list, e.g.
tl.ramp(lockbox__MOT__MHz=[500e-3,0.0])
or
tl.ramp(lockbox__MOT__MHz=[500e-3, 0.0, "final_ramps"]) - if you want a new context.
This works because by default the ending time is relative to the starting time (see the origin keyword argument), such that 't_end' and 'duration' are the same.
A ramp always starts where its variable is (#142, 2026-09-29). A start value that
differs from the current one is a step hidden inside a ramp, the discontinuity a ramp
exists to avoid. A jump that is wanted is an update before the ramp, where it shows
in the table::
stack(update(coil__A=1.0), ramp(coil__A=3.0, duration=0.1))
So a start is never written: the 2-D form that stated one is refused, and so is a
value in the value slot of origin. A variable not yet set has to be set first.
Nor may a ramp start or end inside another ramp of the same variable (#157).
origin places the start in time. It defaults to wt_config.INFER, the
anchor-then-last chain, and None means the same; origin=0.0 is absolute time, as
for update.
context defaults to wt_config.INFER, and None means the same: an unstated row
inherits the latest context of timeline. Every row has a context (#156), so
context="" is refused.
NOTE: duration is a human-readable convenience for normal API usage. This is because the temporal origin of the second point is almost always in reference to the first point. Where there is a conflict, time2 will have supremacy. It is time2 because origin2 places the same point, the end.
Source code in src/wignertime/timeline.py
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 | |
stack(*stages, **kws)
Composes stages, in execution order, into one stage::
stage = stack(update(...), ramp(...), anchor(...))
timeline = to_timeline(stage, onto=timeline)
stack takes stages only and returns one. To apply it, use to_timeline.
Constituents must already have been called: stack(MOT(duration=15)), never
stack(MOT), which raises. This is the opposite of cascade, which takes the stage
functions themselves and calls them with the keywords routed to each.
Keywords given to stack are passed on to its constituents. This is particularly
convenient for a shared context::
stack(update(...), ramp(...), anchor(...), context="MOT")
A forwarded keyword is a default for the constituents, not an override: it fills
what a constituent left unstated and leaves what it stated alone, so
stack(update(x=1, context="ADwin_Finish"), anchor(1.0), context="finalRamps") keeps
x in its reserved context. The same holds between stacks: a stage is itself a
stack, and a keyword given to an outer one reaches the stages inside it, where each
stack's own keywords, stated closer, take precedence.
Source code in src/wignertime/timeline.py
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 | |
to_timeline(stage, onto=None)
The one way from a stage to a timeline: stage applied to onto, or to an empty
timeline when onto is not given.
A stage is what update, ramp, anchor, stack and cascade return, and what
a user-defined stage returns: a function of a timeline. So it is written once,
relative to its own beginning, and can be placed anywhere. A timeline is data -- the
table that is plotted, archived and converted for the hardware. to_timeline is
where the one becomes the other, and nothing else is::
timeline = to_timeline(cascade(init, MOT, molasses, finish, MOT_duration=15))
onto is first-class, not a convenience. Building a timeline can be expensive, so a
parameter scan keeps its base as a table and places each variation onto it::
base = to_timeline(experiment)
shots = [to_timeline(imaging(delay), onto=base) for delay in delays]
to_timeline(b, onto=to_timeline(a)) is the same table as to_timeline(stack(a, b)).
On an empty timeline there is nothing to be relative to, so the first rows are placed in absolute time, and nothing to inherit a context from, so they must name theirs (#156).
Source code in src/wignertime/timeline.py
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 | |
update(*, time=0.0, context=wt_config.CONTEXT__INFER, origin=wt_config.ORIGIN__INFER, **vtvc_dict)
Commands variables at instants: the rows of a timeline.
It returns a stage, for a stack and for to_timeline. The first rows of a
timeline are an update like any other, applied to an empty timeline::
initial = to_timeline(update(AOM__MOT=1, shutter__MOT=0, time=0.0, context="init"))
Input grammar
A variable is named as a keyword, and followed by what it does::
update(AOM__MOT=<follows>)
where <follows> is one of
====================================== ==========================================
value at time
[time, value]
[time, value, context]
[[time, value], [time, value], ...] several instants for one variable
====================================== ==========================================
Several variables are given at once, and a computed set through **::
update(AOM__MOT=1, shutter__MOT=[0.1, 1, "MOT"])
update(**{name: value for name, value in ...})
time and context are defaults, not overrides — a variable stating its own
keeps it. The keyword namespace is open by design, so an unrecognised keyword is
read as a variable name rather than rejected (see the manuscript's sec:forwarding);
that is what makes the injection idiom work, and it is why there is no second,
positional way in to be confused with it.
Origin and context
origin defaults to wt_config.INFER: the anchor-then-last chain
(config.ORIGIN__DEFAULTS) fills whichever slots are left unstated, so time is a
duration from the end of the preceding stage. None means the same. For absolute
time, write origin=0.0. On an empty timeline there is nothing to be relative to,
and the origin is absolute zero.
time=-math.inf is before the run and time=math.inf after it (#154): the initial
and the final state, in the contexts a backend reserves for them (ADwin_LowInit,
ADwin_Finish). Neither is an instant, so nothing is placed relative to them, and
until something is written at an instant the origin is absolute zero.
context defaults to wt_config.INFER too: an unstated row inherits the latest
context of the timeline it joins, from a row at an instant. None means the same.
Every row has a context (#156): on an empty timeline, or one holding only the state
before the run, there is nothing to inherit, so the rows must name theirs, and
context="" is refused. A row added to a finished timeline inherits from the last
stage of the run, not from the final state at +∞.
Source code in src/wignertime/timeline.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | |
variable
Outlines the conventions for variables and provides some convenience functions for working with them.
parse(variable)
A dictionary of device, UID and unit.
The convention is <device>__<UID>__<unit> for an analog variable and
<device>__<UID> for a digital one, e.g. coil__MOT_lower__A and shutter__MOT.
<device> and <unit> contain no _; the UID may contain single ones.
The convention is spelled out by config.VARIABLE__REGEX, which is read here on every call so that a site applying a different one can rebind it.
Source code in src/wignertime/variable.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
units(timeline, do_digital=True)
Returns a set of different timeline units (strs).
Source code in src/wignertime/variable.py
61 62 63 64 65 66 67 68 69 70 | |
without_unit(variable)
coil__MOT_lower__A without its unit, coil__MOT_lower; a digital name as it is.
Source code in src/wignertime/variable.py
55 56 57 58 | |