Skip to content

Commit

Permalink
Adding SimSystemModel class as higher level class containing Hamilton…
Browse files Browse the repository at this point in the history
…ianModel (Qiskit#496)

* added PulseSystemModel to extract information about backend relevant to simulation

* Moved HamiltonianModel.calculate_frequencies to PulseSystemModel.calculate_channel_frequencies

* Modifying constructors for HamiltonianModel to separate object specification from string parsing

* added PulseDefaults object into the pulse_simulator, and changed the default memory_slots setting in digest to be equal to the number of qubits

* changed digest default 'meas_level' to 2
  • Loading branch information
DanPuzzuoli authored and chriseclectic committed Jan 23, 2020
1 parent c5fb127 commit e7a3570
Show file tree
Hide file tree
Showing 12 changed files with 1,102 additions and 1,120 deletions.
344 changes: 121 additions & 223 deletions example/pulse_sim.ipynb

Large diffs are not rendered by default.

26 changes: 22 additions & 4 deletions qiskit/providers/aer/backends/pulse_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
# pylint: disable=arguments-differ
# pylint: disable=arguments-differ, missing-return-type-doc

"""
Qiskit Aer OpenPulse simulator backend.
Expand All @@ -19,8 +19,9 @@
import time
import datetime
import logging
from numpy import inf
from qiskit.result import Result
from qiskit.providers.models import BackendConfiguration
from qiskit.providers.models import BackendConfiguration, PulseDefaults
from .aerbackend import AerBackend
from ..aerjob import AerJob
from ..aererror import AerError
Expand Down Expand Up @@ -52,23 +53,32 @@ class PulseSimulator(AerBackend):
}

def __init__(self, configuration=None, provider=None):

# purpose of defaults is to pass assemble checks
self._defaults = PulseDefaults(qubit_freq_est=[inf],
meas_freq_est=[inf],
buffer=0,
cmd_def=[],
pulse_library=[])
super().__init__(self,
BackendConfiguration.from_dict(self.DEFAULT_CONFIGURATION),
provider=provider)

def run(self, qobj,
system_model,
backend_options=None,
noise_model=None,
validate=False):
"""Run a qobj on the backend."""
# Submit job
job_id = str(uuid.uuid4())
aer_job = AerJob(self, job_id, self._run_job, qobj,
aer_job = AerJob(self, job_id, self._run_job, qobj, system_model,
backend_options, noise_model, validate)
aer_job.submit()
return aer_job

def _run_job(self, job_id, qobj,
system_model,
backend_options,
noise_model,
validate):
Expand All @@ -77,7 +87,7 @@ def _run_job(self, job_id, qobj,
if validate:
self._validate(qobj, backend_options, noise_model)
# Send to solver
openpulse_system = digest_pulse_obj(qobj, backend_options, noise_model)
openpulse_system = digest_pulse_obj(qobj, system_model, backend_options, noise_model)
results = opsolve(openpulse_system)
end = time.time()
return self._format_results(job_id, results, end - start, qobj.qobj_id)
Expand Down Expand Up @@ -106,3 +116,11 @@ def _validate(self, qobj, backend_options, noise_model):
'entry to configure the simulator')

super()._validate(qobj, backend_options, noise_model)

def defaults(self):
"""Return defaults.
Returns:
PulseDefaults: object for passing assemble.
"""
return self._defaults
185 changes: 99 additions & 86 deletions qiskit/providers/aer/openpulse/hamiltonian_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,50 @@
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
# pylint: disable=eval-used, exec-used, invalid-name
# pylint: disable=eval-used, exec-used, invalid-name, missing-return-type-doc

"HamiltonianModel class for system specification for the PulseSimulator"

from collections import OrderedDict
import numpy as np
import numpy.linalg as la
from .qobj.opparse import HamiltonianParser
from ..aererror import AerError


class HamiltonianModel():
"""Hamiltonian model for pulse simulator."""
def __init__(self, hamiltonian, qubits=None):

def __init__(self,
system=None,
variables=None,
qubit_dims=None,
oscillator_dims=None):
"""Initialize a Hamiltonian model.
Args:
hamiltonian (dict): Hamiltonian dictionary.
qubits (list or None): List of qubits to extract from the hamiltonian.
system (list): List of Qobj objects representing operator form of the Hamiltonian.
variables (OrderedDict): Ordered dict for parameter values in Hamiltonian.
qubit_dims (dict): dict of qubit dimensions.
oscillator_dims (dict): dict of oscillator dimensions.
Raises:
ValueError: if arguments are invalid.
"""

# Initialize internal variables
# The system Hamiltonian in numerical format
self._system = None
self._system = system
# System variables
self._vars = None
self._variables = variables
# Channels in the Hamiltonian string
# Qubit subspace dimensinos
self._qubit_dims = qubit_dims or {}
# Oscillator subspace dimensions
self._oscillator_dims = oscillator_dims or {}

# The rest are computed from the previous

# These tell the order in which the channels are evaluated in
# the RHS solver.
self._channels = None
Expand All @@ -48,100 +64,82 @@ def __init__(self, hamiltonian, qubits=None):
self._evals = None
# Eigenstates of the time-indepedent hamiltonian
self._estates = None
# Qubit subspace dimensinos
self._dim_qub = {}
# Oscillator subspace dimensions
self._dim_osc = {}

# Parse Hamiltonian
# TODO: determine n_qubits from hamiltonian if qubits is None
n_qubits = len(qubits) if qubits else None
if not n_qubits:
raise ValueError("TODO: Need to infer n_qubits from "
"Hamiltonian if qubits list is not specified")
# populate self._channels
self._calculate_hamiltonian_channels()

# populate self._h_diag, self._evals, self._estates
self._compute_drift_data()

@classmethod
def from_dict(cls, hamiltonian, qubit_list=None):
"""Initialize from a Hamiltonian string specification.
Args:
hamiltonian (dict): dictionary representing Hamiltonian in string specification.
qubit_list (list or None): List of qubits to extract from the hamiltonian.
Returns:
HamiltonianModel: instantiated from hamiltonian dictionary
Raises:
ValueError: if arguments are invalid.
"""

_hamiltonian_parse_exceptions(hamiltonian)

self._vars = OrderedDict(hamiltonian['vars'])
# get variables
variables = OrderedDict(hamiltonian['vars'])

# Get qubit subspace dimensions
if 'qub' in hamiltonian:
self._dim_qub = {
if qubit_list is None:
qubit_list = [int(qubit) for qubit in hamiltonian['qub']]

qubit_dims = {
int(key): val
for key, val in hamiltonian['qub'].items()
}
else:
self._dim_qub = {}.fromkeys(range(n_qubits), 2)
qubit_dims = {}

# Get oscillator subspace dimensions
if 'osc' in hamiltonian:
self._dim_osc = {
oscillator_dims = {
int(key): val
for key, val in hamiltonian['osc'].items()
}
else:
oscillator_dims = {}

# Step 1: Parse the Hamiltonian
# Parse the Hamiltonian
system = HamiltonianParser(h_str=hamiltonian['h_str'],
dim_osc=self._dim_osc,
dim_qub=self._dim_qub)
system.parse(qubits)
self._system = system.compiled
dim_osc=oscillator_dims,
dim_qub=qubit_dims)
system.parse(qubit_list)
system = system.compiled

# Step #2: Determine Hamiltonian channels
self._calculate_hamiltonian_channels()
return cls(system, variables, qubit_dims, oscillator_dims)

# Step 3: Calculate diagonal hamiltonian
self._calculate_drift_hamiltonian()

def calculate_frequencies(self, qubit_lo_freq=None, u_channel_lo=None):
"""Calulate frequencies for the Hamiltonian.
Args:
qubit_lo_freq (list or None): list of qubit linear
oscillator drive frequencies. If None these will be calcualted
automatically from hamiltonian (Default: None).
u_channel_lo (list or None): list of u channel parameters (Default: None).
def get_qubit_lo_from_drift(self):
""" Computes a list of qubit frequencies corresponding to the exact energy
gap between the ground and first excited states of each qubit.
Returns:
OrderedDict: a dictionary of channel frequencies.
Raises:
ValueError: If channel or u_channel_lo are invalid.
qubit_lo_freq (list): the list of frequencies
"""
# TODO: Update docstring with description of what qubit_lo_freq and
# u_channel_lo are

# Setup freqs for the channels
freqs = OrderedDict()

# Set qubit frequencies from hamiltonian
if not qubit_lo_freq or (
qubit_lo_freq == 'from_hamiltonian' and len(self._dim_osc) == 0):
qubit_lo_freq = np.zeros(len(self._dim_qub))
min_eval = np.min(self._evals)
for q_idx in range(len(self._dim_qub)):
single_excite = _first_excited_state(q_idx, self._dim_qub)
dressed_eval = _eval_for_max_espace_overlap(
single_excite, self._evals, self._estates)
qubit_lo_freq[q_idx] = (dressed_eval - min_eval) / (2 * np.pi)

# TODO: set u_channel_lo from hamiltonian
if not u_channel_lo:
raise ValueError("u_channel_lo cannot be None.")

# Set frequencies
for key in self._channels.keys():
chidx = int(key[1:])
if key[0] == 'D':
freqs[key] = qubit_lo_freq[chidx]
elif key[0] == 'U':
freqs[key] = 0
for u_lo_idx in u_channel_lo[chidx]:
if u_lo_idx['q'] < len(qubit_lo_freq):
qfreq = qubit_lo_freq[u_lo_idx['q']]
qscale = u_lo_idx['scale'][0]
freqs[key] += qfreq * qscale
else:
raise ValueError("Channel is not D or U")
return freqs
qubit_lo_freq = [0] * len(self._qubit_dims)

# compute difference between first excited state of each qubit and
# the ground energy
min_eval = np.min(self._evals)
for q_idx in range(len(self._qubit_dims)):
single_excite = _first_excited_state(q_idx, self._qubit_dims)
dressed_eval = _eval_for_max_espace_overlap(
single_excite, self._evals, self._estates)
qubit_lo_freq[q_idx] = (dressed_eval - min_eval) / (2 * np.pi)

return qubit_lo_freq

def _calculate_hamiltonian_channels(self):
""" Get all the qubit channels D_i and U_i in the string
Expand Down Expand Up @@ -181,7 +179,7 @@ def _calculate_hamiltonian_channels(self):

self._channels = channel_dict

def _calculate_drift_hamiltonian(self):
def _compute_drift_data(self):
"""Calculate the the drift Hamiltonian.
This computes the dressed frequencies and eigenstates of the
Expand All @@ -197,8 +195,8 @@ def _calculate_drift_hamiltonian(self):

# might be a better solution to replace the 'var' in the hamiltonian
# string with 'op_system.vars[var]'
for var in self._vars:
exec('%s=%f' % (var, self._vars[var]))
for var in self._variables:
exec('%s=%f' % (var, self._variables[var]))

ham_full = np.zeros(np.shape(self._system[0][0].full()), dtype=complex)
for ham_part in self._system:
Expand All @@ -219,7 +217,20 @@ def _calculate_drift_hamiltonian(self):
self._h_diag = np.ascontiguousarray(np.diag(ham_full).real)


def _first_excited_state(qubit_idx, dim_qub):
def _hamiltonian_parse_exceptions(hamiltonian):
"""Raises exceptions for hamiltonian specification.
Parameters:
hamiltonian (dict): dictionary specification of hamiltonian
Returns:
Raises:
AerError: if some part of the hamiltonian dictionary is unsupported
"""
if 'osc' in hamiltonian:
raise AerError('Oscillator-type systems are not supported.')


def _first_excited_state(qubit_idx, qubit_dims):
"""
Returns the vector corresponding to all qubits in the 0 state, except for
qubit_idx in the 1 state.
Expand All @@ -231,17 +242,19 @@ def _first_excited_state(qubit_idx, dim_qub):
Parameters:
qubit_idx (int): the qubit to be in the 1 state
dim_qub (dict): a dictionary with keys being qubit index, and
qubit_dims (dict): a dictionary with keys being qubit index, and
value being the dimension of the qubit
Returns:
vector: the state with qubit_idx in state 1, and the rest in state 0
"""
vector = np.array([1.])
# iterate through qubits, tensoring on the state
for qubit, dim in dim_qub.items():
new_vec = np.zeros(dim)
if int(qubit) == qubit_idx:
qubit_indices = [int(qubit) for qubit in qubit_dims]
qubit_indices.sort()
for idx in qubit_indices:
new_vec = np.zeros(qubit_dims[idx])
if idx == qubit_idx:
new_vec[1] = 1
else:
new_vec[0] = 1
Expand Down
Loading

0 comments on commit e7a3570

Please sign in to comment.