diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d8d7ebc..265933dc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,12 +37,15 @@ Changelog and it is now a keyword-only argument (@karalekas, gh-1071). - `PauliSum` objects are now hashable (@ecpeterson, gh-1073). - The code in `device.py` as been reorganized into a new `device` subdirectory - in a completely backwards-compatible fashion (@karalekas, gh-1066). + in a completely backwards-compatible fashion (@karalekas, gh-1066, gh-1094). - `PauliTerm` and `PauliSum` now have `__repr__` methods (@karalekas, gh-1080). - The experiment-schema-related code in `operator_estimation.py` has been moved - into a new `experiment` subdirectory (@karalekas, gh-1084). + into a new `experiment` subdirectory (@karalekas, gh-1084, gh-1094). - The keyword arguments to `measure_observables` are now captured as part of the `TomographyExperiment` class (@karalekas, gh-1090). +- Type hints have been added to the `pyquil.gates`, `pyquil.quilatom`, and + `pyquil.quilbase` modules (@appleby, gh-999). +- We now support Python 3.8 and it is tested in the CI (@karalekas, gh-1093). ### Bugfixes @@ -87,8 +90,6 @@ Changelog the `--runslow` option is specified for `pytest` (@kilimanjaro, gh-1001). - `PauliSum` objects can now be constructed from strings via `from_compact_str()` and `PauliTerm.from_compact_str()` supports multi-qubit strings (@jlbosse, gh-984). -- Type hints have been added to the `pyquil.gates`, `pyquil.quilatom`, and `pyquil.quilbase` - modules (@appleby gh-999). ### Bugfixes diff --git a/pyquil/device/__init__.py b/pyquil/device/__init__.py index 5842021df..efb256944 100644 --- a/pyquil/device/__init__.py +++ b/pyquil/device/__init__.py @@ -1,183 +1,4 @@ -############################################################################## -# Copyright 2016-2019 Rigetti Computing -# -# 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 warnings -from abc import ABC, abstractmethod -from typing import List, Tuple - -import networkx as nx -import numpy as np - -from pyquil.noise import NoiseModel - from pyquil.device._isa import (Edge, GateInfo, ISA, MeasureInfo, Qubit, THETA, gates_in_isa, isa_from_graph, isa_to_graph) +from pyquil.device._main import AbstractDevice, Device, NxDevice from pyquil.device._specs import EdgeSpecs, QubitSpecs, Specs, specs_from_graph - -PERFECT_FIDELITY = 1e0 -PERFECT_DURATION = 1 / 100 -DEFAULT_CZ_DURATION = 200 -DEFAULT_CZ_FIDELITY = 0.89 -DEFAULT_RX_DURATION = 50 -DEFAULT_RX_FIDELITY = 0.95 -DEFAULT_MEASURE_FIDELITY = 0.90 -DEFAULT_MEASURE_DURATION = 2000 - - -class AbstractDevice(ABC): - - @abstractmethod - def qubits(self): - """ - A sorted list of qubits in the device topology. - """ - - @abstractmethod - def qubit_topology(self) -> nx.Graph: - """ - The connectivity of qubits in this device given as a NetworkX graph. - """ - - @abstractmethod - def get_isa(self, oneq_type='Xhalves', twoq_type='CZ') -> ISA: - """ - Construct an ISA suitable for targeting by compilation. - - This will raise an exception if the requested ISA is not supported by the device. - - :param oneq_type: The family of one-qubit gates to target - :param twoq_type: The family of two-qubit gates to target - """ - - @abstractmethod - def get_specs(self) -> Specs: - """ - Construct a Specs object required by compilation - """ - - -class Device(AbstractDevice): - """ - A device (quantum chip) that can accept programs. - - Only devices that are online will actively be - accepting new programs. In addition to the ``self._raw`` attribute, two other attributes are - optionally constructed from the entries in ``self._raw`` -- ``isa`` and ``noise_model`` -- which - should conform to the dictionary format required by the ``.from_dict()`` methods for ``ISA`` - and ``NoiseModel``, respectively. - - :ivar dict _raw: Raw JSON response from the server with additional information about the device. - :ivar ISA isa: The instruction set architecture (ISA) for the device. - :ivar NoiseModel noise_model: The noise model for the device. - """ - - def __init__(self, name, raw): - """ - :param name: name of the device - :param raw: raw JSON response from the server with additional information about this device. - """ - self.name = name - self._raw = raw - - # TODO: Introduce distinction between supported ISAs and target ISA - self._isa = ISA.from_dict(raw['isa']) if 'isa' in raw and raw['isa'] != {} else None - self.specs = Specs.from_dict(raw['specs']) if raw.get('specs') else None - self.noise_model = NoiseModel.from_dict(raw['noise_model']) \ - if raw.get('noise_model') else None - - @property - def isa(self): - warnings.warn("Accessing the static ISA is deprecated. Use `get_isa`", DeprecationWarning) - return self._isa - - def qubits(self): - return sorted(q.id for q in self._isa.qubits if not q.dead) - - def qubit_topology(self) -> nx.Graph: - """ - The connectivity of qubits in this device given as a NetworkX graph. - """ - return isa_to_graph(self._isa) - - def get_specs(self): - return self.specs - - def get_isa(self, oneq_type=None, twoq_type=None) -> ISA: - """ - Construct an ISA suitable for targeting by compilation. - - This will raise an exception if the requested ISA is not supported by the device. - """ - if oneq_type is not None or twoq_type is not None: - raise ValueError("oneq_type and twoq_type are both fatally deprecated. If you want to " - "make an ISA with custom gate types, you'll have to do it by hand.") - - qubits = [Qubit(id=q.id, type=None, dead=q.dead, gates=[ - MeasureInfo(operator="MEASURE", qubit=q.id, target="_", - fidelity=self.specs.fROs()[q.id] or DEFAULT_MEASURE_FIDELITY, - duration=DEFAULT_MEASURE_DURATION), - MeasureInfo(operator="MEASURE", qubit=q.id, target=None, - fidelity=self.specs.fROs()[q.id] or DEFAULT_MEASURE_FIDELITY, - duration=DEFAULT_MEASURE_DURATION), - GateInfo(operator="RZ", parameters=["_"], arguments=[q.id], - duration=PERFECT_DURATION, fidelity=PERFECT_FIDELITY), - GateInfo(operator="RX", parameters=[0.0], arguments=[q.id], - duration=DEFAULT_RX_DURATION, fidelity=PERFECT_FIDELITY)] + [ - GateInfo(operator="RX", parameters=[param], arguments=[q.id], - duration=DEFAULT_RX_DURATION, - fidelity=self.specs.f1QRBs()[q.id] or DEFAULT_RX_FIDELITY) - for param in [np.pi, -np.pi, np.pi / 2, -np.pi / 2]]) - for q in self._isa.qubits] - edges = [Edge(targets=e.targets, type=None, dead=e.dead, gates=[ - GateInfo(operator="CZ", parameters=[], arguments=["_", "_"], - duration=DEFAULT_CZ_DURATION, - fidelity=self.specs.fCZs()[tuple(e.targets)] or DEFAULT_CZ_FIDELITY)]) - for e in self._isa.edges] - return ISA(qubits, edges) - - def __str__(self): - return ''.format(self.name) - - def __repr__(self): - return str(self) - - -class NxDevice(AbstractDevice): - """A shim over the AbstractDevice API backed by a NetworkX graph. - - A ``Device`` holds information about the physical device. - Specifically, you might want to know about connectivity, available gates, performance specs, - and more. This class implements the AbstractDevice API for devices not available via - ``get_devices()``. Instead, the user is responsible for constructing a NetworkX - graph which represents a chip topology. - """ - - def __init__(self, topology: nx.Graph) -> None: - self.topology = topology - - def qubit_topology(self): - return self.topology - - def get_isa(self, oneq_type='Xhalves', twoq_type='CZ'): - return isa_from_graph(self.topology, oneq_type=oneq_type, twoq_type=twoq_type) - - def get_specs(self): - return specs_from_graph(self.topology) - - def qubits(self) -> List[int]: - return sorted(self.topology.nodes) - - def edges(self) -> List[Tuple[int, int]]: - return sorted(tuple(sorted(pair)) for pair in self.topology.edges) # type: ignore diff --git a/pyquil/device/_main.py b/pyquil/device/_main.py new file mode 100644 index 000000000..8ab922b17 --- /dev/null +++ b/pyquil/device/_main.py @@ -0,0 +1,182 @@ +############################################################################## +# Copyright 2016-2019 Rigetti Computing +# +# 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 warnings +from abc import ABC, abstractmethod +from typing import List, Tuple + +import networkx as nx +import numpy as np + +from pyquil.device._isa import (Edge, GateInfo, ISA, MeasureInfo, Qubit, isa_from_graph, + isa_to_graph) +from pyquil.device._specs import Specs, specs_from_graph +from pyquil.noise import NoiseModel + +PERFECT_FIDELITY = 1e0 +PERFECT_DURATION = 1 / 100 +DEFAULT_CZ_DURATION = 200 +DEFAULT_CZ_FIDELITY = 0.89 +DEFAULT_RX_DURATION = 50 +DEFAULT_RX_FIDELITY = 0.95 +DEFAULT_MEASURE_FIDELITY = 0.90 +DEFAULT_MEASURE_DURATION = 2000 + + +class AbstractDevice(ABC): + + @abstractmethod + def qubits(self): + """ + A sorted list of qubits in the device topology. + """ + + @abstractmethod + def qubit_topology(self) -> nx.Graph: + """ + The connectivity of qubits in this device given as a NetworkX graph. + """ + + @abstractmethod + def get_isa(self, oneq_type='Xhalves', twoq_type='CZ') -> ISA: + """ + Construct an ISA suitable for targeting by compilation. + + This will raise an exception if the requested ISA is not supported by the device. + + :param oneq_type: The family of one-qubit gates to target + :param twoq_type: The family of two-qubit gates to target + """ + + @abstractmethod + def get_specs(self) -> Specs: + """ + Construct a Specs object required by compilation + """ + + +class Device(AbstractDevice): + """ + A device (quantum chip) that can accept programs. + + Only devices that are online will actively be + accepting new programs. In addition to the ``self._raw`` attribute, two other attributes are + optionally constructed from the entries in ``self._raw`` -- ``isa`` and ``noise_model`` -- which + should conform to the dictionary format required by the ``.from_dict()`` methods for ``ISA`` + and ``NoiseModel``, respectively. + + :ivar dict _raw: Raw JSON response from the server with additional information about the device. + :ivar ISA isa: The instruction set architecture (ISA) for the device. + :ivar NoiseModel noise_model: The noise model for the device. + """ + + def __init__(self, name, raw): + """ + :param name: name of the device + :param raw: raw JSON response from the server with additional information about this device. + """ + self.name = name + self._raw = raw + + # TODO: Introduce distinction between supported ISAs and target ISA + self._isa = ISA.from_dict(raw['isa']) if 'isa' in raw and raw['isa'] != {} else None + self.specs = Specs.from_dict(raw['specs']) if raw.get('specs') else None + self.noise_model = NoiseModel.from_dict(raw['noise_model']) \ + if raw.get('noise_model') else None + + @property + def isa(self): + warnings.warn("Accessing the static ISA is deprecated. Use `get_isa`", DeprecationWarning) + return self._isa + + def qubits(self): + return sorted(q.id for q in self._isa.qubits if not q.dead) + + def qubit_topology(self) -> nx.Graph: + """ + The connectivity of qubits in this device given as a NetworkX graph. + """ + return isa_to_graph(self._isa) + + def get_specs(self): + return self.specs + + def get_isa(self, oneq_type=None, twoq_type=None) -> ISA: + """ + Construct an ISA suitable for targeting by compilation. + + This will raise an exception if the requested ISA is not supported by the device. + """ + if oneq_type is not None or twoq_type is not None: + raise ValueError("oneq_type and twoq_type are both fatally deprecated. If you want to " + "make an ISA with custom gate types, you'll have to do it by hand.") + + qubits = [Qubit(id=q.id, type=None, dead=q.dead, gates=[ + MeasureInfo(operator="MEASURE", qubit=q.id, target="_", + fidelity=self.specs.fROs()[q.id] or DEFAULT_MEASURE_FIDELITY, + duration=DEFAULT_MEASURE_DURATION), + MeasureInfo(operator="MEASURE", qubit=q.id, target=None, + fidelity=self.specs.fROs()[q.id] or DEFAULT_MEASURE_FIDELITY, + duration=DEFAULT_MEASURE_DURATION), + GateInfo(operator="RZ", parameters=["_"], arguments=[q.id], + duration=PERFECT_DURATION, fidelity=PERFECT_FIDELITY), + GateInfo(operator="RX", parameters=[0.0], arguments=[q.id], + duration=DEFAULT_RX_DURATION, fidelity=PERFECT_FIDELITY)] + [ + GateInfo(operator="RX", parameters=[param], arguments=[q.id], + duration=DEFAULT_RX_DURATION, + fidelity=self.specs.f1QRBs()[q.id] or DEFAULT_RX_FIDELITY) + for param in [np.pi, -np.pi, np.pi / 2, -np.pi / 2]]) + for q in self._isa.qubits] + edges = [Edge(targets=e.targets, type=None, dead=e.dead, gates=[ + GateInfo(operator="CZ", parameters=[], arguments=["_", "_"], + duration=DEFAULT_CZ_DURATION, + fidelity=self.specs.fCZs()[tuple(e.targets)] or DEFAULT_CZ_FIDELITY)]) + for e in self._isa.edges] + return ISA(qubits, edges) + + def __str__(self): + return ''.format(self.name) + + def __repr__(self): + return str(self) + + +class NxDevice(AbstractDevice): + """A shim over the AbstractDevice API backed by a NetworkX graph. + + A ``Device`` holds information about the physical device. + Specifically, you might want to know about connectivity, available gates, performance specs, + and more. This class implements the AbstractDevice API for devices not available via + ``get_devices()``. Instead, the user is responsible for constructing a NetworkX + graph which represents a chip topology. + """ + + def __init__(self, topology: nx.Graph) -> None: + self.topology = topology + + def qubit_topology(self): + return self.topology + + def get_isa(self, oneq_type='Xhalves', twoq_type='CZ'): + return isa_from_graph(self.topology, oneq_type=oneq_type, twoq_type=twoq_type) + + def get_specs(self): + return specs_from_graph(self.topology) + + def qubits(self) -> List[int]: + return sorted(self.topology.nodes) + + def edges(self) -> List[Tuple[int, int]]: + return sorted(tuple(sorted(pair)) for pair in self.topology.edges) # type: ignore diff --git a/pyquil/experiment/__init__.py b/pyquil/experiment/__init__.py index 3adca9ca3..794db553b 100644 --- a/pyquil/experiment/__init__.py +++ b/pyquil/experiment/__init__.py @@ -1,297 +1,6 @@ -############################################################################## -# Copyright 2016-2019 Rigetti Computing -# -# 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. -############################################################################## -""" -Schema definition of a TomographyExperiment, which is a collection of ExperimentSetting -objects and a main program body (or ansatz). This schema is widely useful for defining -and executing many common types of algorithms / applications, including state and process -tomography, and the variational quantum eigensolver. -""" -import json -import logging -import warnings -from json import JSONEncoder -from enum import IntEnum -from typing import List, Union, Optional - -from pyquil import Program +from pyquil.experiment._main import (OperatorEncoder, SymmetrizationLevel, TomographyExperiment, + read_json, to_json) from pyquil.experiment._result import ExperimentResult from pyquil.experiment._setting import (_OneQState, _pauli_to_product_state, ExperimentSetting, SIC0, SIC1, SIC2, SIC3, TensorProductState, minusX, minusY, minusZ, plusX, plusY, plusZ, zeros_state) -from pyquil.quilbase import DefPermutationGate, Reset - - -log = logging.getLogger(__name__) - - -class SymmetrizationLevel(IntEnum): - EXHAUSTIVE = -1 - NONE = 0 - OA_STRENGTH_1 = 1 - OA_STRENGTH_2 = 2 - OA_STRENGTH_3 = 3 - - -def _abbrev_program(program: Program, max_len=10): - """Create an abbreviated string representation of a Program. - - This will join all instructions onto a single line joined by '; '. If the number of - instructions exceeds ``max_len``, some will be excluded from the string representation. - """ - program_lines = program.out().splitlines() - if max_len is not None and len(program_lines) > max_len: - first_n = max_len // 2 - last_n = max_len - first_n - excluded = len(program_lines) - max_len - program_lines = (program_lines[:first_n] + [f'... {excluded} instrs not shown ...'] - + program_lines[-last_n:]) - - return ' ' + '\n '.join(program_lines) - - -def _remove_reset_from_program(program: Program) -> Program: - """ - Trim the RESET from a program because in measure_observables it is re-added. - - :param program: Program to remove RESET(s) from. - :return: Trimmed Program. - """ - definitions = [gate for gate in program.defined_gates] - - p = Program([inst for inst in program if not isinstance(inst, Reset)]) - - for definition in definitions: - if isinstance(definition, DefPermutationGate): - p.inst(DefPermutationGate(definition.name, list(definition.permutation))) - else: - p.defgate(definition.name, definition.matrix, definition.parameters) - return p - - -class TomographyExperiment: - """ - A tomography-like experiment. - - Many near-term quantum algorithms involve: - - - some limited state preparation - - enacting a quantum process (like in tomography) or preparing a variational ansatz state - (like in VQE) - - measuring observables of the state. - - Where we typically use a large number of (state_prep, measure) pairs but keep the ansatz - program consistent. This class stores the ansatz program as a :py:class:`~pyquil.Program` - and maintains a list of :py:class:`ExperimentSetting` objects which each represent a - (state_prep, measure) pair. - - Settings diagonalized by a shared tensor product basis (TPB) can (optionally) be estimated - simultaneously. Therefore, this class is backed by a list of list of ExperimentSettings. - Settings sharing an inner list will be estimated simultaneously. If you don't want this, - provide a list of length-1-lists. As a convenience, if you pass a 1D list to the constructor - will expand it to a list of length-1-lists. - - This class will not group settings for you. Please see :py:func:`group_experiments` for - a function that will automatically process a TomographyExperiment to group Experiments sharing - a TPB. - - :ivar settings: The collection of ExperimentSetting objects that define this experiment. - :ivar program: The main program body of this experiment. Also determines the ``shots`` - and ``reset`` instance variables. The ``shots`` instance variable is the number of - shots to take per ExperimentSetting. The ``reset`` instance variable is whether to - actively reset qubits instead of waiting several times the coherence length for qubits - to decay to ``|0>`` naturally. Setting this to True is much faster but there is a ~1% - error per qubit in the reset operation. Thermal noise from "traditional" reset is not - routinely characterized but is of the same order. - :ivar symmetrization: the level of readout symmetrization to perform for the estimation - and optional calibration of each observable. The following integer levels, encapsulated in - the ``SymmetrizationLevel`` integer enum, are currently supported: - - * -1 -- exhaustive symmetrization uses every possible combination of flips - * 0 -- no symmetrization - * 1 -- symmetrization using an orthogonal array (OA) with strength 1 - * 2 -- symmetrization using an orthogonal array (OA) with strength 2 - * 3 -- symmetrization using an orthogonal array (OA) with strength 3 - - Note that (default) exhaustive symmetrization requires a number of QPU calls exponential in - the number of qubits in the union of the support of the observables in any group of settings - in ``tomo_experiment``; the number of shots may need to be increased to accommodate this. - see :func:`run_symmetrized_readout` in api._quantum_computer for more information. - """ - - def __init__(self, - settings: Union[List[ExperimentSetting], List[List[ExperimentSetting]]], - program: Program, - qubits: Optional[List[int]] = None, - *, - symmetrization: int = SymmetrizationLevel.EXHAUSTIVE): - if len(settings) == 0: - settings = [] - else: - if isinstance(settings[0], ExperimentSetting): - # convenience wrapping in lists of length 1 - settings = [[expt] for expt in settings] - - self._settings = settings # type: List[List[ExperimentSetting]] - self.program = program - if qubits is not None: - warnings.warn("The 'qubits' parameter has been deprecated and will be removed" - "in a future release of pyquil") - self.qubits = qubits - self.symmetrization = SymmetrizationLevel(symmetrization) - self.shots = self.program.num_shots - - if 'RESET' in self.program.out(): - self.reset = True - self.program = _remove_reset_from_program(self.program) - else: - self.reset = False - - def __len__(self): - return len(self._settings) - - def __getitem__(self, item): - return self._settings[item] - - def __setitem__(self, key, value): - self._settings[key] = value - - def __delitem__(self, key): - self._settings.__delitem__(key) - - def __iter__(self): - yield from self._settings - - def __reversed__(self): - yield from reversed(self._settings) - - def __contains__(self, item): - return item in self._settings - - def append(self, expts): - if not isinstance(expts, list): - expts = [expts] - return self._settings.append(expts) - - def count(self, expt): - return self._settings.count(expt) - - def index(self, expt, start=None, stop=None): - return self._settings.index(expt, start, stop) - - def extend(self, expts): - return self._settings.extend(expts) - - def insert(self, index, expt): - return self._settings.insert(index, expt) - - def pop(self, index=None): - return self._settings.pop(index) - - def remove(self, expt): - return self._settings.remove(expt) - - def reverse(self): - return self._settings.reverse() - - def sort(self, key=None, reverse=False): - return self._settings.sort(key, reverse) - - def setting_strings(self): - yield from ('{i}: {st_str}'.format(i=i, st_str=', '.join(str(setting) - for setting in settings)) - for i, settings in enumerate(self._settings)) - - def settings_string(self, abbrev_after=None): - setting_strs = list(self.setting_strings()) - if abbrev_after is not None and len(setting_strs) > abbrev_after: - first_n = abbrev_after // 2 - last_n = abbrev_after - first_n - excluded = len(setting_strs) - abbrev_after - setting_strs = (setting_strs[:first_n] + [f'... {excluded} settings not shown ...'] - + setting_strs[-last_n:]) - return ' ' + '\n '.join(setting_strs) - - def __repr__(self): - string = f'shots: {self.shots}\n' - if self.reset: - string += f'active reset: enabled\n' - else: - string += f'active reset: disabled\n' - string += f'symmetrization: {self.symmetrization} ({self.symmetrization.name.lower()})\n' - string += f'program:\n{_abbrev_program(self.program)}\n' - string += f'settings:\n{self.settings_string(abbrev_after=20)}' - return string - - def serializable(self): - return { - 'type': 'TomographyExperiment', - 'settings': self._settings, - 'program': self.program.out(), - 'symmetrization': self.symmetrization, - 'shots': self.shots, - 'reset': self.reset - } - - def __eq__(self, other): - if not isinstance(other, TomographyExperiment): - return False - return self.serializable() == other.serializable() - - -class OperatorEncoder(JSONEncoder): - def default(self, o): - if isinstance(o, ExperimentSetting): - return o.serializable() - if isinstance(o, TomographyExperiment): - return o.serializable() - if isinstance(o, ExperimentResult): - return o.serializable() - return o - - -def to_json(fn, obj): - """ - Convenience method to save pyquil.experiment objects as a JSON file. - - See :py:func:`read_json`. - """ - with open(fn, 'w') as f: - json.dump(obj, f, cls=OperatorEncoder, indent=2, ensure_ascii=False) - return fn - - -def _operator_object_hook(obj): - if 'type' in obj and obj['type'] == 'TomographyExperiment': - # I bet this doesn't work for grouped experiment settings - settings = [[ExperimentSetting.from_str(s) for s in stt] for stt in obj['settings']] - p = Program(obj['program']) - p.wrap_in_numshots_loop(obj['shots']) - ex = TomographyExperiment(settings=settings, - program=p, - symmetrization=obj['symmetrization']) - ex.reset = obj['reset'] - return ex - return obj - - -def read_json(fn): - """ - Convenience method to read pyquil.experiment objects from a JSON file. - - See :py:func:`to_json`. - """ - with open(fn) as f: - return json.load(f, object_hook=_operator_object_hook) diff --git a/pyquil/experiment/_main.py b/pyquil/experiment/_main.py new file mode 100644 index 000000000..e901d5926 --- /dev/null +++ b/pyquil/experiment/_main.py @@ -0,0 +1,295 @@ +############################################################################## +# Copyright 2016-2019 Rigetti Computing +# +# 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. +############################################################################## +""" +Schema definition of a TomographyExperiment, which is a collection of ExperimentSetting +objects and a main program body (or ansatz). This schema is widely useful for defining +and executing many common types of algorithms / applications, including state and process +tomography, and the variational quantum eigensolver. +""" +import json +import logging +import warnings +from json import JSONEncoder +from enum import IntEnum +from typing import List, Union, Optional + +from pyquil import Program +from pyquil.experiment._result import ExperimentResult +from pyquil.experiment._setting import ExperimentSetting +from pyquil.quilbase import DefPermutationGate, Reset + + +log = logging.getLogger(__name__) + + +class SymmetrizationLevel(IntEnum): + EXHAUSTIVE = -1 + NONE = 0 + OA_STRENGTH_1 = 1 + OA_STRENGTH_2 = 2 + OA_STRENGTH_3 = 3 + + +def _abbrev_program(program: Program, max_len=10): + """Create an abbreviated string representation of a Program. + + This will join all instructions onto a single line joined by '; '. If the number of + instructions exceeds ``max_len``, some will be excluded from the string representation. + """ + program_lines = program.out().splitlines() + if max_len is not None and len(program_lines) > max_len: + first_n = max_len // 2 + last_n = max_len - first_n + excluded = len(program_lines) - max_len + program_lines = (program_lines[:first_n] + [f'... {excluded} instrs not shown ...'] + + program_lines[-last_n:]) + + return ' ' + '\n '.join(program_lines) + + +def _remove_reset_from_program(program: Program) -> Program: + """ + Trim the RESET from a program because in measure_observables it is re-added. + + :param program: Program to remove RESET(s) from. + :return: Trimmed Program. + """ + definitions = [gate for gate in program.defined_gates] + + p = Program([inst for inst in program if not isinstance(inst, Reset)]) + + for definition in definitions: + if isinstance(definition, DefPermutationGate): + p.inst(DefPermutationGate(definition.name, list(definition.permutation))) + else: + p.defgate(definition.name, definition.matrix, definition.parameters) + return p + + +class TomographyExperiment: + """ + A tomography-like experiment. + + Many near-term quantum algorithms involve: + + - some limited state preparation + - enacting a quantum process (like in tomography) or preparing a variational ansatz state + (like in VQE) + - measuring observables of the state. + + Where we typically use a large number of (state_prep, measure) pairs but keep the ansatz + program consistent. This class stores the ansatz program as a :py:class:`~pyquil.Program` + and maintains a list of :py:class:`ExperimentSetting` objects which each represent a + (state_prep, measure) pair. + + Settings diagonalized by a shared tensor product basis (TPB) can (optionally) be estimated + simultaneously. Therefore, this class is backed by a list of list of ExperimentSettings. + Settings sharing an inner list will be estimated simultaneously. If you don't want this, + provide a list of length-1-lists. As a convenience, if you pass a 1D list to the constructor + will expand it to a list of length-1-lists. + + This class will not group settings for you. Please see :py:func:`group_experiments` for + a function that will automatically process a TomographyExperiment to group Experiments sharing + a TPB. + + :ivar settings: The collection of ExperimentSetting objects that define this experiment. + :ivar program: The main program body of this experiment. Also determines the ``shots`` + and ``reset`` instance variables. The ``shots`` instance variable is the number of + shots to take per ExperimentSetting. The ``reset`` instance variable is whether to + actively reset qubits instead of waiting several times the coherence length for qubits + to decay to ``|0>`` naturally. Setting this to True is much faster but there is a ~1% + error per qubit in the reset operation. Thermal noise from "traditional" reset is not + routinely characterized but is of the same order. + :ivar symmetrization: the level of readout symmetrization to perform for the estimation + and optional calibration of each observable. The following integer levels, encapsulated in + the ``SymmetrizationLevel`` integer enum, are currently supported: + + * -1 -- exhaustive symmetrization uses every possible combination of flips + * 0 -- no symmetrization + * 1 -- symmetrization using an orthogonal array (OA) with strength 1 + * 2 -- symmetrization using an orthogonal array (OA) with strength 2 + * 3 -- symmetrization using an orthogonal array (OA) with strength 3 + + Note that (default) exhaustive symmetrization requires a number of QPU calls exponential in + the number of qubits in the union of the support of the observables in any group of settings + in ``tomo_experiment``; the number of shots may need to be increased to accommodate this. + see :func:`run_symmetrized_readout` in api._quantum_computer for more information. + """ + + def __init__(self, + settings: Union[List[ExperimentSetting], List[List[ExperimentSetting]]], + program: Program, + qubits: Optional[List[int]] = None, + *, + symmetrization: int = SymmetrizationLevel.EXHAUSTIVE): + if len(settings) == 0: + settings = [] + else: + if isinstance(settings[0], ExperimentSetting): + # convenience wrapping in lists of length 1 + settings = [[expt] for expt in settings] + + self._settings = settings # type: List[List[ExperimentSetting]] + self.program = program + if qubits is not None: + warnings.warn("The 'qubits' parameter has been deprecated and will be removed" + "in a future release of pyquil") + self.qubits = qubits + self.symmetrization = SymmetrizationLevel(symmetrization) + self.shots = self.program.num_shots + + if 'RESET' in self.program.out(): + self.reset = True + self.program = _remove_reset_from_program(self.program) + else: + self.reset = False + + def __len__(self): + return len(self._settings) + + def __getitem__(self, item): + return self._settings[item] + + def __setitem__(self, key, value): + self._settings[key] = value + + def __delitem__(self, key): + self._settings.__delitem__(key) + + def __iter__(self): + yield from self._settings + + def __reversed__(self): + yield from reversed(self._settings) + + def __contains__(self, item): + return item in self._settings + + def append(self, expts): + if not isinstance(expts, list): + expts = [expts] + return self._settings.append(expts) + + def count(self, expt): + return self._settings.count(expt) + + def index(self, expt, start=None, stop=None): + return self._settings.index(expt, start, stop) + + def extend(self, expts): + return self._settings.extend(expts) + + def insert(self, index, expt): + return self._settings.insert(index, expt) + + def pop(self, index=None): + return self._settings.pop(index) + + def remove(self, expt): + return self._settings.remove(expt) + + def reverse(self): + return self._settings.reverse() + + def sort(self, key=None, reverse=False): + return self._settings.sort(key, reverse) + + def setting_strings(self): + yield from ('{i}: {st_str}'.format(i=i, st_str=', '.join(str(setting) + for setting in settings)) + for i, settings in enumerate(self._settings)) + + def settings_string(self, abbrev_after=None): + setting_strs = list(self.setting_strings()) + if abbrev_after is not None and len(setting_strs) > abbrev_after: + first_n = abbrev_after // 2 + last_n = abbrev_after - first_n + excluded = len(setting_strs) - abbrev_after + setting_strs = (setting_strs[:first_n] + [f'... {excluded} settings not shown ...'] + + setting_strs[-last_n:]) + return ' ' + '\n '.join(setting_strs) + + def __repr__(self): + string = f'shots: {self.shots}\n' + if self.reset: + string += f'active reset: enabled\n' + else: + string += f'active reset: disabled\n' + string += f'symmetrization: {self.symmetrization} ({self.symmetrization.name.lower()})\n' + string += f'program:\n{_abbrev_program(self.program)}\n' + string += f'settings:\n{self.settings_string(abbrev_after=20)}' + return string + + def serializable(self): + return { + 'type': 'TomographyExperiment', + 'settings': self._settings, + 'program': self.program.out(), + 'symmetrization': self.symmetrization, + 'shots': self.shots, + 'reset': self.reset + } + + def __eq__(self, other): + if not isinstance(other, TomographyExperiment): + return False + return self.serializable() == other.serializable() + + +class OperatorEncoder(JSONEncoder): + def default(self, o): + if isinstance(o, ExperimentSetting): + return o.serializable() + if isinstance(o, TomographyExperiment): + return o.serializable() + if isinstance(o, ExperimentResult): + return o.serializable() + return o + + +def to_json(fn, obj): + """ + Convenience method to save pyquil.experiment objects as a JSON file. + + See :py:func:`read_json`. + """ + with open(fn, 'w') as f: + json.dump(obj, f, cls=OperatorEncoder, indent=2, ensure_ascii=False) + return fn + + +def _operator_object_hook(obj): + if 'type' in obj and obj['type'] == 'TomographyExperiment': + # I bet this doesn't work for grouped experiment settings + settings = [[ExperimentSetting.from_str(s) for s in stt] for stt in obj['settings']] + p = Program(obj['program']) + p.wrap_in_numshots_loop(obj['shots']) + ex = TomographyExperiment(settings=settings, + program=p, + symmetrization=obj['symmetrization']) + ex.reset = obj['reset'] + return ex + return obj + + +def read_json(fn): + """ + Convenience method to read pyquil.experiment objects from a JSON file. + + See :py:func:`to_json`. + """ + with open(fn) as f: + return json.load(f, object_hook=_operator_object_hook) diff --git a/pyquil/experiment/tests/test_experiment.py b/pyquil/experiment/tests/test_experiment.py index cbf238019..c727d9230 100644 --- a/pyquil/experiment/tests/test_experiment.py +++ b/pyquil/experiment/tests/test_experiment.py @@ -5,10 +5,10 @@ import pytest from pyquil import Program -from pyquil.experiment import (_remove_reset_from_program, ExperimentSetting, SIC0, SIC1, SIC2, - SIC3, TensorProductState, TomographyExperiment, plusX, minusX, - plusY, minusY, plusZ, minusZ, read_json, to_json, zeros_state, - ExperimentResult) +from pyquil.experiment._main import _remove_reset_from_program +from pyquil.experiment import (ExperimentSetting, SIC0, SIC1, SIC2, SIC3, TensorProductState, + TomographyExperiment, plusX, minusX, plusY, minusY, plusZ, minusZ, + read_json, to_json, zeros_state, ExperimentResult) from pyquil.gates import RESET, X, Y from pyquil.paulis import sI, sX, sY, sZ diff --git a/pyquil/latex/__init__.py b/pyquil/latex/__init__.py index 519943210..56cd7ea2a 100644 --- a/pyquil/latex/__init__.py +++ b/pyquil/latex/__init__.py @@ -1,3 +1,3 @@ -from pyquil.latex.latex_generation import to_latex +from pyquil.latex._main import to_latex from pyquil.latex._diagram import DiagramSettings from pyquil.latex._ipython import display diff --git a/pyquil/latex/_diagram.py b/pyquil/latex/_diagram.py index 6bd79a85a..344ad2ad4 100644 --- a/pyquil/latex/_diagram.py +++ b/pyquil/latex/_diagram.py @@ -79,82 +79,6 @@ class DiagramSettings: """ -# Overview of LaTeX generation. -# -# The main entry point is the `to_latex` function. Here are some high -# points of the generation procedure: -# -# - The most basic building block are the TikZ operators, which are constructed -# by the functions below (e.g. TIKZ_CONTROL, TIKZ_NOP, TIKZ_MEASURE). -# - TikZ operators are maintained by a DiagramState object, with roughly each -# qubit line in a diagram represented as a list of TikZ operators on the DiagramState. -# - The DiagramBuilder is the actual driver. This traverses a Program and, for -# each instruction, performs a suitable manipulation of the DiagramState. At -# the end of this, the DiagramState is traversed and raw LaTeX is emitted. -# - Most options are specified by DiagramSettings. One exception is this: it is possible -# to request that a certain subset of the program is rendered as a group (and colored -# as such). This is specified by a new pragma in the Program source: -# -# PRAGMA LATEX_GATE_GROUP ? -# ... -# PRAGMA END_LATEX_GATE_GROUP -# -# The is optional, and will be used to label the group. Nested gate -# groups are currently not supported. - - -def header(): - """ - Writes the LaTeX header using the settings file. - - The header includes all packages and defines all tikz styles. - - :return: Header of the LaTeX document. - :rtype: string - """ - packages = (r"\documentclass[convert={density=300,outext=.png}]{standalone}", - r"\usepackage[margin=1in]{geometry}", - r"\usepackage{tikz}", - r"\usetikzlibrary{quantikz}") - - init = (r"\begin{document}", - r"\begin{tikzcd}") - - return "\n".join(("\n".join(packages), "\n".join(init))) - - -def footer(): - """ - Return the footer of the LaTeX document. - - :return: LaTeX document footer. - :rtype: string - """ - return "\\end{tikzcd}\n\\end{document}" - - -def body(circuit: Program, settings: DiagramSettings): - """ - Return the body of the LaTeX document, including the entire circuit in - TikZ format. - - :param Program circuit: The circuit to be drawn, represented as a pyquil program. - :param DiagramSettings settings: Options controlling rendering and layout. - - :return: LaTeX string to draw the entire circuit. - :rtype: string - """ - - diagram = DiagramBuilder(circuit, settings).build() - - # flush lines - quantikz_out = [] - for qubit in diagram.qubits: - quantikz_out.append(" & ".join(diagram.lines[qubit])) - - return " \\\\\n".join(quantikz_out) - - # Constants @@ -165,10 +89,12 @@ def body(circuit: Program, settings: DiagramSettings): Wait, Reset, ResetQubit, JumpConditional, JumpWhen, JumpUnless, Jump, UnaryClassicalInstruction, LogicalBinaryOp, ArithmeticBinaryOp, - ClassicalMove, ClassicalExchange, ClassicalConvert, ClassicalLoad, ClassicalStore, ClassicalComparison, + ClassicalMove, ClassicalExchange, ClassicalConvert, + ClassicalLoad, ClassicalStore, ClassicalComparison, RawInstr ) + # TikZ operators @@ -235,8 +161,10 @@ def TIKZ_GATE(name, size=1, params=None, dagger=False, settings=None): def TIKZ_GATE_GROUP(qubits, width, label): num_qubits = max(qubits) - min(qubits) + 1 - return "\\gategroup[{qubits},steps={width},style={{dashed, rounded corners,fill=blue!20, inner xsep=2pt}}, background]{{{label}}}".format( - qubits=num_qubits, width=width, label=label) + return "\\gategroup[{qubits},steps={width},style={{dashed, rounded corners," \ + "fill=blue!20, inner xsep=2pt}}, background]{{{label}}}".format(qubits=num_qubits, + width=width, + label=label) SOURCE_TARGET_OP = { @@ -247,14 +175,12 @@ def TIKZ_GATE_GROUP(qubits, width, label): } -# DiagramState - - class DiagramState: """ A representation of a circuit diagram. - This maintains an ordered list of qubits, and for each qubit a 'line': that is, a list of TikZ operators. + This maintains an ordered list of qubits, and for each qubit a 'line': that is, a list of + TikZ operators. """ def __init__(self, qubits): self.qubits = qubits @@ -296,7 +222,8 @@ def append_diagram(self, diagram, group=None): self.extend_lines_to_common_edge(grouped_qubits) # record info for later (optional) group placement - # the group is marked with a rectangle. we compute the upper-left corner and the width of the rectangle + # the group is marked with a rectangle. we compute the upper-left corner and the width of + # the rectangle corner_row = grouped_qubits[0] corner_col = len(self.lines[corner_row]) + 1 group_width = diagram.width(corner_row) - 1 @@ -307,10 +234,12 @@ def append_diagram(self, diagram, group=None): self.append(q, op) # add tikz grouping command if group is not None: - self.lines[corner_row][corner_col] += " " + TIKZ_GATE_GROUP(grouped_qubits, group_width, group) + self.lines[corner_row][corner_col] += " " + TIKZ_GATE_GROUP(grouped_qubits, + group_width, + group) return self - def interval(self, low, high): + def interval(self, low, high) -> list: """ All qubits in the diagram, from low to high, inclusive. """ @@ -318,14 +247,14 @@ def interval(self, low, high): qubits = list(set(full_interval) & set(self.qubits)) return sorted(qubits) - def is_interval(self, qubits): + def is_interval(self, qubits) -> bool: """ Do the specified qubits correspond to an interval in this diagram? """ return qubits == self.interval(min(qubits), max(qubits)) -def split_on_terminal_measures(program: Program): +def split_on_terminal_measures(program: Program) -> tuple: """ Split a program into two lists of instructions: @@ -383,7 +312,8 @@ def build(self): Actually build the diagram. """ qubits = self.circuit.get_qubits() - all_qubits = range(min(qubits), max(qubits) + 1) if self.settings.impute_missing_qubits else sorted(qubits) + all_qubits = range(min(qubits), max(qubits) + 1) if self.settings.impute_missing_qubits \ + else sorted(qubits) self.diagram = DiagramState(all_qubits) if self.settings.right_align_terminal_measurements: @@ -408,7 +338,8 @@ def build(self): if isinstance(instr, Pragma) and instr.command == PRAGMA_BEGIN_GROUP: self._build_group() elif isinstance(instr, Pragma) and instr.command == PRAGMA_END_GROUP: - raise ValueError("PRAGMA {} found without matching {}.".format(PRAGMA_END_GROUP, PRAGMA_BEGIN_GROUP)) + raise ValueError("PRAGMA {} found without matching {}.".format(PRAGMA_END_GROUP, + PRAGMA_BEGIN_GROUP)) elif isinstance(instr, Measurement): self._build_measure() elif isinstance(instr, Gate): @@ -438,8 +369,9 @@ def build(self): for instr in self.working_instructions: self._build_measure() + offset = max(self.settings.qubit_line_open_wire_length, 0) self.diagram.extend_lines_to_common_edge(self.diagram.qubits, - offset=max(self.settings.qubit_line_open_wire_length, 0)) + offset=offset) return self.diagram def _build_group(self): @@ -450,11 +382,13 @@ def _build_group(self): """ instr = self.working_instructions[self.index] if len(instr.args) != 0: - raise ValueError("PRAGMA {} expected a freeform string, or nothing at all.".format(PRAGMA_BEGIN_GROUP)) + raise ValueError(f"PRAGMA {PRAGMA_BEGIN_GROUP} expected a freeform string, or nothing " + f"at all.") start = self.index + 1 # walk instructions until the group end for j in range(start, len(self.working_instructions)): - if isinstance(self.working_instructions[j], Pragma) and self.working_instructions[j].command == PRAGMA_END_GROUP: + if isinstance(self.working_instructions[j], Pragma) \ + and self.working_instructions[j].command == PRAGMA_END_GROUP: # recursively build the diagram for this block # we do not want labels here! block_settings = replace(self.settings, @@ -482,7 +416,8 @@ def _build_measure(self): def _build_custom_source_target_op(self): """ - Update the partial diagram with a single operation involving a source and a target (e.g. a controlled gate, a swap). + Update the partial diagram with a single operation involving a source and a target + (e.g. a controlled gate, a swap). Advances the index by one. """ @@ -491,7 +426,7 @@ def _build_custom_source_target_op(self): displaced = self.diagram.interval(min(source, target), max(source, target)) self.diagram.extend_lines_to_common_edge(displaced) source_op, target_op = SOURCE_TARGET_OP[instr.name] - offset = (-1 if source > target else 1) * (len(displaced) - 1) # this is a directed quantity + offset = (-1 if source > target else 1) * (len(displaced) - 1) # a directed quantity self.diagram.append(source, source_op(source, offset)) self.diagram.append(target, target_op()) self.diagram.extend_lines_to_common_edge(displaced) @@ -526,20 +461,22 @@ def _build_generic_unitary(self): control_qubits = qubits[:controls] target_qubits = qubits[controls:] if not self.diagram.is_interval(sorted(target_qubits)): - raise ValueError("Unable to render instruction {} which targets non-adjacent qubits.".format(instr)) + raise ValueError(f"Unable to render instruction {instr} which targets non-adjacent " + f"qubits.") for q in control_qubits: self.diagram.append(q, TIKZ_CONTROL(q, target_qubits[0])) # we put the gate on the first target line, and nop on the others - self.diagram.append(target_qubits[0], TIKZ_GATE(instr.name, size=len(qubits), params=instr.params, dagger=dagger)) + self.diagram.append(target_qubits[0], TIKZ_GATE(instr.name, size=len(qubits), + params=instr.params, dagger=dagger)) for q in target_qubits[1:]: self.diagram.append(q, TIKZ_NOP()) self.index += 1 -def qubit_indices(instr: AbstractInstruction): +def qubit_indices(instr: AbstractInstruction) -> list: """ Get a list of indices associated with the given instruction. """ diff --git a/pyquil/latex/_ipython.py b/pyquil/latex/_ipython.py index ddb4ad9d1..838132cef 100644 --- a/pyquil/latex/_ipython.py +++ b/pyquil/latex/_ipython.py @@ -22,7 +22,7 @@ from IPython.display import Image from pyquil import Program -from pyquil.latex.latex_generation import to_latex +from pyquil.latex._main import to_latex from pyquil.latex._diagram import DiagramSettings diff --git a/pyquil/latex/_main.py b/pyquil/latex/_main.py new file mode 100644 index 000000000..ecd60b508 --- /dev/null +++ b/pyquil/latex/_main.py @@ -0,0 +1,109 @@ +############################################################################## +# Copyright 2016-2019 Rigetti Computing +# +# 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. +############################################################################## +""" +The main entry point to the LaTeX generation functionality in pyQuil. +""" +from typing import Optional + +from pyquil import Program +from pyquil.latex._diagram import DiagramBuilder, DiagramSettings + + +def to_latex(circuit: Program, settings: Optional[DiagramSettings] = None) -> str: + """ + Translates a given pyQuil Program to a TikZ picture in a LaTeX document. + + Here are some high points of the generation procedure (see ``pyquil/latex/_diagram.py``): + + - The most basic building block are the TikZ operators, which are constructed + by the functions in ``_diagram.py`` (e.g. TIKZ_CONTROL, TIKZ_NOP, TIKZ_MEASURE). + - TikZ operators are maintained by a DiagramState object, with roughly each + qubit line in a diagram represented as a list of TikZ operators on the ``DiagramState``. + - The ``DiagramBuilder`` is the actual driver. This traverses a ``Program`` and, for + each instruction, performs a suitable manipulation of the ``DiagramState``. At + the end of this, the ``DiagramState`` is traversed and raw LaTeX is emitted. + - Most options are specified by ``DiagramSettings``. One exception is this: it is possible + to request that a certain subset of the program is rendered as a group (and colored + as such). This is specified by a new pragma in the ``Program`` source: + + PRAGMA LATEX_GATE_GROUP ? + ... + PRAGMA END_LATEX_GATE_GROUP + + The is optional, and will be used to label the group. Nested gate + groups are currently not supported. + + :param circuit: The circuit to be drawn, represented as a pyquil program. + :param settings: An optional object of settings controlling diagram rendering and layout. + :return: LaTeX document string which can be compiled. + """ + if settings is None: + settings = DiagramSettings() + text = header() + text += "\n" + text += body(circuit, settings) + text += "\n" + text += footer() + return text + + +def header() -> str: + """ + Writes the LaTeX header using the settings file. + + The header includes all packages and defines all tikz styles. + + :return: Header of the LaTeX document. + """ + packages = (r"\documentclass[convert={density=300,outext=.png}]{standalone}", + r"\usepackage[margin=1in]{geometry}", + r"\usepackage{tikz}", + r"\usetikzlibrary{quantikz}") + + init = (r"\begin{document}", + r"\begin{tikzcd}") + + return "\n".join(("\n".join(packages), "\n".join(init))) + + +def footer() -> str: + """ + Return the footer of the LaTeX document. + + :return: LaTeX document footer. + """ + return "\\end{tikzcd}\n\\end{document}" + + +def body(circuit: Program, settings: DiagramSettings) -> str: + """ + Return the body of the LaTeX document, including the entire circuit in + TikZ format. + + :param circuit: The circuit to be drawn, represented as a pyquil program. + :param settings: Options controlling rendering and layout. + + :return: LaTeX string to draw the entire circuit. + """ + + diagram = DiagramBuilder(circuit, settings).build() + + # flush lines + quantikz_out = [] + for qubit in diagram.qubits: + quantikz_out.append(" & ".join(diagram.lines[qubit])) + + return " \\\\\n".join(quantikz_out) diff --git a/pyquil/latex/latex_generation.py b/pyquil/latex/latex_generation.py index efb852c34..8b476484d 100644 --- a/pyquil/latex/latex_generation.py +++ b/pyquil/latex/latex_generation.py @@ -14,26 +14,15 @@ # limitations under the License. ############################################################################## +import warnings from typing import Optional from pyquil import Program -from pyquil.latex._diagram import header, body, footer, DiagramSettings +from pyquil.latex._diagram import DiagramSettings def to_latex(circuit: Program, settings: Optional[DiagramSettings] = None) -> str: - """ - Translates a given pyquil Program to a TikZ picture in a LaTeX document. - - :param Program circuit: The circuit to be drawn, represented as a pyquil program. - :param DiagramSettings settings: An optional object of settings controlling diagram rendering and layout. - :return: LaTeX document string which can be compiled. - :rtype: string - """ - if settings is None: - settings = DiagramSettings() - text = header() - text += "\n" - text += body(circuit, settings) - text += "\n" - text += footer() - return text + from pyquil.latex._main import to_latex + warnings.warn('"pyquil.latex.latex_generation.to_latex" has been moved -- please import it' + 'as "from pyquil.latex import to_latex going forward"', FutureWarning) + return to_latex(circuit, settings)