diff --git a/README.md b/README.md index ceb972a49..932b35be7 100644 --- a/README.md +++ b/README.md @@ -5,38 +5,13 @@ A framework for interacting with [Pasqal][pasqal] devices at the **pulse** level Pulser is a library that allows for writing sequences of pulses representing the behaviour of some Pasqal processor prototypes. -- The user can define one or several channels to target the qubits in the device. -- A basis can be chosen to represent the transition levels of the Rydberg atom-arrays setup. -- Channels can be local or global depending on the application. In the local case, - a phase-shift option is included to reduce complexity -- There's a visualization routine for ease of use. - -The pulse sequences can then be read and operated by real Pasqal devices or -emulated (using [QuTiP][qutip] libraries). ## Installation -To install Pulser from source, do the following from within the repository -after cloning it: - -```bash -pip install -e . -``` - -## Testing - -To run the test suite, after installation first run the following to install -development requirements: - -```bash -pip install -r requirements.txt -``` - -Then, do the following to run the test suite and report test coverage: +To install Pulser from PyPI: ```bash -pytest --cov pulser +pip install pulser ``` [pasqal]: https://pasqal.io/ -[qutip]: http://qutip.org/ diff --git a/pulser/__init__.py b/pulser/__init__.py index dd5827e1c..c8045dc6b 100644 --- a/pulser/__init__.py +++ b/pulser/__init__.py @@ -15,7 +15,3 @@ """A pulse-level composer for Pasqal's quantum devices.""" from pulser.pulse import Pulse - -from pulser.register import Register - -from pulser.sequence import Sequence diff --git a/pulser/channels.py b/pulser/channels.py deleted file mode 100644 index 9a016b459..000000000 --- a/pulser/channels.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2020 Pulser Development Team -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - - -class Channel(ABC): - """Base class for an hardware channel.""" - - def __init__(self, addressing, max_abs_detuning, max_amp, - retarget_time=None, max_targets=1): - """Initialize a channel with specific characteristics. - - Args: - addressing (str): 'Local' or 'Global'. - max_abs_detuning (tuple): Maximum possible detuning (in MHz), in - absolute value. - max_amp(tuple): Maximum pulse amplitude (in MHz). - - Keyword Args: - retarget_time (default = None): Time to change the target (in ns). - max_targets (int, default=1): (For local channels only) How - many qubits can be addressed at once by the same beam. - """ - if addressing == 'Local': - if retarget_time is None: - raise ValueError("Must set retarget time for local channel.") - self.retarget_time = int(retarget_time) - if not isinstance(max_targets, int): - raise TypeError("max_targets must be an int.") - elif max_targets < 1: - raise ValueError("max_targets must be at least 1") - else: - self.max_targets = max_targets - - elif addressing != 'Global': - raise ValueError("Addressing can only be 'Global' or 'Local'.") - - self.addressing = addressing - - if max_abs_detuning < 0: - raise ValueError("Maximum absolute detuning has to be positive.") - self.max_abs_detuning = max_abs_detuning - - if max_amp <= 0: - raise ValueError("Maximum channel amplitude has to be positive.") - self.max_amp = max_amp - - @classmethod - def Local(cls, max_abs_detuning, max_amp, retarget_time, max_targets=1): - """Initializes the channel with local adressing. - - Args: - max_abs_detuning (tuple): Maximum possible detuning (in MHz), in - absolute value. - max_amp(tuple): Maximum pulse amplitude (in MHz). - - Keyword Args: - retarget_time (default = None): Time to change the target (in ns). - max_targets (int, default=1): (For local channels only) How - many qubits can be addressed at once by the same beam.""" - - return cls('Local', max_abs_detuning, max_amp, max_targets=max_targets, - retarget_time=retarget_time) - - @classmethod - def Global(cls, max_abs_detuning, max_amp): - """Initializes the channel with global adressing. - - Args: - max_abs_detuning (tuple): Maximum possible detuning (in MHz), in - absolute value. - max_amp(tuple): Maximum pulse amplitude (in MHz).""" - - return cls('Global', max_abs_detuning, max_amp) - - @property - @abstractmethod - def name(self): - pass - - @property - @abstractmethod - def basis(self): - """The target transition at zero detuning.""" - pass - - def __repr__(self): - s = ".{}(Max Absolute Detuning: {} MHz, Max Amplitude: {} MHz" - config = s.format(self.addressing, self.max_abs_detuning, self.max_amp) - if self.addressing == 'Local': - config += f", Target time: {self.retarget_time} ns" - if self.max_targets > 1: - config += f", Max targets: {self.max_targets}" - config += f", Basis: '{self.basis}'" - return self.name + config + ")" - - -class Raman(Channel): - """Raman beam channel. - - Args: - addressing (str): 'Local' or 'Global'. - max_abs_detuning (tuple): Maximum possible detuning (in MHz), in - absolute value. - max_amp(tuple): Maximum pulse amplitude (in MHz). - """ - @property - def name(self): - return 'Raman' - - @property - def basis(self): - """The target transition at zero detuning.""" - return 'digital' - - -class Rydberg(Channel): - """Rydberg beam channel. - - Args: - addressing (str): 'Local' or 'Global'. - max_abs_detuning (tuple): Maximum possible detuning (in MHz), in - absolute value. - max_amp(tuple): Maximum pulse amplitude (in MHz). - """ - @property - def name(self): - return 'Rydberg' - - @property - def basis(self): - """The target transition at zero detuning.""" - return 'ground-rydberg' - - -class MW(Channel): - """Microwave channel. - - Args: - addressing (str): 'Local' or 'Global'. - max_abs_detuning (tuple): Maximum possible detuning (in MHz), in - absolute value. - max_amp(tuple): Maximum pulse amplitude (in MHz). - """ - @property - def name(self): - return 'MW' - - # TODO: Define basis for this channel diff --git a/pulser/devices.py b/pulser/devices.py deleted file mode 100644 index 30a5e3aa1..000000000 --- a/pulser/devices.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright 2020 Pulser Development Team -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - -import numpy as np -from scipy.spatial.distance import pdist - -from pulser.channels import Raman, Rydberg -from pulser.register import Register - - -class PasqalDevice(ABC): - """Abstract class for Pasqal Devices. - - Every Pasqal QPU should be defined as a child class of PasqalDevice, thus - following this template. - - Args: - qubits (dict, Register): A dictionary or a Register class instance with - all the qubits' names and respective positions in the array. - """ - - def __init__(self, qubits): - if isinstance(qubits, dict): - register = Register(qubits) - elif isinstance(qubits, Register): - register = qubits - else: - raise TypeError("The qubits must be a in a dict or Register class " - "instance.") - - self._check_array(list(register.qubits.values())) - self._register = register - - @property - @abstractmethod - def name(self): - """The device name.""" - pass - - @property - @abstractmethod - def max_dimensionality(self): - """Whether it works at most with a 2D or 3D array (returns 2 or 3).""" - pass - - @property - @abstractmethod - def max_atom_num(self): - """Maximum number of atoms that can be simultaneously trapped.""" - pass - - @property - @abstractmethod - def max_radial_distance(self): - """Maximum allowed distance from the center of the array.""" - pass - - @property - @abstractmethod - def min_atom_distance(self): - """Minimal allowed distance of atoms in the trap (in um).""" - pass - - @property - @abstractmethod - def channels(self): - """Channels available on the device.""" - pass - - @property - def supported_bases(self): - """Available electronic transitions for control and measurement.""" - return {ch.basis for ch in self.channels.values()} - - @property - def qubits(self): - """The dictionary of qubit names and their positions.""" - return self._register.qubits - - def _check_array(self, atoms): - if len(atoms) > self.max_atom_num: - raise ValueError("Too many atoms in the array, accepts at most" - "{} atoms.".format(self.max_atom_num)) - for pos in atoms: - if len(pos) != self.max_dimensionality: - raise ValueError("All qubit positions must be {}D " - "vectors.".format(self.max_dimensionality)) - - if len(atoms) > 1: - distances = pdist(atoms) # Pairwise distance between atoms - if np.min(distances) < self.min_atom_distance: - raise ValueError("Qubit positions don't respect the minimal " - "distance between atoms for this device.") - - if np.max(np.linalg.norm(atoms, axis=1)) > self.max_radial_distance: - raise ValueError("All qubits must be at most {}um away from the " - "center of the array.".format( - self.max_radial_distance)) - - -class Chadoq2(PasqalDevice): - """Chadoq2 device specifications.""" - - @property - def name(self): - """The device name.""" - return "Chadoq2" - - @property - def max_dimensionality(self): - """Whether it works at most with a 2D or 3D array (returns 2 or 3).""" - return 2 - - @property - def max_atom_num(self): - """Maximum number of atoms that can be simultaneously trapped.""" - return 100 - - @property - def max_radial_distance(self): - """Maximum allowed distance from the center of the array (in um).""" - return 50 - - @property - def min_atom_distance(self): - """Minimal allowed distance of atoms in the trap (in um).""" - return 4 - - @property - def channels(self): - """Channels available on the device.""" - return {'rydberg_global': Rydberg.Global(50, 2.5), - 'rydberg_local': Rydberg.Local(50, 10, 100), - 'rydberg_local2': Rydberg.Local(50, 10, 100), - 'raman_local': Raman.Local(50, 10, 100)} diff --git a/pulser/register.py b/pulser/register.py deleted file mode 100644 index 56b50c525..000000000 --- a/pulser/register.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright 2020 Pulser Development Team -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import matplotlib.pyplot as plt -import numpy as np - - -class Register: - """A quantum register containing a set of qubits. - - Args: - qubits (dict): Dictionary with the qubit names as keys and their - position coordinates (in um) as values - (e.g. {'q0':(2, -1, 0), 'q1':(-5, 10, 0), ...}). - """ - - def __init__(self, qubits): - if not isinstance(qubits, dict): - raise TypeError("The qubits have to be stored in a dictionary " - "matching qubit ids to position coordinates.") - self._ids = list(qubits.keys()) - self._coords = list(qubits.values()) - - @property - def qubits(self): - return dict(zip(self._ids, self._coords)) - - @classmethod - def from_coordinates(cls, coords, center=True, prefix=None): - """Creates the register from an array of coordinates. - - Args: - coords (ndarray): The coordinates of each qubit to include in the - register. - - Keyword args: - center(defaut=True): Whether or not to center the entire array - around the origin. - prefix (str): The prefix for the qubit ids. If defined, each qubit - id starts with the prefix, followed by an int from 0 to N-1 - (e.g. prefix='q' -> IDs: 'q0', 'q1', 'q2', ...). - """ - if center: - coords -= np.mean(coords, axis=0) # Centers the array - if prefix is not None: - pre = str(prefix) - qubits = {pre+str(i): pos for i, pos in enumerate(coords)} - else: - qubits = dict(enumerate(coords)) - return cls(qubits) - - @classmethod - def rectangle(cls, rows, columns, spacing=4, prefix=None): - """Initializes the register with the qubits in a rectangular array. - - Args: - rows (int): Number of rows. - columns (int): Number of columns. - - Keyword args: - spacing(float): The distance between neighbouring qubits in um. - prefix (str): The prefix for the qubit ids. If defined, each qubit - id starts with the prefix, followed by an int from 0 to N-1 - (e.g. prefix='q' -> IDs: 'q0', 'q1', 'q2', ...) - """ - coords = np.array([(x, y) for y in range(rows) - for x in range(columns)], dtype=float) * spacing - - return cls.from_coordinates(coords, center=True, prefix=prefix) - - @classmethod - def square(cls, side, spacing=4, prefix=None): - """Initializes the register with the qubits in a square array. - - Args: - side (int): Side of the square in number of qubits. - - Keyword args: - spacing(float): The distance between neighbouring qubits in um. - prefix (str): The prefix for the qubit ids. If defined, each qubit - id starts with the prefix, followed by an int from 0 to N-1 - (e.g. prefix='q' -> IDs: 'q0', 'q1', 'q2', ...). - """ - return cls.rectangle(side, side, spacing=spacing, prefix=prefix) - - @classmethod - def triangular_lattice(cls, rows, atoms_per_row, spacing=4, prefix=None): - """Initializes the register with the qubits in a triangular lattice. - - Initializes the qubits in a triangular lattice pattern, more - specifically a triangular lattice with horizontal rows, meaning the - triangles are pointing up and down. - - Args: - rows (int): Number of rows. - atoms_per_row (int): Number of atoms per row. - - Keyword args: - spacing(float): The distance between neighbouring qubits in um. - prefix (str): The prefix for the qubit ids. If defined, each qubit - id starts with the prefix, followed by an int from 0 to N-1 - (e.g. prefix='q' -> IDs: 'q0', 'q1', 'q2', ...). - """ - coords = np.array([(x, y) for y in range(rows) - for x in range(atoms_per_row)], dtype=float) - coords[:, 0] += 0.5 * np.mod(coords[:, 1], 2) - coords[:, 1] *= np.sqrt(3) / 2 - coords *= spacing - - return cls.from_coordinates(coords, center=True, prefix=prefix) - - def rotate(self, degrees): - """Rotate the array around the origin by the given angle. - - Args: - degrees (float): The angle of rotation in degrees. - """ - theta = np.deg2rad(degrees) - rot = np.array([[np.cos(theta), -np.sin(theta)], - [np.sin(theta), np.cos(theta)]]) - self._coords = [rot @ v for v in self._coords] - - def draw(self, with_labels=True): - """Draws the entire register. - - Keyword args: - with_labels(bool, default=True): If True, writes the qubit ID's - next to each qubit. - """ - pos = np.array(self._coords) - diffs = np.max(pos, axis=0) - np.min(pos, axis=0) - diffs[diffs < 9] *= 1.5 - diffs[diffs < 9] += 2 - big_side = max(diffs) - proportions = diffs / big_side - Ls = proportions * min(big_side/4, 10) # Figsize is, at most, (10,10) - - fig, ax = plt.subplots(figsize=Ls) - ax.scatter(pos[:, 0], pos[:, 1], s=30, alpha=0.7, - c='darkgreen') - ax.axvline(0, c='grey', alpha=0.5, linestyle=':') - ax.axhline(0, c='grey', alpha=0.5, linestyle=':') - ax.set_xlabel(r"$\mu m$") - ax.set_ylabel(r"$\mu m$") - ax.axis('equal') - ax.spines['right'].set_color('none') - ax.spines['top'].set_color('none') - - if with_labels: - for q, coords in zip(self._ids, self._coords): - ax.annotate(q, coords, fontsize=12, ha='left', va='bottom') - - plt.show() diff --git a/pulser/seq_drawer.py b/pulser/seq_drawer.py deleted file mode 100644 index 17e3947aa..000000000 --- a/pulser/seq_drawer.py +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright 2020 Pulser Development Team -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import matplotlib.pyplot as plt -import numpy as np - -from pulser.waveforms import ConstantWaveform - - -def gather_data(seq): - """Collects the whole sequence data for plotting. - - Args: - seq (Sequence): The input sequence of operations on a device. - - Returns: - data: The data to plot. - """ - # The minimum time axis length is 100 ns - seq._total_duration = max([seq._last(ch).tf for ch in seq._schedule - if seq._schedule[ch]] + [100]) - data = {} - for ch, sch in seq._schedule.items(): - time = [-1] # To not break the "time[-1]" later on - amp = [] - detuning = [] - target = {} - # phase_shift = {} - for slot in sch: - if slot.ti == -1: - target['initial'] = slot.targets - time += [0] - amp += [0] - detuning += [0] - continue - if slot.type in ['delay', 'target']: - time += [slot.ti, slot.tf-1] - amp += [0, 0] - detuning += [0, 0] - if slot.type == 'target': - target[(slot.ti, slot.tf-1)] = slot.targets - continue - pulse = slot.type - if (isinstance(pulse.amplitude, ConstantWaveform) and - isinstance(pulse.detuning, ConstantWaveform)): - time += [slot.ti, slot.tf-1] - amp += [pulse.amplitude._value] * 2 - detuning += [pulse.detuning._value] * 2 - else: - time += list(range(slot.ti, slot.tf)) - amp += pulse.amplitude.samples.tolist() - detuning += pulse.detuning.samples.tolist() - if time[-1] < seq._total_duration - 1: - time += [time[-1]+1, seq._total_duration-1] - amp += [0, 0] - detuning += [0, 0] - # Store everything - time.pop(0) # Removes the -1 in the beginning - data[ch] = {'time': time, 'amp': amp, 'detuning': detuning, - 'target': target} - if hasattr(seq, "_measurement"): - data[ch]['measurement'] = seq._measurement - return data - - -def draw_sequence(seq): - """Draw the entire sequence. - - Args: - seq (Sequence): The input sequence of operations on a device. - - Returns: - plt.show(): The plot of the sequence. - """ - - def phase_str(phi): - """Formats a phase value for printing.""" - value = (((phi + np.pi) % (2*np.pi)) - np.pi) / np.pi - if value == -1: - return r"$\pi$" - elif value == 0: - return "0" - else: - return r"{:.2g}$\pi$".format(value) - - n_channels = len(seq._channels) - if not n_channels: - raise SystemError("Can't draw an empty sequence.") - data = gather_data(seq) - time_scale = 1e3 if seq._total_duration > 1e4 else 1 - - # Boxes for qubit and phase text - q_box = dict(boxstyle="round", facecolor='orange') - ph_box = dict(boxstyle="round", facecolor='ghostwhite') - - fig = plt.figure(constrained_layout=False, figsize=(20, 4.5*n_channels)) - gs = fig.add_gridspec(n_channels, 1, hspace=0.075) - - ch_axes = {} - for i, (ch, gs_) in enumerate(zip(seq._channels, gs)): - ax = fig.add_subplot(gs_) - ax.spines['top'].set_color('none') - ax.spines['bottom'].set_color('none') - ax.spines['left'].set_color('none') - ax.spines['right'].set_color('none') - ax.tick_params(labelcolor='w', top=False, bottom=False, left=False, - right=False) - ax.set_ylabel(ch, labelpad=40, fontsize=18) - subgs = gs_.subgridspec(2, 1, hspace=0.) - ax1 = fig.add_subplot(subgs[0, :]) - ax2 = fig.add_subplot(subgs[1, :]) - ch_axes[ch] = (ax1, ax2) - for j, ax in enumerate(ch_axes[ch]): - ax.axvline(0, linestyle='--', linewidth=0.5, color='grey') - if j == 0: - ax.spines['bottom'].set_visible(False) - else: - ax.spines['top'].set_visible(False) - - if i < n_channels - 1 or j == 0: - ax.tick_params(axis='x', which='both', bottom=True, - top=False, labelbottom=False, direction='in') - else: - unit = 'ns' if time_scale == 1 else r'$\mu s$' - ax.set_xlabel(f't ({unit})', fontsize=12) - - for ch, (a, b) in ch_axes.items(): - basis = seq._channels[ch].basis - t = np.array(data[ch]['time']) / time_scale - ya = data[ch]['amp'] - yb = data[ch]['detuning'] - - t_min = -t[-1]*0.03 - t_max = t[-1]*1.05 - a.set_xlim(t_min, t_max) - b.set_xlim(t_min, t_max) - - max_amp = np.max(ya) - max_amp = 1 if max_amp == 0 else max_amp - amp_top = max_amp * 1.2 - a.set_ylim(-0.02, amp_top) - det_max = np.max(yb) - det_min = np.min(yb) - det_range = det_max - det_min - if det_range == 0: - det_min, det_max, det_range = -1, 1, 2 - det_top = det_max + det_range * 0.15 - det_bottom = det_min - det_range * 0.05 - b.set_ylim(det_bottom, det_top) - - a.plot(t, ya, color="darkgreen", linewidth=0.8) - b.plot(t, yb, color='indigo', linewidth=0.8) - a.fill_between(t, 0, ya, color="darkgreen", alpha=0.3) - b.fill_between(t, 0, yb, color="indigo", alpha=0.3) - a.set_ylabel('Amplitude (MHz)', fontsize=12, labelpad=10) - b.set_ylabel('Detuning (MHz)', fontsize=12) - - target_regions = [] # [[start1, [targets1], end1],...] - for coords in data[ch]['target']: - targets = list(data[ch]['target'][coords]) - tgt_strs = [str(q) for q in targets] - tgt_txt_y = max_amp*1.1-0.25*(len(targets)-1) - tgt_str = "\n".join(tgt_strs) - if coords == 'initial': - x = t_min + t[-1]*0.005 - target_regions.append([0, targets]) - if seq._channels[ch].addressing == 'Global': - a.text(x, amp_top*0.98, "GLOBAL", fontsize=13, - rotation=90, ha='left', va='top', bbox=q_box) - else: - a.text(x, tgt_txt_y, tgt_str, fontsize=12, ha='left', - bbox=q_box) - phase = seq._phase_ref[basis][targets[0]][0] - if phase: - msg = r"$\phi=$" + phase_str(phase) - a.text(0, max_amp*1.1, msg, ha='left', fontsize=12, - bbox=ph_box) - else: - ti, tf = np.array(coords) / time_scale - target_regions[-1].append(ti) # Closing previous regions - target_regions.append([tf + 1/time_scale, targets]) # New one - phase = seq._phase_ref[basis][targets[0]][tf * time_scale + 1] - a.axvspan(ti, tf, alpha=0.4, color='grey', hatch='//') - b.axvspan(ti, tf, alpha=0.4, color='grey', hatch='//') - a.text(tf + t[-1]*5e-3, tgt_txt_y, tgt_str, ha='left', - fontsize=12, bbox=q_box) - if phase: - msg = r"$\phi=$" + phase_str(phase) - wrd_len = len(max(tgt_strs, key=len)) - x = tf + t[-1]*0.01*(wrd_len+1) - a.text(x, max_amp*1.1, msg, ha='left', - fontsize=12, bbox=ph_box) - # Terminate the last open regions - if target_regions: - target_regions[-1].append(t[-1]) - for start, targets, end in target_regions: - q = targets[0] # All targets have the same ref, so we pick - ref = seq._phase_ref[basis][q] - if end != seq._total_duration - 1 or 'measurement' not in data[ch]: - end += 1 / time_scale - for t_, delta in ref.changes(start, end, time_scale=time_scale): - conf = dict(linestyle='--', linewidth=1.5, color='black') - a.axvline(t_, **conf) - b.axvline(t_, **conf) - msg = u"\u27F2 " + phase_str(delta) - a.text(t_-t[-1]*8e-3, max_amp*1.1, msg, ha='right', - fontsize=14, bbox=ph_box) - - if 'measurement' in data[ch]: - msg = f"Basis: {data[ch]['measurement']}" - b.text(t[-1]*1.025, det_top, msg, ha='center', va='center', - fontsize=14, color='white', rotation=90) - a.axvspan(t[-1], t_max, color='midnightblue', alpha=1) - b.axvspan(t[-1], t_max, color='midnightblue', alpha=1) - a.axhline(0, xmax=0.95, linestyle='-', linewidth=0.5, - color='grey') - b.axhline(0, xmax=0.95, linestyle=':', linewidth=0.5, - color='grey') - else: - a.axhline(0, linestyle='-', linewidth=0.5, color='grey') - b.axhline(0, linestyle=':', linewidth=0.5, color='grey') - - plt.show() diff --git a/pulser/sequence.py b/pulser/sequence.py deleted file mode 100644 index 9661445ec..000000000 --- a/pulser/sequence.py +++ /dev/null @@ -1,413 +0,0 @@ -# Copyright 2020 Pulser Development Team -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import namedtuple -from collections.abc import Iterable -import copy -import warnings - -import numpy as np - -from pulser.devices import PasqalDevice -from pulser.pulse import Pulse -from pulser.seq_drawer import draw_sequence -from pulser.utils import validate_duration - -# Auxiliary class to store the information in the schedule -TimeSlot = namedtuple('TimeSlot', ['type', 'ti', 'tf', 'targets']) - - -class Sequence: - """A sequence of operations on a device. - - A sequence is composed by - - The device in which we want to implement it - - The device's channels that are used - - The schedule of operations on each channel - """ - def __init__(self, device): - if not isinstance(device, PasqalDevice): - raise TypeError("The Sequence's device has to be a PasqalDevice.") - self._device = device - self._channels = {} - self._schedule = {} - self._phase_ref = {} # The phase reference of each channel - self._taken_channels = [] # Stores the ids of selected channels - self._qids = set(self.qubit_info.keys()) # IDs of all qubits in device - self._last_used = {} # Last time each qubit was used, by basis - - @property - def qubit_info(self): - """Returns the dictionary with the qubit's IDs and positions.""" - return self._device.qubits - - @property - def declared_channels(self): - return dict(self._channels) - - @property - def available_channels(self): - return {id: ch for id, ch in self._device.channels.items() - if id not in self._taken_channels} - - def current_phase_ref(self, qubit, basis='digital'): - """Returns the current phase reference of a specific qubit in a basis. - - Args: - qubit (str): The id of the qubit whose phase shift is desired. - - Keyword args: - basis (str): The basis (i.e. electronic transition) the phase - reference is associated with. Must correspond to the basis of a - declared channel. - - """ - if qubit not in self._qids: - raise ValueError("'qubit' must be the id of a qubit declared in " - "this sequence's device.") - - if basis not in self._phase_ref: - raise ValueError("No declared channel targets the given 'basis'.") - - return self._phase_ref[basis][qubit].last_phase - - def declare_channel(self, name, channel_id, initial_target=None): - """Declare a new channel to the Sequence. - - Args: - name (str): Unique name for the channel in the sequence. - channel_id (str): How the channel is identified in the device. - - Keyword Args: - initial_target (set, default=None): For 'Local' adressing channels - where a target has to be defined, it can be done when the - channel is first declared. If left as None, this will have to - be done manually as the first addition to a channel. - """ - - if name in self._channels: - raise ValueError("The given name is already in use.") - - if channel_id not in self._device.channels: - raise ValueError("No channel %s in the device." % channel_id) - - if channel_id in self._taken_channels: - raise ValueError("Channel %s has already been added." % channel_id) - - ch = self._device.channels[channel_id] - self._channels[name] = ch - self._taken_channels.append(channel_id) - self._schedule[name] = [] - - if ch.basis not in self._phase_ref: - self._phase_ref[ch.basis] = {q: PhaseTracker(0) - for q in self._qids} - self._last_used[ch.basis] = {q: 0 for q in self._qids} - - if ch.addressing == 'Global': - self._add_to_schedule(name, TimeSlot('target', -1, 0, self._qids)) - elif initial_target is not None: - self.target(initial_target, name) - - def add(self, pulse, channel, protocol='min-delay'): - """Add a pulse to a channel. - - Args: - pulse (Pulse): The pulse object to add to the channel. - channel (str): The channel's name provided when declared. - - Keyword Args: - protocol (default='min-delay'): Stipulates how to deal with - eventual conflicts with other channels, specifically in terms - of having to channels act on the same target simultaneously. - 'min-delay': Before adding the pulse, introduces the smallest - possible delay that avoids all exisiting conflicts. - 'no-delay': Adds the pulse to the channel, regardless of - existing conflicts. - 'wait-for-all': Before adding the pulse, adds a delay that - idles the channel until the end of the other channels' - latest pulse. - """ - - last = self._last(channel) - self._validate_pulse(pulse, channel) - - valid_protocols = ['min-delay', 'no-delay', 'wait-for-all'] - if protocol not in valid_protocols: - raise ValueError(f"Invalid protocol '{protocol}', only accepts " - "protocols: " + ", ".join(valid_protocols)) - - t0 = last.tf # Preliminary ti - basis = self._channels[channel].basis - phase_barriers = [self._phase_ref[basis][q].last_time - for q in last.targets] - current_max_t = max(t0, *phase_barriers) - if protocol != 'no-delay': - for ch, seq in self._schedule.items(): - if ch == channel: - continue - for op in self._schedule[ch][::-1]: - if op.tf <= current_max_t: - break - if not isinstance(op.type, Pulse): - continue - if op.targets & last.targets or protocol == 'wait-for-all': - current_max_t = op.tf - break - ti = current_max_t - tf = ti + pulse.duration - if ti > t0: - self.delay(ti-t0, channel) - - prs = {self._phase_ref[basis][q].last_phase for q in last.targets} - if len(prs) != 1: - raise ValueError("Cannot do a multiple-target pulse on qubits " - "with different phase references for the same " - "basis.") - else: - phase_ref = prs.pop() - - if phase_ref != 0: - # Has to copy to keep the original pulse intact - pulse = copy.deepcopy(pulse) - pulse.phase = (pulse.phase + phase_ref) % (2 * np.pi) - - self._add_to_schedule(channel, TimeSlot(pulse, ti, tf, last.targets)) - - for q in last.targets: - if self._last_used[basis][q] < tf: - self._last_used[basis][q] = tf - - if pulse.post_phase_shift: - self.phase_shift(pulse.post_phase_shift, *last.targets, - basis=basis) - - def target(self, qubits, channel): - """Changes the target qubit of a 'Local' channel. - - Args: - qubits (hashable, iterable): The new target for this channel. Must - correspond to a qubit ID in device or an iterable of qubit IDs, - when multi-qubit adressing is possible. - channel (str): The channel's name provided when declared. - """ - - if channel not in self._channels: - raise ValueError("Use the name of a declared channel.") - - if isinstance(qubits, Iterable) and not isinstance(qubits, str): - qs = set(qubits) - else: - qs = {qubits} - - if not qs.issubset(self._qids): - raise ValueError("The given qubits have to belong to the device.") - - if self._channels[channel].addressing != 'Local': - raise ValueError("Can only choose target of 'Local' channels.") - elif len(qs) > self._channels[channel].max_targets: - raise ValueError( - "This channel can target at most " - f"{self._channels[channel].max_targets} qubits at a time" - ) - - basis = self._channels[channel].basis - phase_refs = {self._phase_ref[basis][q].last_phase for q in qs} - if len(phase_refs) != 1: - raise ValueError("Cannot target multiple qubits with different " - "phase references for the same basis.") - - try: - last = self._last(channel) - if last.targets == qs: - warnings.warn("The provided qubits are already the target. " - "Skipping this target instruction.") - return - ti = last.tf - tf = ti + self._channels[channel].retarget_time - except ValueError: - ti = -1 - tf = 0 - - self._add_to_schedule(channel, TimeSlot('target', ti, tf, qs)) - - def delay(self, duration, channel): - """Idle a given choosen for a specific duration. - - Args: - duration (int): Time to delay (in ns). - channel (str): The channel's name provided when declared. - """ - last = self._last(channel) - ti = last.tf - tf = ti + validate_duration(duration) - self._add_to_schedule(channel, TimeSlot('delay', ti, tf, last.targets)) - - def measure(self, basis='ground-rydberg'): - """Measure in a valid basis. - - Args: - basis (str): Valid basis for measurement (consult the - 'supported_bases' attribute of the selected device for - the available options). - """ - available = self._device.supported_bases - if basis not in available: - raise ValueError(f"The basis '{basis}' is not supported by the " - "selected device. The available options are: " - + ", ".join(list(available))) - - if hasattr(self, '_measurement'): - raise SystemError("The sequence has already been measured.") - - self._measurement = basis - - def phase_shift(self, phi, *targets, basis='digital'): - """Shift the phase of a qubit's reference by 'phi', for a given basis. - - This is equivalent to an Rz(phi) gate (i.e. a rotation of the target - qubit's state by an angle phi around the z-axis of the Bloch sphere). - - Args: - phi (float): The intended phase shift (in rads). - targets: The ids of the qubits on which to apply the phase shift. - - Keyword Args: - basis(str): The basis (i.e. electronic transition) to associate - the phase shift to. Must correspond to the basis of a declared - channel. - """ - if phi % (2*np.pi) == 0: - warnings.warn("A phase shift of 0 is meaningless, " - "it will be ommited.") - return - if not set(targets) <= self._qids: - raise ValueError("All given targets have to be qubit ids declared" - " in this sequence's device.") - - if basis not in self._phase_ref: - raise ValueError("No declared channel targets the given 'basis'.") - - for q in targets: - t = self._last_used[basis][q] - new_phase = self._phase_ref[basis][q].last_phase + phi - self._phase_ref[basis][q][t] = new_phase - - def draw(self): - draw_sequence(self) - - def __str__(self): - full = "" - pulse_line = "t: {}->{} | {} | Targets: {}\n" - target_line = "t: {}->{} | Target: {} | Phase Reference: {}\n" - delay_line = "t: {}->{} | Delay \n" - # phase_line = "t: {} | Phase shift of: {:.3f} | Targets: {}\n" - for ch, seq in self._schedule.items(): - basis = self._channels[ch].basis - full += f"Channel: {ch}\n" - first_slot = True - for ts in seq: - if ts.type == 'delay': - full += delay_line.format(ts.ti, ts.tf) - continue - - tgts = list(ts.targets) - tgt_txt = ", ".join([str(t) for t in tgts]) - if isinstance(ts.type, Pulse): - full += pulse_line.format(ts.ti, ts.tf, ts.type, tgt_txt) - elif ts.type == 'target': - phase = self._phase_ref[basis][tgts[0]][ts.tf] - if first_slot: - full += (f"t: 0 | Initial targets: {tgt_txt} | " + - f"Phase Reference: {phase} \n") - first_slot = False - else: - full += target_line.format(ts.ti, ts.tf, tgt_txt, - phase) - - full += "\n" - - if hasattr(self, "_measurement"): - full += f"Measured in basis: {self._measurement}" - - return full - - def _add_to_schedule(self, channel, timeslot): - if hasattr(self, "_measurement"): - raise SystemError("The sequence has already been measured. " - "Nothing more can be added.") - self._schedule[channel].append(timeslot) - - def _last(self, channel): - """Shortcut to last element in the channel's schedule.""" - if channel not in self._schedule: - raise ValueError("Use the name of a declared channel.") - try: - return self._schedule[channel][-1] - except IndexError: - raise ValueError("The chosen channel has no target.") - - def _validate_pulse(self, pulse, channel): - if not isinstance(pulse, Pulse): - raise TypeError("pulse input must be of type Pulse, not of type " - "{}.".format(type(pulse))) - - ch = self._channels[channel] - if np.any(pulse.amplitude.samples > ch.max_amp): - raise ValueError("The pulse's amplitude goes over the maximum " - "value allowed for the chosen channel.") - if np.any(np.abs(pulse.detuning.samples) > ch.max_abs_detuning): - raise ValueError("The pulse's detuning values go out of the range " - "allowed for the chosen channel.") - - -class PhaseTracker: - """Tracks a phase reference over time.""" - - def __init__(self, initial_phase): - self._times = [0] - self._phases = [self._format(initial_phase)] - - @property - def last_time(self): - return self._times[-1] - - @property - def last_phase(self): - return self._phases[-1] - - def changes(self, ti, tf, time_scale=1): - """Changes in phases within ]ti, tf].""" - start, end = np.searchsorted( - self._times, (ti * time_scale, tf * time_scale), side='right') - for i in range(start, end): - change = self._phases[i] - self._phases[i-1] - yield (self._times[i] / time_scale, change) - - def _format(self, phi): - return phi % (2 * np.pi) - - def __setitem__(self, t, phi): - phase = self._format(phi) - if t in self._times: - ind = self._times.index(t) - self._phases[ind] = phase - else: - ind = np.searchsorted(self._times, t, side='right') - self._times.insert(ind, t) - self._phases.insert(ind, phase) - - def __getitem__(self, t): - ind = np.searchsorted(self._times, t, side='right') - 1 - return self._phases[ind] diff --git a/setup.py b/setup.py index 0737b8abe..346592e57 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( name="pulser", - version="0.0.1", + version="0.0.1a1", install_requires=[ "matplotlib", "numpy",