Skip to content

Commit 4d26344

Browse files
authored
Merge pull request #77 from zakv/square-wave
Added AnalogQuantity.square_wave().
2 parents 62455b5 + deca5d8 commit 4d26344

File tree

2 files changed

+219
-0
lines changed

2 files changed

+219
-0
lines changed

labscript/functions.py

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

158+
def square_wave(duration, level_0, level_1, frequency, phase, duty_cycle):
159+
def square_wave_fixed_parameters(t):
160+
# Phase goes from 0 to 1 (NOT 2 pi) over one period.
161+
edge_phase_0_to_1 = duty_cycle
162+
wrapped_phases = (frequency * t + phase) % 1.0
163+
# Ensure wrapped_phases is an array.
164+
wrapped_phases = np.array(wrapped_phases)
165+
166+
# Round phases to avoid issues with numerics. Rounding the phase only
167+
# changes the output when the phase is just below a threshold where the
168+
# output changes values. So if a phase is just below the threshold where
169+
# the output changes state (within PHASE_TOLERANCE), round it up so that
170+
# the output does change state there. The value of PHASE_TOLERANCE is
171+
# based on the fact that labscript internally rounds all times to
172+
# multiples of 0.1 ns.
173+
LABSCRIPT_TIME_RESOLUTION = 0.1e-9 # 0.1 ns.
174+
MIN_PHASE_STEP = frequency * LABSCRIPT_TIME_RESOLUTION
175+
PHASE_TOLERANCE = MIN_PHASE_STEP / 2.0
176+
# Round phases near level_0 -> level_1 transition at phase =
177+
# edge_phase_0_to_1.
178+
is_near_edge = np.isclose(
179+
wrapped_phases,
180+
edge_phase_0_to_1,
181+
rtol=0,
182+
atol=PHASE_TOLERANCE,
183+
)
184+
wrapped_phases[is_near_edge] = edge_phase_0_to_1
185+
# Round phases near level_1 -> level_0 transition at phase = 1.
186+
is_near_edge = np.isclose(
187+
wrapped_phases,
188+
1,
189+
rtol=0,
190+
atol=PHASE_TOLERANCE,
191+
)
192+
wrapped_phases[is_near_edge] = 0
193+
194+
# Initialize array to store output values.
195+
outputs = np.full_like(t, level_0)
196+
197+
# Use boolean indexing to set output to level_1 at the appropriate
198+
# times. For example level_0 for phases [0, 0.5) and level_1 for phases
199+
# [0.5, 1.0) when duty_cycle is 0.5.
200+
level_1_times = (wrapped_phases >= edge_phase_0_to_1)
201+
outputs[level_1_times] = level_1
202+
return outputs
203+
return square_wave_fixed_parameters
204+
158205
def pulse_sequence(pulse_sequence,period):
159206
"""Returns a function that interpolates a pulse sequence.
160207

labscript/labscript.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,6 +1974,178 @@ def piecewise_accel_ramp(self, t, duration, initial, final, samplerate, units=No
19741974
'initial time': t, 'end time': t + truncation*duration, 'clock rate': samplerate, 'units': units})
19751975
return truncation*duration
19761976

1977+
def square_wave(self, t, duration, amplitude, frequency, phase, offset,
1978+
duty_cycle, samplerate, units=None, truncation=1.):
1979+
"""A standard square wave.
1980+
1981+
This method generates a square wave which starts HIGH (when its phase is
1982+
zero) then transitions to/from LOW at the specified `frequency` in Hz.
1983+
The `amplitude` parameter specifies the peak-to-peak amplitude of the
1984+
square wave which is centered around `offset`. For example, setting
1985+
`amplitude=1` and `offset=0` would give a square wave which transitions
1986+
between `0.5` and `-0.5`. Similarly, setting `amplitude=2` and
1987+
`offset=3` would give a square wave which transitions between `4` and
1988+
`2`. To instead specify the HIGH/LOW levels directly, use
1989+
`square_wave_levels()`.
1990+
1991+
Note that because the transitions of a square wave are sudden and
1992+
discontinuous, small changes in timings (e.g. due to numerical rounding
1993+
errors) can affect the output value. This is particularly relevant at
1994+
the end of the waveform, as the final output value may be different than
1995+
expected if the end of the waveform is close to an edge of the square
1996+
wave. Care is taken in the implementation of this method to avoid such
1997+
effects, but it still may be desirable to call `constant()` after
1998+
`square_wave()` to ensure a particular final value. The output value may
1999+
also be different than expected at certain moments in the middle of the
2000+
waveform due to the finite samplerate (which may be different than the
2001+
requested `samplerate`), particularly if the actual samplerate is not a
2002+
multiple of `frequency`.
2003+
2004+
Args:
2005+
t (float): The time at which to start the square wave.
2006+
duration (float): The duration for which to output a square wave
2007+
when `truncation` is set to `1`. When `truncation` is set to a
2008+
value less than `1`, the actual duration will be shorter than
2009+
`duration` by that factor.
2010+
amplitude (float): The peak-to-peak amplitude of the square wave.
2011+
See above for an example of how to calculate the HIGH/LOW output
2012+
values given the `amplitude` and `offset` values.
2013+
frequency (float): The frequency of the square wave, in Hz.
2014+
phase (float): The initial phase of the square wave. Note that the
2015+
square wave is defined such that the phase goes from 0 to 1 (NOT
2016+
2 pi) over one cycle, so setting `phase=0.5` will start the
2017+
square wave advanced by 1/2 of a cycle. Setting `phase` equal to
2018+
`duty_cycle` will cause the waveform to start LOW rather than
2019+
HIGH.
2020+
offset (float): The offset of the square wave, which is the value
2021+
halfway between the LOW and HIGH output values. Note that this
2022+
is NOT the LOW output value; setting `offset` to `0` will cause
2023+
the HIGH/LOW values to be symmetrically split around `0`. See
2024+
above for an example of how to calculate the HIGH/LOW output
2025+
values given the `amplitude` and `offset` values.
2026+
duty_cycle (float): The fraction of the cycle for which the output
2027+
should be HIGH. This should be a number between zero and one
2028+
inclusively. For example, setting `duty_cycle=0.1` will
2029+
create a square wave which outputs HIGH over 10% of the
2030+
cycle and outputs LOW over 90% of the cycle.
2031+
samplerate (float): The requested rate at which to update the output
2032+
value. Note that the actual samplerate used may be different if,
2033+
for example, another output of the same device has a
2034+
simultaneous ramp with a different requested `samplerate`, or if
2035+
`1 / samplerate` isn't an integer multiple of the pseudoclock's
2036+
timing resolution.
2037+
units (str, optional): The units of the output values. If set to
2038+
`None` then the output's base units will be used. Defaults to
2039+
`None`.
2040+
truncation (float, optional): The actual duration of the square wave
2041+
will be `duration * truncation` and `truncation` must be set to
2042+
a value in the range [0, 1] (inclusively). Set to `1` to output
2043+
the full duration of the square wave. Setting it to `0` will
2044+
skip the square wave entirely. Defaults to `1.`.
2045+
2046+
Returns:
2047+
duration (float): The actual duration of the square wave, accounting
2048+
for `truncation`.
2049+
"""
2050+
# Convert to values used by square_wave_levels, then call that method.
2051+
level_0 = offset + 0.5 * amplitude
2052+
level_1 = offset - 0.5 * amplitude
2053+
return self.square_wave_levels(
2054+
t,
2055+
duration,
2056+
level_0,
2057+
level_1,
2058+
frequency,
2059+
phase,
2060+
duty_cycle,
2061+
samplerate,
2062+
units,
2063+
truncation,
2064+
)
2065+
2066+
def square_wave_levels(self, t, duration, level_0, level_1, frequency,
2067+
phase, duty_cycle, samplerate, units=None,
2068+
truncation=1.):
2069+
"""A standard square wave.
2070+
2071+
This method generates a square wave which starts at `level_0` (when its
2072+
phase is zero) then transitions to/from `level_1` at the specified
2073+
`frequency`. This is the same waveform output by `square_wave()`, but
2074+
parameterized differently. See that method's docstring for more
2075+
information.
2076+
2077+
Args:
2078+
t (float): The time at which to start the square wave.
2079+
duration (float): The duration for which to output a square wave
2080+
when `truncation` is set to `1`. When `truncation` is set to a
2081+
value less than `1`, the actual duration will be shorter than
2082+
`duration` by that factor.
2083+
level_0 (float): The initial level of the square wave, when the
2084+
phase is zero.
2085+
level_1 (float): The other level of the square wave.
2086+
frequency (float): The frequency of the square wave, in Hz.
2087+
phase (float): The initial phase of the square wave. Note that the
2088+
square wave is defined such that the phase goes from 0 to 1 (NOT
2089+
2 pi) over one cycle, so setting `phase=0.5` will start the
2090+
square wave advanced by 1/2 of a cycle. Setting `phase` equal to
2091+
`duty_cycle` will cause the waveform to start at `level_1`
2092+
rather than `level_0`.
2093+
duty_cycle (float): The fraction of the cycle for which the output
2094+
should be set to `level_0`. This should be a number between zero
2095+
and one inclusively. For example, setting `duty_cycle=0.1` will
2096+
create a square wave which outputs `level_0` over 10% of the
2097+
cycle and outputs `level_1` over 90% of the cycle.
2098+
samplerate (float): The requested rate at which to update the output
2099+
value. Note that the actual samplerate used may be different if,
2100+
for example, another output of the same device has a
2101+
simultaneous ramp with a different requested `samplerate`, or if
2102+
`1 / samplerate` isn't an integer multiple of the pseudoclock's
2103+
timing resolution.
2104+
units (str, optional): The units of the output values. If set to
2105+
`None` then the output's base units will be used. Defaults to
2106+
`None`.
2107+
truncation (float, optional): The actual duration of the square wave
2108+
will be `duration * truncation` and `truncation` must be set to
2109+
a value in the range [0, 1] (inclusively). Set to `1` to output
2110+
the full duration of the square wave. Setting it to `0` will
2111+
skip the square wave entirely. Defaults to `1.`.
2112+
2113+
Returns:
2114+
duration (float): The actual duration of the square wave, accounting
2115+
for `truncation`.
2116+
"""
2117+
# Check the argument values.
2118+
self._check_truncation(truncation)
2119+
if duty_cycle < 0 or duty_cycle > 1:
2120+
msg = """Square wave duty cycle must be in the range [0, 1]
2121+
(inclusively) but was set to {duty_cycle}.""".format(
2122+
duty_cycle=duty_cycle
2123+
)
2124+
raise LabscriptError(dedent(msg))
2125+
2126+
if truncation > 0:
2127+
# Add the instruction.
2128+
func = functions.square_wave(
2129+
round(t + duration, 10) - round(t, 10),
2130+
level_0,
2131+
level_1,
2132+
frequency,
2133+
phase,
2134+
duty_cycle,
2135+
)
2136+
self.add_instruction(
2137+
t,
2138+
{
2139+
'function': func,
2140+
'description': 'square wave',
2141+
'initial time': t,
2142+
'end time': t + truncation * duration,
2143+
'clock rate': samplerate,
2144+
'units': units,
2145+
}
2146+
)
2147+
return truncation * duration
2148+
19772149
def customramp(self, t, duration, function, *args, **kwargs):
19782150
"""Define a custom function for the output.
19792151

0 commit comments

Comments
 (0)