diff --git a/src/qibolab/pulses.py b/src/qibolab/pulses.py index d37417bfc..6a0a39134 100644 --- a/src/qibolab/pulses.py +++ b/src/qibolab/pulses.py @@ -19,6 +19,22 @@ """ +def _np_array_from_string_or_array_like(x) -> np.ndarray: + """Convert input into numpy array. + + The input can be in one of these forms: + 1. string in form '[1, 2, 3, ...]' + 2. list, e.g. [1, 2, 3, ...] + 3. numpy array. This will be identity conversion. + """ + if isinstance(x, str): + return np.fromstring(x[1:-1], sep=",") + elif isinstance(x, (list, np.ndarray)): + return np.array(x) + else: + raise ValueError(f"Data in unrecognized format: {x}") + + class PulseType(Enum): """An enumeration to distinguish different types of pulses. @@ -215,12 +231,19 @@ def eval(value: str) -> "PulseShape": To be replaced by proper serialization. """ - shape_name = re.findall(r"(\w+)", value)[0] - if shape_name not in globals(): - raise ValueError(f"shape {value} not found") - shape_parameters = re.findall(r"[-\w+\d\.\d]+", value)[1:] - # TODO: create multiple tests to prove regex working correctly - return globals()[shape_name](*shape_parameters) + match = re.fullmatch(r"(\w+)\((.*)\)", value) + shape_name, params = None, None + if match is not None: + shape_name, params = match.groups() + if match is None or shape_name not in globals(): + raise ValueError(f"shape {value} not recognized") + + single_item_pattern = r"[^,\s\[\]\(\)]+" + csv_items_pattern = rf"(?:{single_item_pattern}(?:,\s*)?)*" + param_pattern = ( + rf"\[{csv_items_pattern}\]|\w+\({csv_items_pattern}\)|{single_item_pattern}" + ) + return globals()[shape_name](*re.findall(param_pattern, params)) class Rectangular(PulseShape): @@ -509,12 +532,14 @@ class IIR(PulseShape): # p = [b0 = 1−k +k ·α, b1 = −(1−k)·(1−α),a0 = 1 and a1 = −(1−α)] # p = [b0, b1, a0, a1] - def __init__(self, b, a, target: PulseShape): + def __init__(self, b, a, target): self.name = "IIR" - self.target: PulseShape = target + self.target: PulseShape = ( + PulseShape.eval(target) if isinstance(target, str) else target + ) self._pulse: Pulse = None - self.a: np.ndarray = np.array(a) - self.b: np.ndarray = np.array(b) + self.a: np.ndarray = _np_array_from_string_or_array_like(a) + self.b: np.ndarray = _np_array_from_string_or_array_like(b) # Check len(a) = len(b) = 2 def __eq__(self, item) -> bool: @@ -596,8 +621,10 @@ class SNZ(PulseShape): def __init__(self, t_idling, b_amplitude=None): self.name = "SNZ" self.pulse: Pulse = None - self.t_idling: float = t_idling - self.b_amplitude = b_amplitude + self.t_idling: float = float(t_idling) + self.b_amplitude = ( + float(b_amplitude) if b_amplitude is not None else b_amplitude + ) def __eq__(self, item) -> bool: """Overloads == operator.""" @@ -708,9 +735,11 @@ def __init__(self, envelope_i, envelope_q=None): self.name = "Custom" self.pulse: Pulse = None - self.envelope_i: np.ndarray = np.array(envelope_i) + self.envelope_i: np.ndarray = _np_array_from_string_or_array_like(envelope_i) if envelope_q is not None: - self.envelope_q: np.ndarray = np.array(envelope_q) + self.envelope_q: np.ndarray = _np_array_from_string_or_array_like( + envelope_q + ) else: self.envelope_q = self.envelope_i @@ -745,7 +774,7 @@ def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: raise ShapeInitError def __repr__(self): - return f"{self.name}({self.envelope_i[:3]}, ..., {self.envelope_q[:3]}, ...)" + return f"{self.name}({self.envelope_i[:3]}, {self.envelope_q[:3]})" @dataclass diff --git a/tests/test_pulses.py b/tests/test_pulses.py index 9939b8fae..62f7a64b6 100644 --- a/tests/test_pulses.py +++ b/tests/test_pulses.py @@ -12,6 +12,7 @@ Custom, Drag, DrivePulse, + Exponential, FluxPulse, Gaussian, GaussianSquare, @@ -275,8 +276,70 @@ def test_pulses_pulseshape_sampling_rate(shape): def test_pulseshape_eval(): shape = PulseShape.eval("Rectangular()") assert isinstance(shape, Rectangular) - with pytest.raises(ValueError): - shape = PulseShape.eval("Ciao()") + + shape = PulseShape.eval("Exponential(1, 2)") + assert isinstance(shape, Exponential) + assert shape.tau == 1 + assert shape.upsilon == 2 + + shape = PulseShape.eval("Exponential(4, 5, 6)") + assert isinstance(shape, Exponential) + assert shape.tau == 4 + assert shape.upsilon == 5 + assert shape.g == 6 + + shape = PulseShape.eval("Gaussian(3.1)") + assert isinstance(shape, Gaussian) + assert shape.rel_sigma == 3.1 + + shape = PulseShape.eval("GaussianSquare(5, 78)") + assert isinstance(shape, GaussianSquare) + assert shape.rel_sigma == 5 + assert shape.width == 78 + + shape = PulseShape.eval("Drag(4, 0.1)") + assert isinstance(shape, Drag) + assert shape.rel_sigma == 4 + assert shape.beta == 0.1 + + shape = PulseShape.eval("IIR([1, 2, 3], [5], Drag(3, 0.2))") + assert isinstance(shape, IIR) + assert np.array_equal(shape.b, np.array([1, 2, 3])) + assert np.array_equal(shape.a, np.array([5])) + assert isinstance(shape.target, Drag) + assert shape.target.rel_sigma == 3 + assert shape.target.beta == 0.2 + + shape = PulseShape.eval("SNZ(10, 20)") + assert isinstance(shape, SNZ) + assert shape.t_idling == 10 + assert shape.b_amplitude == 20 + + shape = PulseShape.eval("eCap(3.14)") + assert isinstance(shape, eCap) + assert shape.alpha == 3.14 + + shape = PulseShape.eval("Custom([1, 2, 3], [4, 5, 6])") + assert isinstance(shape, Custom) + assert np.array_equal(shape.envelope_i, np.array([1, 2, 3])) + assert np.array_equal(shape.envelope_q, np.array([4, 5, 6])) + + with pytest.raises(ValueError, match="shape .* not recognized"): + _ = PulseShape.eval("Ciao()") + + +@pytest.mark.parametrize( + "value_str", + ["-0.1", "+0.1", "1.", "-3.", "+1.", "-0.1e2", "1e-2", "+1e3", "-3e-1", "-.4"], +) +def test_pulse_shape_eval_numeric_varieties(value_str): + shape = PulseShape.eval(f"Drag(1, {value_str})") + assert isinstance(shape, Drag) + assert shape.beta == float(value_str) + + shape = PulseShape.eval(f"Custom([0.1, {value_str}])") + assert isinstance(shape, Custom) + assert np.array_equal(shape.envelope_i, np.array([0.1, float(value_str)])) @pytest.mark.parametrize("rel_sigma,beta", [(5, 1), (5, -1), (3, -0.03), (4, 0.02)])