Attune Data Structures

The data structures in attune take inspiration from drawing parallels to nomenclature in music. The experimenter is the conductor of an orchestra of several Instrument objects.

At a high level, the Instrument is what users will directly interact with. An Instrument represents a collection of motors which must follow interpolated curves to produce one logical “one to many” mapping of logical position to motor positions. This collection can be as simple as a Spectral Delay Correction (SDC) mapping a color of light onto a single motor position to account for arrival time differences due to color of light. Alternatively, it can be a complex collection of several motors as in an Optical Parametric Amplifier (OPA), which requires all motors to be set to produce light of a selected color. When called like a function, the Instrument provides a Note which maps a given position to underlying motor positions.

An Instrument may consist of several different modes (Arrangement s) which can allow for things like different mixing processes in OPAs or multiple correction factors for SDC. Each Arrangement in turn consists of Tune objects, which provide individual mappings for input to output. By default a Tune maps an input to a motor position or Setable, however Arrangement s may be nested allowing for references to lower level arrangements. This behavior allows the tuning curve for OPA mixing processes (such as Second Harmonic of Signal) to be built by adding one (or more) additional Setable to an existing Signal Arrangement. Parent Arrangement s may override the position of Setable s in the child arrangement.

These data structures are treated as “immutable” objects. This means that once created the values and the relationships of the objects are not changed. Instead, we have a system of Transitions which provide new, updated Instrument instances. This allows the context of how instruments were created to be preserved.

Setable

A Setable consists of only two pieces of information: a name (a string) and a default position (None or string or float).

If the default is set, then any Note which does not explicitly set that Setable will inherit the default position. If there is no default, then the Note will simply not specify the Setable at all.

In most cases, Setable objects are not required to be explicitly created, unless you wish to take advantage of default behavior.

Setable provides an as_dict() method to allow for serialization.

no_default = attune.Setable("no_default")
default = attune.Setable("default", default=1.2)

Tune

A Tune represents a continuous transformation from an independent variable to a dependent variable.

Currently the Tune class assumes the independent variable is in units of nm to simplify the code. The dependent variable units can be specified using the dep_units kwarg to __init__().

The Tune object can be called as a function, which returns the linear interpolation of the independent to dependent variable mapping. The units of the input and/or desired output can be specified using keyword arguments.

Tune provides an as_dict() method to allow for serialization. Tune also provides convenience attributes to access the limits of the tune: ind_min and ind_max.

tune = attune.Tune([400, 500, 600, 700], [0, 1, 4, 9], dep_units="mm")
val = tune(555) # returns 2.65
val = tune(555, dep_units="cm") # returns 0.265
val = tune(20555, ind_units="wn") # returns 0.86499635

DiscreteTune

A DiscreteTune represents a discrete transform from a continuous independent variable to discrete string output dependent values.

Currently the DiscreteTune class assumes the independent variable is in units of nm to simplify the code.

The outputs are stored as a dictionary of output key string to 2-tuple of ranges (min, max), and a default value as fallback. The dictionary is ordered, and the first valid range (inclusive of endpoints) is the value returned. Notably, this construction does limit each potential output to a single range, thus limiting (though not eliminating) the ability to have non-consecutive ranges which evaluate to the same output value. You can, however, place higher priority (earlier) ranges inside of other ranges to allow for some cases of non-consecutive ranges, as well as using default to get a similar effect.

DiscreteTune provides an as_dict() method to allow for serialization.

dt = attune.DiscreteTune({"hi": (100, 200), "lo": (10, 20), "inner": (50, 60), "med": (20, 100)}, default="def")
dt(5) == "def"
dt(15) == "lo"
dt(20) == "lo"
dt(30) == "med"
dt(55) == "inner"
dt(70) == "med"
dt(100) == "hi"
dt(150) == "hi"
dt(500) == "def"

Arrangement

An Arrangment provides a dict-like set of string names to Tune and DiscreteTune objects. The tunes may represent either Setable (the default) or an Arrangement (when the Instrument contains an Arrangement of that name). When it represents an Arrangement, the Instrument will recursively evaluate for all Setable s.

All of the tunes must have the same independent units and must overlap (the former is easy since all tunes currently have nm units).

Arrangement provides an as_dict() method to allow for serialization.

arr = attune.Arrangement("arr", {"continuous": tune, "discrete": dt})

Instrument

An Instrument is the top level representation of the system, the one which users most directly interact with. An Instrument provides a dict-like access to a set of Arrangement s as well as a secondary dict of Setable s. Additionally, Instrument provide a system of tracking history via Transition object (See also Transitions).

Most commonly, Instrument objects are called like functions to provide Setable positions (as a Note) for a particular independent value. If the independent value is valid for only a single arrangement, then the arrangement does not need to be specified. If, however, the independent value is valid for multiple arrangements, it must be specified.

The setables may be ignored if there is no need for defaults.

Instrument provides both as_dict() and save() to allow for serialization.

tune = attune.Tune([0, 1], [0, 1])
tune1 = attune.Tune([0.5, 1.5], [0, 1])
first = attune.Arrangement("first", {"tune": tune})
second = attune.Arrangement("second", {"tune": tune1})
inst = attune.Instrument({"first": first, "second": second}, {"tune": attune.Setable("tune")})
inst(0.25)["tune"] == 0.25
inst(1.25)["tune"] == 0.75
inst(0.75) # raises exception because it is valid for both arrangements
inst(0.75, "first")["tune"] == 0.75
inst(0.75, "second")["tune"] == 0.25

Loading from files

The native format for Instrument is JSON encodable as provided by save(). To read back an attune JSON file you can use attune.open().

instr = attune.open("instrument.json")

Alternatively, some formats such as Light Conversion TOPAS4 files can be parsed into attune Instrument s. TOPAS4 tuning curves are made up of multiple files which contain the information needed to recreate the Instrument, so the method points to a folder which contains the files.

instr = attune.io.from_topas4("path/to/topas4/")

Note

A Note is the type returned when an Instrument is called as a function. It is little more than a dict-like mapping of setable names to positions plus an indication of which arrangement was used to generate those positions. A Note also contains a dictionary of setables for convenience.