Skip to content

Commit 054e281

Browse files
committed
Added AnalogQuantity.square_wave().
1 parent a805b69 commit 054e281

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

labscript/functions.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,53 @@ def piecewise_accel(duration,initial,final):
4848
+ (-9*t**3/duration**3 + 27./2*t**2/duration**2 - 9./2*t/duration + 1./2) * (t<2*duration/3)*(t>=duration/3)
4949
+ (9./2*t**3/duration**3 - 27./2 * t**2/duration**2 + 27./2*t/duration - 7./2) * (t>= 2*duration/3))
5050

51+
def square_wave(duration, level_0, level_1, frequency, phase, duty_cycle):
52+
def square_wave_fixed_parameters(t):
53+
# Phase goes from 0 to 1 (NOT 2 pi) over one period.
54+
rising_edge_phase = 1 - duty_cycle
55+
wrapped_phases = (frequency * t + phase) % 1.0
56+
# Ensure wrapped_phases is an array.
57+
wrapped_phases = np.array(wrapped_phases)
58+
59+
# Round phases to avoid issues with numerics. Rounding the phase only
60+
# changes the output when the phase is just below a threshold where the
61+
# output changes values. So if a phase is just below the threshold where
62+
# the output changes state (within PHASE_TOLERANCE), round it up so that
63+
# the output does change state there. The value of PHASE_TOLERANCE is
64+
# based on the fact that labscript internally rounds all times to
65+
# multiples of 0.1 ns.
66+
LABSCRIPT_TIME_RESOLUTION = 0.1e-9 # 0.1 ns.
67+
MIN_PHASE_STEP = frequency * LABSCRIPT_TIME_RESOLUTION
68+
PHASE_TOLERANCE = MIN_PHASE_STEP / 2.0
69+
# Round phases near level_0 -> level_1 transition at phase =
70+
# rising_edge_phase.
71+
is_near_edge = np.isclose(
72+
wrapped_phases,
73+
rising_edge_phase,
74+
rtol=0,
75+
atol=PHASE_TOLERANCE,
76+
)
77+
wrapped_phases[is_near_edge] = rising_edge_phase
78+
# Round phases near level_1 -> level_0 transition at phase = 1.
79+
is_near_edge = np.isclose(
80+
wrapped_phases,
81+
1,
82+
rtol=0,
83+
atol=PHASE_TOLERANCE,
84+
)
85+
wrapped_phases[is_near_edge] = 0
86+
87+
# Initialize array to store output values.
88+
outputs = np.full_like(t, level_0)
89+
90+
# Use boolean indexing to set output to level_1 at the appropriate
91+
# times. For example level_0 for phases [0, 0.5) and level_1 for phases
92+
# [0.5, 1.0) when duty_cycle is 0.5.
93+
level_1_times = (wrapped_phases >= rising_edge_phase)
94+
outputs[level_1_times] = level_1
95+
return outputs
96+
return square_wave_fixed_parameters
97+
5198
def pulse_sequence(pulse_sequence,period):
5299
pulse_sequence = np.asarray(sorted(pulse_sequence, key=lambda x: x[0], reverse=True))
53100
pulse_sequence_times = pulse_sequence[:, 0]

labscript/labscript.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,6 +1539,97 @@ def piecewise_accel_ramp(self, t, duration, initial, final, samplerate, units=No
15391539
'initial time': t, 'end time': t + truncation*duration, 'clock rate': samplerate, 'units': units})
15401540
return truncation*duration
15411541

1542+
def square_wave(self, t, duration, level_0, level_1, frequency, phase,
1543+
duty_cycle, samplerate, units=None, truncation=1.):
1544+
"""A standard square wave.
1545+
1546+
This method generates a square wave which starts at `level_0` (when its
1547+
phase is zero) then transitions to/from `level_1` at the specified
1548+
`frequency`.
1549+
1550+
Note that because the transitions of a square wave are sudden and
1551+
discontinuous, small changes in timings (e.g. due to numerical rounding
1552+
errors) can affect the output value. This is particularly relevant at
1553+
the end of the waveform, as the final output value may be different than
1554+
expected if the end of the waveform is close to an edge of the square
1555+
wave. Care is taken in the implementation of this method to avoid such
1556+
effects, but it still may be desirable to call `constant()` after
1557+
`square_wave()` to ensure a particular final value. The output value may
1558+
also be different than expected at certain moments in the middle of the
1559+
waveform due to the finite samplerate (which may be different than the
1560+
requested `samplerate`), particularly if the actual samplerate is not a
1561+
multiple of `frequency`.
1562+
1563+
Args:
1564+
t (float): The time at which to start the square wave.
1565+
duration (float): The duration for which to output a square wave
1566+
(assuming `truncation=1.0`).
1567+
level_0 (float): The initial level of the square wave, when the
1568+
phase is zero.
1569+
level_1 (float): The other level of the square wave.
1570+
frequency (float): The frequency of the square wave, in Hz.
1571+
phase (float): The initial phase of the square wave. Note that the
1572+
square wave is defined such that the phase goes from 0 to 1 (NOT
1573+
2 pi) over one cycle, so setting `phase=0.5` will start the
1574+
square wave advanced by 1/2 of a cycle. Setting `phase` to be
1575+
`1 - duty_cycle` will cause the waveform to start at `level_1`
1576+
rather than `level_0`.
1577+
duty_cycle (float): The fraction of the cycle for which the output
1578+
should be set to `level_1`. This should be a number between zero
1579+
and one inclusively. For example, setting `duty_cycle=0.1` will
1580+
create a square wave which outputs `level_0` over 90% of the
1581+
cycle and outputs `level_1` over 10% of the cycle.
1582+
samplerate (float): The requested rate at which to update the output
1583+
value. Note that the actual samplerate used may be different if,
1584+
for example, another output of the same device has a
1585+
simultaneous ramp with a different requested `samplerate`, or if
1586+
`1 / samplerate` isn't an integer multiple of the pseudoclock's
1587+
timing resolution.
1588+
units (str, optional): The units of the output values. If set to
1589+
`None` then the output's base units will be used. Defaults to
1590+
`None`.
1591+
truncation (float, optional): The actual duration of the square wave
1592+
will be `duration * truncation` and `truncation` must be set to
1593+
a value in the range [0, 1] (inclusively). Set to `1` to output
1594+
the full duration of the square wave. Setting it to `0` will
1595+
skip the square wave entirely. Defaults to `1.`.
1596+
1597+
Returns:
1598+
duration (float): The actual duration of the square wave, accounting
1599+
for `truncation`.
1600+
"""
1601+
# Check the argument values.
1602+
self._check_truncation(truncation)
1603+
if duty_cycle < 0 or duty_cycle > 1:
1604+
msg = """Square wave duty cycle must be in the range [0, 1]
1605+
(inclusively) but was set to {duty_cycle}.""".format(
1606+
duty_cycle=duty_cycle
1607+
)
1608+
raise LabscriptError(dedent(msg))
1609+
1610+
if truncation > 0:
1611+
# Add the instruction.
1612+
func = functions.square_wave(
1613+
round(t + duration, 10) - round(t, 10),
1614+
level_0,
1615+
level_1,
1616+
frequency,
1617+
phase,
1618+
duty_cycle,
1619+
)
1620+
self.add_instruction(
1621+
t,
1622+
{
1623+
'function': func,
1624+
'description': 'square wave',
1625+
'initial time': t,
1626+
'end time': t + truncation * duration,
1627+
'clock rate': samplerate,
1628+
'units': units,
1629+
}
1630+
)
1631+
return truncation * duration
1632+
15421633
def customramp(self, t, duration, function, *args, **kwargs):
15431634
units = kwargs.pop('units', None)
15441635
samplerate = kwargs.pop('samplerate')

0 commit comments

Comments
 (0)