Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

Commit

Permalink
Convert datetime string to object (#658)
Browse files Browse the repository at this point in the history
* convert datetime

* always check for pulse backend

* need terra 0.14
  • Loading branch information
jyu00 authored May 12, 2020
1 parent 23e0106 commit 864a5b6
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 49 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ The format is based on [Keep a Changelog].
> - **Security**: in case of vulnerabilities.

## [0.7.1] - 2020-05-12
## [0.7.1] - 2020-05-13

### Fixed

- Fixed an issue where job status was incorrectly shown as `RUNNING` when it
should be `QUEUED`. (\#663)
- Fixed timestamp formats in `QueueInfo`. (\#668)
- Fixed timestamp formats in `QueueInfo`. (\#668)
- Fixed timestamp formats in `backend.configuration()` and `backend.properties()`
return values. (\#658)

## [0.7.0] - 2020-04-29

Expand Down
4 changes: 2 additions & 2 deletions qiskit/providers/ibmq/accountprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from .ibmqbackend import IBMQBackend, IBMQSimulator
from .credentials import Credentials
from .ibmqbackendservice import IBMQBackendService
from .utils.json_decoder import decode_pulse_backend_config
from .utils.json_decoder import decode_backend_configuration

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -121,8 +121,8 @@ def _discover_remote_backends(self, timeout: Optional[float] = None) -> Dict[str
continue

try:
decode_backend_configuration(raw_config)
if raw_config.get('open_pulse', False):
decode_pulse_backend_config(raw_config)
config = PulseBackendConfiguration.from_dict(raw_config)
else:
config = QasmBackendConfiguration.from_dict(raw_config)
Expand Down
4 changes: 3 additions & 1 deletion qiskit/providers/ibmq/ibmqbackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
IBMQBackendJobLimitError)
from .job import IBMQJob
from .utils import update_qobj_config, validate_job_tags
from .utils.json_decoder import decode_pulse_defaults
from .utils.json_decoder import decode_pulse_defaults, decode_backend_properties

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -260,10 +260,12 @@ def properties(
api_properties = self._api.backend_properties(self.name(), datetime=datetime)
if not api_properties:
return None
decode_backend_properties(api_properties)
return BackendProperties.from_dict(api_properties)

if refresh or self._properties is None:
api_properties = self._api.backend_properties(self.name())
decode_backend_properties(api_properties)
self._properties = BackendProperties.from_dict(api_properties)

return self._properties
Expand Down
2 changes: 2 additions & 0 deletions qiskit/providers/ibmq/job/ibmqjob.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from ..utils.utils import RefreshQueue, validate_job_tags
from ..utils import utc_to_local
from ..utils.qobj_utils import dict_to_qobj
from ..utils.json_decoder import decode_backend_properties
from .exceptions import (IBMQJobApiError, IBMQJobFailureError,
IBMQJobTimeoutError, IBMQJobInvalidStateError)
from .queueinfo import QueueInfo
Expand Down Expand Up @@ -206,6 +207,7 @@ def properties(self) -> Optional[BackendProperties]:
if not properties:
return None

decode_backend_properties(properties)
return BackendProperties.from_dict(properties)

def result(
Expand Down
45 changes: 34 additions & 11 deletions qiskit/providers/ibmq/utils/json_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

from typing import Dict, Union, List

import dateutil.parser


def decode_pulse_qobj(pulse_qobj: Dict) -> None:
"""Decode a pulse Qobj.
Expand All @@ -31,17 +33,6 @@ def decode_pulse_qobj(pulse_qobj: Dict) -> None:
_decode_pulse_qobj_instr(instr)


def decode_pulse_backend_config(config: Dict) -> None:
"""Decode pulse backend configuration data.
Args:
config: A ``PulseBackendConfiguration`` in dictionary format.
"""
for u_channle_list in config['u_channel_lo']:
for u_channle_lo in u_channle_list:
u_channle_lo['scale'] = _to_complex(u_channle_lo['scale'])


def decode_pulse_defaults(defaults: Dict) -> None:
"""Decode pulse defaults data.
Expand All @@ -57,6 +48,38 @@ def decode_pulse_defaults(defaults: Dict) -> None:
_decode_pulse_qobj_instr(instr)


def decode_backend_properties(properties: Dict) -> None:
"""Decode backend properties.
Args:
properties: A ``BackendProperties`` in dictionary format.
"""
properties['last_update_date'] = dateutil.parser.isoparse(properties['last_update_date'])
for qubit in properties['qubits']:
for nduv in qubit:
nduv['date'] = dateutil.parser.isoparse(nduv['date'])
for gate in properties['gates']:
for param in gate['parameters']:
param['date'] = dateutil.parser.isoparse(param['date'])
for gen in properties['general']:
gen['date'] = dateutil.parser.isoparse(gen['date'])


def decode_backend_configuration(config: Dict) -> None:
"""Decode backend configuration.
Args:
config: A ``QasmBackendConfiguration`` or ``PulseBackendConfiguration``
in dictionary format.
"""
config['online_date'] = dateutil.parser.isoparse(config['online_date'])

if 'u_channel_lo' in config:
for u_channle_list in config['u_channel_lo']:
for u_channle_lo in u_channle_list:
u_channle_lo['scale'] = _to_complex(u_channle_lo['scale'])


def _to_complex(value: Union[List[float], complex]) -> complex:
"""Convert the input value to type ``complex``.
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
nest-asyncio>=1.0.0,!=1.1.0
qiskit-terra>=0.13
qiskit-terra>=0.14
requests>=2.19
requests_ntlm>=1.1.0
websockets>=7,<8
Expand Down
101 changes: 69 additions & 32 deletions test/ibmq/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from unittest import skipIf
from typing import Any

import dateutil.parser
import qiskit
from qiskit.compiler import assemble

Expand All @@ -28,20 +29,26 @@
class TestSerialization(IBMQTestCase):
"""Test data serialization."""

@classmethod
@requires_provider
def test_qasm_qobj(self, provider):
def setUpClass(cls, provider):
"""Initial class level setup."""
# pylint: disable=arguments-differ
super().setUpClass()
cls.provider = provider

def test_qasm_qobj(self):
"""Test serializing qasm qobj data."""
backend = provider.get_backend('ibmq_qasm_simulator')
backend = self.provider.get_backend('ibmq_qasm_simulator')
qobj = bell_in_qobj(backend=backend)
job = backend.run(qobj, validate_qobj=True)
rqobj = backend.retrieve_job(job.job_id()).qobj()

self.assertEqual(_array_to_list(qobj.to_dict()), rqobj.to_dict())

@requires_provider
def test_pulse_qobj(self, provider):
def test_pulse_qobj(self):
"""Test serializing pulse qobj data."""
backends = provider.backends(operational=True, open_pulse=True)
backends = self.provider.backends(operational=True, open_pulse=True)
if not backends:
self.skipTest('Need pulse backends.')

Expand All @@ -64,52 +71,48 @@ def test_pulse_qobj(self, provider):

cancel_job(job)

@requires_provider
@skipIf(qiskit.__version__ < '0.14.0', 'Test requires terra 0.14.0')
def test_backend_configuration(self, provider):
def test_backend_configuration(self):
"""Test deserializing backend configuration."""
backends = provider.backends(operational=True, open_pulse=True)
if not backends:
self.skipTest('Need pulse backends.')
backends = self.provider.backends(operational=True, simulator=False)

# Known keys that look like a serialized complex number.
good_keys = ('coupling_map', 'qubit_lo_range', 'meas_lo_range', 'gates.coupling_map',
'meas_levels')
'meas_levels', 'qubit_channel_mapping', 'backend_version')
good_keys_prefix = ('channels',)

for backend in backends:
with self.subTest(backend=backend):
configuration = backend.configuration()
complex_keys = set()
_find_potential_complex(configuration.to_dict(), '', complex_keys)
suspect_keys = set()
_find_potential_encoded(configuration.to_dict(), '', suspect_keys)

for gkey in good_keys:
try:
complex_keys.remove(gkey)
suspect_keys.remove(gkey)
except KeyError:
pass

for gkey in good_keys_prefix:
complex_keys = {ckey for ckey in complex_keys if not ckey.startswith(gkey)}
suspect_keys = {ckey for ckey in suspect_keys if not ckey.startswith(gkey)}

self.assertFalse(complex_keys)
self.assertFalse(suspect_keys)

@requires_provider
@skipIf(qiskit.__version__ < '0.14.0', 'Test requires terra 0.14.0')
def test_pulse_defaults(self, provider):
def test_pulse_defaults(self):
"""Test deserializing backend configuration."""
backends = provider.backends(operational=True, open_pulse=True)
backends = self.provider.backends(operational=True, open_pulse=True)
if not backends:
self.skipTest('Need pulse backends.')

# Known keys that look like a serialized complex number.
good_keys = ('cmd_def.qubits',)
good_keys = ('cmd_def.qubits', 'cmd_def.sequence.ch')

for backend in backends:
with self.subTest(backend=backend):
defaults = backend.defaults()
complex_keys = set()
_find_potential_complex(defaults.to_dict(), '', complex_keys)
_find_potential_encoded(defaults.to_dict(), '', complex_keys)

for gkey in good_keys:
try:
Expand All @@ -119,26 +122,60 @@ def test_pulse_defaults(self, provider):

self.assertFalse(complex_keys)

@skipIf(qiskit.__version__ < '0.14.0', 'Test requires terra 0.14.0')
def test_backend_properties(self):
"""Test deserializing backend properties."""
backends = self.provider.backends(operational=True, simulator=False)

# Known keys that look like a serialized object.
good_keys = ('gates.qubits', 'qubits.name', 'backend_version')

def _find_potential_complex(data: Any, c_key: str, tally: set) -> None:
"""Find data that looks like serialized complex numbers.
for backend in backends:
with self.subTest(backend=backend):
properties = backend.properties()
suspect_keys = set()
_find_potential_encoded(properties.to_dict(), '', suspect_keys)

for gkey in good_keys:
try:
suspect_keys.remove(gkey)
except KeyError:
pass

self.assertFalse(suspect_keys)


def _find_potential_encoded(data: Any, c_key: str, tally: set) -> None:
"""Find data that may be in JSON serialized format.
Args:
data: Data to be recursively traversed to find potential complex numbers.
data: Data to be recursively traversed to find suspects.
c_key: Key of the field currently being traversed.
tally: Keys of fields that may contain complex numbers.
tally: Keys of fields that look suspect.
"""
if _check_encoded(data):
tally.add(c_key)

if isinstance(data, list):
if len(data) == 2 and all(isinstance(x, (float, int)) for x in data):
tally.add(c_key)
for item in data:
if isinstance(item, (dict, list)):
_find_potential_complex(item, c_key, tally)
_find_potential_encoded(item, c_key, tally)
elif isinstance(data, dict):
for key, value in data.items():
if isinstance(value, (dict, list)):
full_key = c_key + '.' + str(key) if c_key else str(key)
_find_potential_complex(value, full_key, tally)
full_key = c_key + '.' + str(key) if c_key else str(key)
_find_potential_encoded(value, full_key, tally)


def _check_encoded(data):
"""Check if the input data is potentially in JSON serialized format."""
if isinstance(data, list) and len(data) == 2 and all(isinstance(x, (float, int)) for x in data):
return True
elif isinstance(data, str):
try:
dateutil.parser.parse(data)
return True
except ValueError:
pass
return False


def _array_to_list(data):
Expand Down

0 comments on commit 864a5b6

Please sign in to comment.