diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 777c7566b234..8ea9db037e19 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,9 +25,11 @@ Added - Introduced new options for handling credentials (qiskitrc file, environment variables) and automatic registration. (#547) - Add OpenMP parallelization for Apple builds of the cpp simulator (#698). +- Add parallelization utilities (#701) +- Parallelize transpilation (#701) - New interactive visualizations (#765). - Added option to reverse the qubit order when plotting a circuit. (#762, #786) -- Jupyter notebook magic function qiskit_job_status (#734). +- Jupyter notebook magic function qiskit_job_status, qiskit_progress_bar (#701, #734) - Add a new function ``qobj_to_circuits`` to convert a Qobj object to a list of QuantumCircuit objects (#877) diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 006e9d410134..2d45f2826a31 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -11,6 +11,7 @@ """Main QISKit public functionality.""" import os +import sys import pkgutil # First, check for required Python and API version @@ -37,6 +38,10 @@ # to be placed *before* the wrapper imports or any non-import code. __path__ = pkgutil.extend_path(__path__, __name__) +# Allow extending this namespace. Please note that currently this line needs +# to be placed *before* the wrapper imports. +__path__ = pkgutil.extend_path(__path__, __name__) + from .wrapper._wrapper import ( available_backends, local_backends, remote_backends, get_backend, compile, execute, register, unregister, diff --git a/qiskit/_util.py b/qiskit/_util.py index cd301ce7e05b..33a326ccdde1 100644 --- a/qiskit/_util.py +++ b/qiskit/_util.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - # Copyright 2017, IBM. # # This source code is licensed under the Apache License, Version 2.0 found in @@ -12,9 +11,11 @@ import logging import re import sys +import platform import warnings import socket from collections import UserDict +import psutil API_NAME = 'IBMQuantumExperience' logger = logging.getLogger(__name__) @@ -160,6 +161,22 @@ def _parse_ibmq_credentials(url, hub=None, group=None, project=None): return url +def local_hardware_info(): + """Basic hardware information about the local machine. + + Gives actual number of CPU's in the machine, even when hyperthreading is + turned on. + + Returns: + dict: The hardware information. + + """ + results = {'os': platform.system()} + results['memory'] = psutil.virtual_memory().total / (1024**3) + results['cpus'] = psutil.cpu_count(logical=False) + return results + + def _has_connection(hostname, port): """Checks to see if internet connection exists to host via specified port diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index d0f7d84cd8da..494333569957 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -12,3 +12,6 @@ # pylint: disable=redefined-builtin from ._transpiler import compile, transpile + +from ._parallel import parallel_map +from ._progressbar import TextProgressBar diff --git a/qiskit/transpiler/_parallel.py b/qiskit/transpiler/_parallel.py new file mode 100644 index 000000000000..6adf93e96ed1 --- /dev/null +++ b/qiskit/transpiler/_parallel.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +# This file is part of QuTiP: Quantum Toolbox in Python. +# +# Copyright (c) 2011 and later, Paul D. Nation and Robert J. Johansson. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the QuTiP: Quantum Toolbox in Python nor the names +# of its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +############################################################################### + +"""Routines for running Python functions in parallel using process pools +from the multiprocessing library. +""" + +import os +import platform +from multiprocessing import Pool +from qiskit._qiskiterror import QISKitError +from qiskit._util import local_hardware_info +from ._receiver import receiver as rec +from ._progressbar import BaseProgressBar + +# Number of local physical cpus +CPU_COUNT = local_hardware_info()['cpus'] + +# Set parallel ennvironmental variable +os.environ['QISKIT_IN_PARALLEL'] = 'FALSE' + + +def parallel_map(task, values, task_args=tuple(), task_kwargs={}, # pylint: disable=W0102 + num_processes=CPU_COUNT): + """ + Parallel execution of a mapping of `values` to the function `task`. This + is functionally equivalent to:: + result = [task(value, *task_args, **task_kwargs) for value in values] + + On Windows this function defaults to a serial implimentation to avoid the + overhead from spawning processes in Windows. + + Parameters: + task (func): Function that is to be called for each value in ``task_vec``. + values (array_like): List or array of values for which the ``task`` + function is to be evaluated. + task_args (list): Optional additional arguments to the ``task`` function. + task_kwargs (dict): Optional additional keyword argument to the ``task`` function. + num_processes (int): Number of processes to spawn. + + Returns: + result: The result list contains the value of + ``task(value, *task_args, **task_kwargs)`` for + each value in ``values``. + + Raises: + QISKitError: If user interupts via keyboard. + """ + # len(values) == 1 + if len(values) == 1: + return [task(values[0], *task_args, **task_kwargs)] + + # Get last element of the receiver channels + if any(rec.channels): + progress_bar = None + for idx in rec.channels: + if rec.channels[idx].type == 'progressbar' and not rec.channels[idx].touched: + progress_bar = rec.channels[idx] + break + if progress_bar is None: + progress_bar = BaseProgressBar() + else: + progress_bar = BaseProgressBar() + + progress_bar.start(len(values)) + nfinished = [0] + + def _callback(x): # pylint: disable=W0613 + nfinished[0] += 1 + progress_bar.update(nfinished[0]) + + # Run in parallel if not Win and not in parallel already + if platform.system() != 'Windows' and num_processes > 1 \ + and os.getenv('QISKIT_IN_PARALLEL') == 'FALSE': + os.environ['QISKIT_IN_PARALLEL'] = 'TRUE' + try: + pool = Pool(processes=num_processes) + + async_res = [pool.apply_async(task, (value,) + task_args, task_kwargs, + _callback) for value in values] + + while not all([item.ready() for item in async_res]): + for item in async_res: + item.wait(timeout=0.1) + + pool.terminate() + pool.join() + + except KeyboardInterrupt: + pool.terminate() + pool.join() + progress_bar.finished() + raise QISKitError('Keyboard interrupt in parallel_map.') + + progress_bar.finished() + os.environ['QISKIT_IN_PARALLEL'] = 'FALSE' + return [ar.get() for ar in async_res] + + # Cannot do parallel on Windows , if another parallel_map is running in parallel, + # or len(values) == 1. + results = [] + for _, value in enumerate(values): + result = task(value, *task_args, **task_kwargs) + results.append(result) + _callback(0) + progress_bar.finished() + return results diff --git a/qiskit/transpiler/_progressbar.py b/qiskit/transpiler/_progressbar.py new file mode 100644 index 000000000000..fac3cd5552a3 --- /dev/null +++ b/qiskit/transpiler/_progressbar.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +# This file is part of QuTiP: Quantum Toolbox in Python. +# +# Copyright (c) 2011 and later, Paul D. Nation and Robert J. Johansson. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the QuTiP: Quantum Toolbox in Python nor the names +# of its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +############################################################################### + +"""Progress bars module""" + +import time +import datetime +import sys +from ._receiver import receiver as rec + + +class BaseProgressBar(object): + """An abstract progress bar with some shared functionality. + """ + + def __init__(self): + self.type = 'progressbar' + self.touched = False + self.channel_id = rec.add_channel(self) + self.iter = None + self.t_start = None + self.t_done = None + + def start(self, iterations): + """Start the progress bar. + + Parameters: + iterations (int): Number of iterations. + """ + self.touched = True + self.iter = int(iterations) + self.t_start = time.time() + + def update(self, n): + """Update status of progress bar. + """ + pass + + def time_elapsed(self): + """Return the time elapsed since start. + + Returns: + elapsed_time: Time since progress bar started. + """ + return "%6.2fs" % (time.time() - self.t_start) + + def time_remaining_est(self, completed_iter): + """Estimate the remaining time left. + + Parameters: + completed_iter (int): Number of iterations completed. + + Returns: + est_time: Estimated time remaining. + """ + if completed_iter: + t_r_est = (time.time() - self.t_start) / \ + completed_iter*(self.iter-completed_iter) + else: + t_r_est = 0 + date_time = datetime.datetime(1, 1, 1) + datetime.timedelta(seconds=t_r_est) + time_string = "%02d:%02d:%02d:%02d" % \ + (date_time.day - 1, date_time.hour, date_time.minute, date_time.second) + + return time_string + + def finished(self): + """Run when progress bar has completed. + """ + rec.remove_channel(self.channel_id) + + +class TextProgressBar(BaseProgressBar): + """ + A simple text-based progress bar. + """ + def start(self, iterations): + self.touched = True + self.iter = int(iterations) + self.t_start = time.time() + pbar = '-' * 50 + sys.stdout.write('\r|%s| %s%s%s [%s]' % + (pbar, 0, '/', self.iter, '')) + + def update(self, n): + filled_length = int(round(50 * n / self.iter)) + pbar = u'█' * filled_length + '-' * (50 - filled_length) + time_left = self.time_remaining_est(n) + sys.stdout.write('\r|%s| %s%s%s [%s]' % (pbar, n, '/', self.iter, time_left)) + if n == self.iter: + sys.stdout.write('\n') + sys.stdout.flush() diff --git a/qiskit/transpiler/_receiver.py b/qiskit/transpiler/_receiver.py new file mode 100644 index 000000000000..4f5473b5d807 --- /dev/null +++ b/qiskit/transpiler/_receiver.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""Receiver module for holding objects that take call backs. +""" + +from collections import OrderedDict +from qiskit._qiskiterror import QISKitError + + +class Reciever(object): + """A receiver class that holds instances of objects + (such as) progressbars that recieve call back info. + """ + def __init__(self): + self._channels = OrderedDict() + self.channel_id = 1 + + def get_channels(self): + """Getter to grab available channels. + """ + return self._channels + + def add_channel(self, transmitter): + """Add channel to the recievers channels. + + Parameters: + transmitter (object): Object to be added to channels. + + Returns: + int: Id of added channel. + """ + self._channels[self.channel_id] = transmitter + _channel_id = self.channel_id + self.channel_id += 1 + return _channel_id + + def remove_channel(self, index): + """Remove a channel from the receiver by index number. + + Parameters: + index (int): Index fo channel to remove. + + Raises: + QISKitError: Index not in receiver keys. + """ + if index in self._channels.keys(): + del self._channels[index] + else: + raise QISKitError('Index not in receiver channels.') + + channels = property(get_channels, add_channel) + + +receiver = Reciever() # pylint: disable=C0103 diff --git a/qiskit/transpiler/_transpiler.py b/qiskit/transpiler/_transpiler.py index 590e42b818d5..4479b35b6706 100644 --- a/qiskit/transpiler/_transpiler.py +++ b/qiskit/transpiler/_transpiler.py @@ -9,7 +9,6 @@ from copy import deepcopy import logging import uuid - import numpy as np import scipy.sparse as sp import scipy.sparse.csgraph as cs @@ -23,6 +22,7 @@ cx_cancellation, direction_mapper, remove_last_measurements, return_last_measurements) from qiskit.qobj import Qobj, QobjConfig, QobjExperiment, QobjItem, QobjHeader +from ._parallel import parallel_map logger = logging.getLogger(__name__) @@ -49,7 +49,7 @@ def compile(circuits, backend, pass_manager (PassManager): a pass_manager for the transpiler stage Returns: - Qobj: the Qobj to be run on the backends + QobjExperiment: Experiment to be wrapped in a Qobj. Raises: TranspilerError: in case of bad compile options, e.g. the hpc options. @@ -111,10 +111,7 @@ def _circuits_2_dags(circuits): list[DAGCircuit]: the dag representation of the circuits to be used in the transpiler """ - dags = [] - for circuit in circuits: - dag = DAGCircuit.fromQuantumCircuit(circuit) - dags.append(dag) + dags = parallel_map(DAGCircuit.fromQuantumCircuit, circuits) return dags @@ -139,22 +136,50 @@ def _transpile_dags(dags, basis_gates='u1,u2,u3,cx,id', coupling_map=None, Raises: TranspilerError: if the format is not valid. """ - # TODO: Parallelize this method - final_dags = [] - for dag, initial_layout in zip(dags, initial_layouts): - final_dag, final_layout = transpile( - dag, - basis_gates=basis_gates, - coupling_map=coupling_map, - initial_layout=initial_layout, - get_layout=True, - seed=seed, - pass_manager=pass_manager) - final_dag.layout = [[k, v] for k, v in final_layout.items()] if final_layout else None - final_dags.append(final_dag) + + index = list(range(len(dags))) + final_dags = parallel_map(_transpile_dags_parallel, index, + task_args=(dags, initial_layouts), + task_kwargs={'basis_gates': basis_gates, + 'coupling_map': coupling_map, + 'seed': seed, + 'pass_manager': pass_manager}) return final_dags +def _transpile_dags_parallel(idx, dags, initial_layouts, basis_gates='u1,u2,u3,cx,id', + coupling_map=None, seed=None, pass_manager=None): + """Helper function for transpiling in parallel (if available). + + Args: + idx (int): Index for dag of interest + dags (list): List of dags + initial_layouts (list): List of initial layouts + basis_gates (str): a comma seperated string for the target basis gates + coupling_map (list): A graph of coupling + seed (int): random seed for the swap mapper + pass_manager (PassManager): pass manager instance for the tranpilation process + If None, a default set of passes are run. + Otherwise, the passes defined in it will run. + If contains no passes in it, no dag transformations occur. + Returns: + DAGCircuit: DAG circuit after going through transpilation. + """ + dag = dags[idx] + initial_layout = initial_layouts[idx] + final_dag, final_layout = transpile( + dag, + basis_gates=basis_gates, + coupling_map=coupling_map, + initial_layout=initial_layout, + get_layout=True, + seed=seed, + pass_manager=pass_manager) + final_dag.layout = [[k, v] + for k, v in final_layout.items()] if final_layout else None + return final_dag + + def _dags_2_qobj(dags, backend_name, config=None, shots=None, max_credits=None, qobj_id=None, basis_gates=None, coupling_map=None, seed=None): @@ -196,28 +221,10 @@ def _dags_2_qobj(dags, backend_name, config=None, shots=None, if seed: qobj.config.seed = seed - for dag in dags: - json_circuit = DagUnroller(dag, JsonBackend(dag.basis)).execute() - # Step 3a: create the Experiment based on json_circuit - experiment = QobjExperiment.from_dict(json_circuit) - # Step 3b: populate the Experiment configuration and header - experiment.header.name = dag.name - # TODO: place in header or config? - experiment_config = deepcopy(config or {}) - experiment_config.update({ - 'coupling_map': coupling_map, - 'basis_gates': basis_gates, - 'layout': dag.layout, - 'memory_slots': sum(dag.cregs.values()), - # TODO: `n_qubits` is not part of the qobj spec, but needed for the simulator. - 'n_qubits': sum(dag.qregs.values())}) - experiment.config = QobjItem(**experiment_config) - - # set eval_symbols=True to evaluate each symbolic expression - # TODO: after transition to qobj, we can drop this - experiment.header.compiled_circuit_qasm = dag.qasm(qeflag=True, eval_symbols=True) - # Step 3c: add the Experiment to the Qobj - qobj.experiments.append(experiment) + qobj.experiments = parallel_map(_dags_2_qobj_parallel, dags, + task_kwargs={'basis_gates': basis_gates, + 'config': config, + 'coupling_map': coupling_map}) # Update the `memory_slots` value. # TODO: remove when `memory_slots` can be provided by the user. @@ -233,6 +240,43 @@ def _dags_2_qobj(dags, backend_name, config=None, shots=None, return qobj +def _dags_2_qobj_parallel(dag, config=None, basis_gates=None, + coupling_map=None): + """Helper function for dags to qobj in parallel (if available). + + Args: + dag (DAGCircuit): DAG to compile + config (dict): dictionary of parameters (e.g. noise) used by runner + basis_gates (list[str])): basis gates for the experiment + coupling_map (list): coupling map (perhaps custom) to target in mapping + + Returns: + Qobj: Qobj to be run on the backends + """ + json_circuit = DagUnroller(dag, JsonBackend(dag.basis)).execute() + # Step 3a: create the Experiment based on json_circuit + experiment = QobjExperiment.from_dict(json_circuit) + # Step 3b: populate the Experiment configuration and header + experiment.header.name = dag.name + # TODO: place in header or config? + experiment_config = deepcopy(config or {}) + experiment_config.update({ + 'coupling_map': coupling_map, + 'basis_gates': basis_gates, + 'layout': dag.layout, + 'memory_slots': sum(dag.cregs.values()), + # TODO: `n_qubits` is not part of the qobj spec, but needed for the simulator. + 'n_qubits': sum(dag.qregs.values())}) + experiment.config = QobjItem(**experiment_config) + + # set eval_symbols=True to evaluate each symbolic expression + # TODO: after transition to qobj, we can drop this + experiment.header.compiled_circuit_qasm = dag.qasm( + qeflag=True, eval_symbols=True) + # Step 3c: add the Experiment to the Qobj + return experiment + + def transpile(dag, basis_gates='u1,u2,u3,cx,id', coupling_map=None, initial_layout=None, get_layout=False, format='dag', seed=None, pass_manager=None): diff --git a/qiskit/wrapper/_wrapper.py b/qiskit/wrapper/_wrapper.py index 8b8a2408de59..836cecda3bc6 100644 --- a/qiskit/wrapper/_wrapper.py +++ b/qiskit/wrapper/_wrapper.py @@ -9,11 +9,23 @@ import logging import warnings -from qiskit import transpiler, QISKitError +from copy import deepcopy +import uuid +from qiskit._qiskiterror import QISKitError +from qiskit._quantumcircuit import QuantumCircuit from qiskit.backends.ibmq import IBMQProvider from qiskit.wrapper import credentials from qiskit.wrapper.defaultqiskitprovider import DefaultQISKitProvider +from qiskit.transpiler._passmanager import PassManager +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler._transpiler import (_matches_coupling_map, + _pick_best_layout, + _dags_2_qobj_parallel, + _transpile_dags_parallel) +from qiskit.qobj._qobj import Qobj, QobjConfig, QobjHeader from qiskit._util import _parse_ibmq_credentials +from qiskit.transpiler._transpilererror import TranspilerError +from qiskit.transpiler._parallel import parallel_map from ._circuittoolkit import circuit_from_qasm_file, circuit_from_qasm_string # Default provider used by the rest of the functions on this module. Please @@ -278,19 +290,104 @@ def compile(circuits, backend, creates a qobj with minimal check nor translation Returns: Qobj: the qobj to be run on the backends + + Raises: + TranspilerError: in case of bad compile options, e.g. the hpc options. """ # pylint: disable=redefined-builtin + + # Check for valid parameters for the experiments. + if hpc is not None and \ + not all(key in hpc for key in ('multi_shot_optimization', 'omp_num_threads')): + raise TranspilerError('Unknown HPC parameter format!') + + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + if isinstance(backend, str): backend = _DEFAULT_PROVIDER.get_backend(backend) pass_manager = None # default pass manager which executes predetermined passes if skip_transpiler: # empty pass manager which does nothing - pass_manager = transpiler.PassManager() + pass_manager = PassManager() + + backend_conf = backend.configuration() + backend_name = backend_conf['name'] + basis_gates = basis_gates or backend_conf['basis_gates'] + coupling_map = coupling_map or backend_conf['coupling_map'] + + qobj_config = deepcopy(config or {}) + qobj_config.update({'shots': shots, + 'max_credits': max_credits, + 'memory_slots': 0}) + + qobj = Qobj(qobj_id=qobj_id or str(uuid.uuid4()), + config=QobjConfig(**qobj_config), + experiments=[], + header=QobjHeader(backend_name=backend_name)) + + if seed: + qobj.config.seed = seed + + qobj.experiments = parallel_map(_build_exp_parallel, list(range(len(circuits))), + task_args=(circuits, backend), + task_kwargs={'initial_layout': initial_layout, + 'basis_gates': basis_gates, + 'config': config, + 'coupling_map': coupling_map, + 'seed': seed, + 'pass_manager': pass_manager}) + + qobj.config.memory_slots = max(experiment.config.memory_slots for + experiment in qobj.experiments) + + qobj.config.n_qubits = max(experiment.config.n_qubits for + experiment in qobj.experiments) + + return qobj + + +def _build_exp_parallel(idx, circuits, backend, initial_layout=None, + basis_gates='u1,u2,u3,cx,id', config=None, + coupling_map=None, seed=None, pass_manager=None): + """Builds a single Qobj experiment. Usually called in parallel mode. + + Args: + idx (int): Index of circuit in circuits list. + circuits (list): List of circuits passed. + backend (BaseBackend or str): a backend to compile for + initial_layout (list): initial layout of qubits in mapping + basis_gates (str): comma-separated basis gate set to compile to + config (dict): dictionary of parameters (e.g. noise) used by runner + coupling_map (list): coupling map (perhaps custom) to target in mapping + initial_layout (list): initial layout of qubits in mapping + seed (int): random seed for simulators + pass_manager (PassManager): pass manager instance for the tranpilation process + If None, a default set of passes are run. + Otherwise, the passes defined in it will run. + If contains no passes in it, no dag transformations occur. + + Returns: + experiment: An instance of an experiment to be added to a Qobj. + """ + + circuit = circuits[idx] + dag = DAGCircuit.fromQuantumCircuit(circuit) + + if (initial_layout is None and not backend.configuration()['simulator'] + and not _matches_coupling_map(dag, coupling_map)): + _initial_layout = _pick_best_layout(dag, backend) + else: + _initial_layout = initial_layout + + dag = _transpile_dags_parallel(0, [dag], [_initial_layout], + basis_gates=basis_gates, coupling_map=coupling_map, + seed=seed, pass_manager=pass_manager) + + experiment = _dags_2_qobj_parallel( + dag, basis_gates=basis_gates, config=config, coupling_map=coupling_map) - return transpiler.compile(circuits, backend, - config, basis_gates, coupling_map, initial_layout, - shots, max_credits, seed, qobj_id, hpc, - pass_manager) + return experiment def execute(circuits, backend, diff --git a/qiskit/wrapper/jupyter/__init__.py b/qiskit/wrapper/jupyter/__init__.py index 1364c6706379..f378cd6532db 100644 --- a/qiskit/wrapper/jupyter/__init__.py +++ b/qiskit/wrapper/jupyter/__init__.py @@ -9,7 +9,9 @@ """ from IPython import get_ipython # pylint: disable=import-error -from .jupyter_magics import StatusMagic +from .jupyter_magics import (ProgressBarMagic, StatusMagic) +from .progressbar import HTMLProgressBar _IP = get_ipython() +_IP.register_magics(ProgressBarMagic) _IP.register_magics(StatusMagic) diff --git a/qiskit/wrapper/jupyter/jupyter_magics.py b/qiskit/wrapper/jupyter/jupyter_magics.py index 8f96e50f2735..359e4e07945e 100644 --- a/qiskit/wrapper/jupyter/jupyter_magics.py +++ b/qiskit/wrapper/jupyter/jupyter_magics.py @@ -15,6 +15,9 @@ from IPython.core.magic import cell_magic, Magics, magics_class # pylint: disable=import-error import ipywidgets as widgets # pylint: disable=import-error import qiskit +from qiskit.transpiler._receiver import receiver as rec +from qiskit.transpiler._progressbar import TextProgressBar +from qiskit.wrapper.jupyter.progressbar import HTMLProgressBar @magics_class @@ -119,3 +122,32 @@ def _checker(job_var, status, header): # Group all HTML widgets into single vertical layout box = widgets.VBox(job_checkers) display(box) + + +@magics_class +class ProgressBarMagic(Magics): + """A class of progress bar magic functions. + """ + @cell_magic + @magic_arguments.magic_arguments() + @magic_arguments.argument( + '-t', + '--type', + type=str, + default='html', + help="Type of progress bar, 'html' or 'text'." + ) + def qiskit_progress_bar(self, line='', cell=None): # pylint: disable=W0613 + """A Jupyter magic function to generate progressbar. + """ + args = magic_arguments.parse_argstring(self.qiskit_progress_bar, line) + if args.type == 'html': + progress_bar = HTMLProgressBar() + elif args.type == 'text': + progress_bar = TextProgressBar() + else: + raise qiskit.QISKitError('Invalid progress bar type.') + self.shell.ex(cell) + # Remove progress bar from receiver if not used in cell + if progress_bar.channel_id in rec.channels.keys(): + rec.remove_channel(progress_bar.channel_id) diff --git a/qiskit/wrapper/jupyter/progressbar.py b/qiskit/wrapper/jupyter/progressbar.py new file mode 100644 index 000000000000..dcd6e42d7fc4 --- /dev/null +++ b/qiskit/wrapper/jupyter/progressbar.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""An HTML progress bar for Jupyter notebooks""" + +import time +import ipywidgets as widgets # pylint: disable=import-error +from IPython.display import display # pylint: disable=import-error +import qiskit.transpiler._receiver as rec +from qiskit.transpiler._progressbar import BaseProgressBar + + +class HTMLProgressBar(BaseProgressBar): + """ + A simple HTML progress bar for using in IPython notebooks. + """ + def __init__(self): + super().__init__() + self.progress_bar = None + self.label = None + self.box = None + + def start(self, iterations): + self.touched = True + self.iter = int(iterations) + self.t_start = time.time() + self.progress_bar = widgets.IntProgress(min=0, max=self.iter, value=0) + self.progress_bar.bar_style = 'info' + self.label = widgets.HTML() + self.box = widgets.VBox(children=[self.label, self.progress_bar]) + display(self.box) + + def update(self, n): + self.progress_bar.value += 1 + lbl = "Completed %s/%s: Est. remaining time: %s." + self.label.value = lbl % (n, self.iter, self.time_remaining_est(n)) + + def finished(self): + self.t_done = time.time() + self.progress_bar.bar_style = 'success' + self.label.value = "Elapsed time: %s" % self.time_elapsed() + rec.receiver.remove_channel(self.channel_id) diff --git a/requirements.txt b/requirements.txt index 3b07ba0bd7bf..008474656e5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ ply>=3.10 scipy>=0.19,!=0.19.1 sympy>=1.0 pillow>=4.2.1 +psutil diff --git a/test/python/current_matplot.png b/test/python/current_matplot.png new file mode 100644 index 000000000000..5a47f6a39738 Binary files /dev/null and b/test/python/current_matplot.png differ diff --git a/test/python/test_compiler.py b/test/python/test_compiler.py index ed26da8e1c31..81ab1b5fb0cb 100644 --- a/test/python/test_compiler.py +++ b/test/python/test_compiler.py @@ -531,6 +531,21 @@ def test_example_swap_bits(self): self.assertEqual(result.get_counts(qc), {'010000': 1024}) + def test_parallel_compile(self): + """Trigger parallel routines in compile. + """ + backend = FakeBackEnd() + qr = QuantumRegister(16) + cr = ClassicalRegister(2) + qc = QuantumCircuit(qr, cr) + qc.h(qr[0]) + for k in range(1, 15): + qc.cx(qr[0], qr[k]) + qc.measure(qr[5], cr[0]) + qlist = [qc for k in range(10)] + qobj = compile(qlist, backend=backend) + self.assertEqual(len(qobj.experiments), 10) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/test/python/test_parallel.py b/test/python/test_parallel.py new file mode 100644 index 000000000000..dae923e0ec29 --- /dev/null +++ b/test/python/test_parallel.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# Copyright 2017, IBM. +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +"""Tests for qiskit/_util.py""" + +import time +from qiskit.transpiler._receiver import receiver as rec +from qiskit.transpiler._parallel import parallel_map +from qiskit.transpiler._progressbar import TextProgressBar +from .common import QiskitTestCase + + +def _parfunc(x): + """Function for testing parallel_map + """ + time.sleep(1) + return x + + +class TestParallel(QiskitTestCase): + """A class for testing parallel_map functionality. + """ + def test_parallel(self): + """Test parallel_map """ + ans = parallel_map(_parfunc, list(range(10))) + self.assertEqual(ans, list(range(10))) + + def test_parallel_progressbar(self): + """Test parallel_map with progress bar""" + TextProgressBar() + ans = parallel_map(_parfunc, list(range(10))) + self.assertEqual(ans, list(range(10))) + + def test_parallel_progbar_used(self): + """Test that correct progressbar is used.""" + not_used = TextProgressBar() + not_used.touched = True + used = TextProgressBar() + parallel_map(_parfunc, list(range(10))) + self.assertTrue(used.channel_id not in rec.channels.keys()) + self.assertTrue(not_used.channel_id in rec.channels.keys())