Skip to content

Commit

Permalink
Serialization to the abstract representation (pasqal-io#355)
Browse files Browse the repository at this point in the history
* Abstract representation for pulses and waveforms

* Initial version of abstract seq format (pasqal-io#346)

* Preliminary AbstractReprEncoder

* added abstract_repr tests (pasqal-io#347)

* WIP: Sequence to abstract_repr conversion

* Abstract representation schema check (pasqal-io#350)

* added json schema test for abstract repr

* changed export POC to fit schema

* separate tests for abstract repr

* sequence misc cleanup and comment

* WIP: Updates to the format

* Moving custom serialization exceptions

* Handling variables in the serialization

* Format updates

* WIP: Unit tests

* Updates to the JSON schema

* Restrict export of InterpolatedWaveform

* More updates to the JSON Schema

* Adding support for new operations

* Changing how signatures are stored for abstract representation

* Adding support for `set` and `np.ndarray`

* Import sorting + myp

* Fixing `InterpolatedWaveform` export

* Preparing for merge

* Adding support for phase shifts and a sequence name

* Adding support for "delay" and updating the JSON schema

* Updates for the latest schema

* Feat: deserialize abstract repr for non param sequences (+refacto abstract serialization)

* Fix: Get rid of inline import statements + type-hint fixes

* Moving pulser.json.signatures into pulser.json.abstract_repr

* Removing `str` variable handling logic

* Feat: Deserialize parametrized objects

* Make CI run on all PRs

* Fix mypy errors

* Fix flake8 errors

* Fix format and import sorting

* Serializer support for args as kwargs

* Add function to test the serialization roundtrip

* Post-merge fixes

* Dropping usage of pos-only args due to python 3.7 conflict

* Reaching 100% coverage of the deserializer

* Updating test ids

* Fixing the path to the json schema

* Finish serializer coverage

* Adding comments + small typo fixes

Co-authored-by: Piotr Migdał <[email protected]>
Co-authored-by: MB <[email protected]>
  • Loading branch information
3 people authored Jul 21, 2022
1 parent 4696b37 commit 4592d39
Show file tree
Hide file tree
Showing 25 changed files with 2,718 additions and 50 deletions.
4 changes: 3 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ extend-ignore =
E203,
per-file-ignores =
# D100 Missing docstring in public module
# D101 Missing docstring in public class
# D102 Missing docstring in public method
# D103 Missing docstring in public function
# F401 Module imported but unused
tests/*: D100, D103
tests/*: D100, D101, D102, D103
__init__.py: F401
setup.py: D100
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ name: build

on:
pull_request:
branches:
- master
- develop
push:
branches:
- master
Expand Down
2 changes: 1 addition & 1 deletion .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ warn_unused_ignores = True
disallow_untyped_defs = True

# 3rd-party libs without type hints nor stubs
[mypy-matplotlib.*,scipy.*,qutip.*]
[mypy-matplotlib.*,scipy.*,qutip.*,jsonschema.*]
follow_imports = silent
ignore_missing_imports = true

Expand Down
1 change: 1 addition & 0 deletions pulser-core/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include README.md
include LICENSE
include pulser/devices/interaction_coefficients/C6_coeffs.json
include pulser/json/abstract_repr/schema.json
2 changes: 1 addition & 1 deletion pulser-core/pulser/devices/_device_datacls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
import numpy as np
from scipy.spatial.distance import pdist, squareform

from pulser import Pulse
from pulser.channels import Channel
from pulser.devices.interaction_coefficients import c6_dict
from pulser.json.utils import obj_to_dict
from pulser.pulse import Pulse
from pulser.register.base_register import BaseRegister, QubitId
from pulser.register.register_layout import COORD_PRECISION, RegisterLayout

Expand Down
14 changes: 14 additions & 0 deletions pulser-core/pulser/json/abstract_repr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2022 Pulser Development Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Serialization and deserialization tools for the abstract representation."""
267 changes: 267 additions & 0 deletions pulser-core/pulser/json/abstract_repr/deserializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# Copyright 2022 Pulser Development Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Deserializer from JSON in the abstract representation."""
from __future__ import annotations

import json
from pathlib import Path
from typing import TYPE_CHECKING, Any, Union, cast, overload

import jsonschema

import pulser
import pulser.devices as devices
from pulser.json.abstract_repr.signatures import (
BINARY_OPERATORS,
UNARY_OPERATORS,
)
from pulser.json.exceptions import AbstractReprError
from pulser.parametrized import ParamObj, Variable
from pulser.pulse import Pulse
from pulser.register.register import Register
from pulser.waveforms import (
BlackmanWaveform,
CompositeWaveform,
ConstantWaveform,
CustomWaveform,
InterpolatedWaveform,
KaiserWaveform,
RampWaveform,
Waveform,
)

if TYPE_CHECKING: # pragma: no cover
from pulser.sequence import Sequence

with open(Path(__file__).parent / "schema.json") as f:
schema = json.load(f)

VARIABLE_TYPE_MAP = {"int": int, "float": float}

ExpReturnType = Union[int, float, ParamObj]


@overload
def _deserialize_parameter(param: int, vars: dict[str, Variable]) -> int:
pass


@overload
def _deserialize_parameter(param: float, vars: dict[str, Variable]) -> float:
pass


@overload
def _deserialize_parameter(
param: dict[str, str], vars: dict[str, Variable]
) -> Variable:
pass


def _deserialize_parameter(
param: Union[int, float, dict[str, Any]],
vars: dict[str, Variable],
) -> Union[ExpReturnType, Variable]:
"""Deserialize a parameterized object.
A parameter can be either a literal, a variable or an expression.
In the first case, return the literal. Otherwise, return a reference
to the variable, or build an expression referencing variables.
Args:
param: The JSON parametrized object to deserialize
vars: The references to the sequence variables
Returns:
A literal (int | float), a ``Variable``, or a ``ParamObj``.
"""
if not isinstance(param, dict):
# This is a literal
return param

if "variable" in param:
# This is a reference to a variable.
if param["variable"] not in vars:
raise AbstractReprError(
f"Variable '{param['variable']}' used in operations "
"but not found in declared variables."
)
return vars[param["variable"]]

if "expression" not in param:
# Can't deserialize param if it is a dict without a
# `variable` or an `expression` key
raise AbstractReprError(
f"Parameter '{param}' is neither a literal nor "
"a variable or an expression."
)

# This is a unary or a binary expression
expression = (
param["expression"] if param["expression"] != "div" else "truediv"
)

if expression in UNARY_OPERATORS:
return cast(
ExpReturnType,
UNARY_OPERATORS[expression](
_deserialize_parameter(param["lhs"], vars)
),
)
elif expression in BINARY_OPERATORS:
return cast(
ExpReturnType,
BINARY_OPERATORS[expression](
_deserialize_parameter(param["lhs"], vars),
_deserialize_parameter(param["rhs"], vars),
),
)
else:
raise AbstractReprError(f"Expression '{param['expression']}' invalid.")


def _deserialize_waveform(obj: dict, vars: dict) -> Waveform:

if obj["kind"] == "constant":
return ConstantWaveform(
duration=_deserialize_parameter(obj["duration"], vars),
value=_deserialize_parameter(obj["value"], vars),
)
if obj["kind"] == "ramp":
return RampWaveform(
duration=_deserialize_parameter(obj["duration"], vars),
start=_deserialize_parameter(obj["start"], vars),
stop=_deserialize_parameter(obj["stop"], vars),
)
if obj["kind"] == "blackman":
return BlackmanWaveform(
duration=_deserialize_parameter(obj["duration"], vars),
area=_deserialize_parameter(obj["area"], vars),
)
if obj["kind"] == "blackman_max":
return BlackmanWaveform.from_max_val(
max_val=_deserialize_parameter(obj["max_val"], vars),
area=_deserialize_parameter(obj["area"], vars),
)
if obj["kind"] == "interpolated":
return InterpolatedWaveform(
duration=_deserialize_parameter(obj["duration"], vars),
values=_deserialize_parameter(obj["values"], vars),
times=_deserialize_parameter(obj["times"], vars),
)
if obj["kind"] == "kaiser":
return KaiserWaveform(
duration=_deserialize_parameter(obj["duration"], vars),
area=_deserialize_parameter(obj["area"], vars),
beta=_deserialize_parameter(obj["beta"], vars),
)
if obj["kind"] == "kaiser_max":
return KaiserWaveform.from_max_val(
max_val=_deserialize_parameter(obj["max_val"], vars),
area=_deserialize_parameter(obj["area"], vars),
beta=_deserialize_parameter(obj["beta"], vars),
)
if obj["kind"] == "composite":
wfs = [_deserialize_waveform(wf, vars) for wf in obj["waveforms"]]
return CompositeWaveform(*wfs)
if obj["kind"] == "custom":
return CustomWaveform(
samples=_deserialize_parameter(obj["samples"], vars)
)

raise AbstractReprError("The object does not encode a known waveform.")


def _deserialize_operation(seq: Sequence, op: dict, vars: dict) -> None:
if op["op"] == "target":
seq.target_index(
qubits=_deserialize_parameter(op["target"], vars),
channel=op["channel"],
)
elif op["op"] == "align":
seq.align(*op["channels"])
elif op["op"] == "delay":
seq.delay(
duration=_deserialize_parameter(op["time"], vars),
channel=op["channel"],
)
elif op["op"] == "phase_shift":
seq.phase_shift_index(
_deserialize_parameter(op["phi"], vars),
*[_deserialize_parameter(t, vars) for t in op["targets"]],
)
elif op["op"] == "pulse":
pulse = Pulse(
amplitude=_deserialize_waveform(op["amplitude"], vars),
detuning=_deserialize_waveform(op["detuning"], vars),
phase=_deserialize_parameter(op["phase"], vars),
post_phase_shift=_deserialize_parameter(
op["post_phase_shift"], vars
),
)
seq.add(
pulse=pulse,
channel=op["channel"],
protocol=op["protocol"],
)


def deserialize_abstract_sequence(obj_str: str) -> Sequence:
"""Deserialize a sequence from an abstract JSON object.
Args:
obj_str: the JSON string representing the sequence encoded
in the abstract JSON format.
Returns:
Sequence: The Pulser sequence.
"""
obj = json.loads(obj_str)

# Validate the format of the data against the JSON schema.
jsonschema.validate(instance=obj, schema=schema)

# Device
device_name = obj["device"]
device = getattr(devices, device_name)

# Register
qubits = obj["register"]
reg = Register({q["name"]: (q["x"], q["y"]) for q in qubits})

seq = pulser.Sequence(reg, device)

# Channels
for name, channel_id in obj["channels"].items():
seq.declare_channel(name, channel_id)

# Variables
vars = {}
for name, desc in obj["variables"].items():
v = seq.declare_variable(
cast(str, name),
size=len(desc["value"]),
dtype=VARIABLE_TYPE_MAP[desc["type"]],
)
vars[name] = v

# Operations
for op in obj["operations"]:
_deserialize_operation(seq, op, vars)

# Measurement
if obj["measurement"] is not None:
seq.measure(obj["measurement"])

return seq
Loading

0 comments on commit 4592d39

Please sign in to comment.