Skip to content
This repository was archived by the owner on Oct 31, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
6b1afb0
starting point - bringing parser files over from Aer
DanPuzzuoli Nov 11, 2021
3689090
adding hamiltonian_model as well as it does some of the parsing
DanPuzzuoli Nov 11, 2021
58679be
added file and sketched signature for desired function
DanPuzzuoli Nov 11, 2021
8ffc260
trying things out - initial success with multiplying string coefficie…
DanPuzzuoli Nov 15, 2021
16a7419
added error raising if more than one channel is specified, or if chan…
DanPuzzuoli Nov 15, 2021
10e7cf6
added operator merging and channel sorting step, and added tests for …
DanPuzzuoli Nov 16, 2021
396b60a
adding some documentation and removing the old hamiltonian_model file…
DanPuzzuoli Nov 16, 2021
dca82ae
saving
DanPuzzuoli Nov 17, 2021
65cd233
initial tests passing with stripped down version of individual string…
DanPuzzuoli Nov 17, 2021
3c1f830
added doc string from operator_from_string giving explicit descriptio…
DanPuzzuoli Nov 17, 2021
0f9a4c5
saving
DanPuzzuoli Nov 19, 2021
dc328cf
Merge branch 'main' into pulse_string_parser
DanPuzzuoli Dec 10, 2021
fd4a577
converting output aggregated channel list to lower case
DanPuzzuoli Dec 10, 2021
a2f4eca
expanded valid channel characters and added test cases
DanPuzzuoli Dec 10, 2021
7182fd3
added more complicated test case and discovered issue with logic for …
DanPuzzuoli Dec 11, 2021
b960d16
restructured validation checks to allow _SUM strings
DanPuzzuoli Dec 13, 2021
c1f31d6
deleting unused old files
DanPuzzuoli Dec 13, 2021
a59be2f
fully deleting all old files
DanPuzzuoli Dec 13, 2021
c934da4
intermediate linting
DanPuzzuoli Dec 13, 2021
9da584b
further updating validation, and adding documentation note for _SUM[x…
DanPuzzuoli Dec 14, 2021
7e5ed5d
about to try noise parsing
DanPuzzuoli Dec 14, 2021
d17f77b
adding noise parsing function and deleting old noise parser object
DanPuzzuoli Dec 14, 2021
d61e507
small changes
DanPuzzuoli Dec 14, 2021
3394501
moving files around
DanPuzzuoli Jan 4, 2022
6d5cdc7
attempting to simplify and linting
DanPuzzuoli Jan 4, 2022
1291532
adding examples to documentation
DanPuzzuoli Jan 4, 2022
7ff2d17
adding parse_hamiltonian_dict to pulse module documentation
DanPuzzuoli Jan 4, 2022
89ed4db
Merge branch 'main' into pulse_string_parser
DanPuzzuoli Jan 5, 2022
4e30d01
updating parse_hamiltonian_dict to also output subsystem labels/dimen…
DanPuzzuoli Jan 14, 2022
32bb596
Merge branch 'main' into pulse_string_parser
DanPuzzuoli Jan 19, 2022
dc8ea0f
Merge branch 'main' into pulse_string_parser
DanPuzzuoli Feb 8, 2022
327d05d
Adding acquire channels to string parser
DanPuzzuoli Feb 8, 2022
eb6d23f
fixing typos and adding tests addressing PR comments
DanPuzzuoli Feb 8, 2022
cee451a
formatting
DanPuzzuoli Feb 8, 2022
c26ffcf
correcting test comment
DanPuzzuoli Feb 8, 2022
6b647b2
Merge branch 'main' into pulse_string_parser
DanPuzzuoli Feb 11, 2022
ad74ed2
updating formatting after merging main
DanPuzzuoli Feb 11, 2022
7e1dea3
addressing most review comments
DanPuzzuoli Feb 11, 2022
524c7ce
Merge branch 'main' into pulse_string_parser
DanPuzzuoli Feb 11, 2022
679ba7e
updating operator_from_String to utilize quantum_info
DanPuzzuoli Feb 16, 2022
ba4a35b
Merge branch 'pulse_string_parser' of github.com:DanPuzzuoli/qiskit-d…
DanPuzzuoli Feb 16, 2022
8ce25bd
renaming various folders, files, and functions
DanPuzzuoli Feb 16, 2022
9591fd9
Merge branch 'main' into pulse_string_parser
DanPuzzuoli Feb 16, 2022
9fb2c31
Merge branch 'main' into pulse_string_parser
DanPuzzuoli Aug 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions qiskit_dynamics/pulse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
:toctree: ../stubs/

InstructionToSignals
parse_hamiltonian_dict
"""

from .pulse_to_signals import InstructionToSignals
from .string_model_parser.string_model_parser import parse_hamiltonian_dict

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function supposed to be directly called by a user? I thought it was more an internal function that is planned to be used by some yet-to-be-made interface for building models from ibm backend configs?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I'll remove this.

17 changes: 17 additions & 0 deletions qiskit_dynamics/pulse/string_model_parser/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# 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.

"""
String model parser functionality
"""
355 changes: 355 additions & 0 deletions qiskit_dynamics/pulse/string_model_parser/legacy_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2018, 2019, 2020, 2021, 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# 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=invalid-name

"""Legacy code for parsing operator strings."""

import re
import copy
from collections import namedtuple, OrderedDict
from .operator_from_string import operator_from_string, apply_func


def legacy_parser(operator_str, subsystem_dims, subsystem_list):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this called legacy_parser? To me legacy implies there is a newer version that that this is planned to be removed at some point

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should have type hints and doc string to say what is expected for arg types, and the return

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using "legacy" here to imply that it's a non-trivial piece of code that is being preserved but not "maintained" per se. I like the usage of "legacy" to imply this - I think the way you're interpreting it is basically synonymous with "expected to be deprecated".

That all being said I'll change this if it's a sticking point for you.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the function to have type hints.

"""Function wrapper for legacy parsing object."""

system = HamiltonianParser(h_str=operator_str, subsystem_dims=subsystem_dims)
system.parse(subsystem_list)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of having parse modify the internal state of the class and compiled return something, rather than just having parse return the desired output like:

return system.parse(subsystem_list)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to return as suggested and simplified legacy_parser function to one line.


return system.compiled


Token = namedtuple("Token", ("type", "name"))

str_elements = OrderedDict(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is only used by HamiltonianParser it should be class variable of that class, no need for global scope. Same for Token

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this into the class.

QubOpr=re.compile(r"(?P<opr>O|Sp|Sm|X|Y|Z|I)(?P<idx>[0-9]+)"),
PrjOpr=re.compile(r"P(?P<idx>[0-9]+),(?P<ket>[0-9]+),(?P<bra>[0-9]+)"),
CavOpr=re.compile(r"(?P<opr>A|C|N)(?P<idx>[0-9]+)"),
Func=re.compile(r"(?P<name>[a-z]+)\("),
Ext=re.compile(r"\.(?P<name>dag)"),
Var=re.compile(r"[a-z]+[0-9]*"),
Num=re.compile(r"[0-9.]+"),
MathOrd0=re.compile(r"[*/]"),
MathOrd1=re.compile(r"[+-]"),
BrkL=re.compile(r"\("),
BrkR=re.compile(r"\)"),
)


class HamiltonianParser:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any need for this to be a class instead of a giant function? If the only way you use it is via a legacy_parser call you could just merge the init + parse + compiled into a single function since there is no reason to save state in an objects variables, all those can just be local variables in the functions scope

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might also want to call this _HamiltonianParser just to add further indication this shouldn't be used and is internal only

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason - this is part of the "legacy" aspect of it and why I wrote the legacy_parser function. Turning it directly into a function was non-trivial given the handling of internal state. The legacy_parser function was added to wrap this in the preferred standard function interface.

"""Legacy object for parsing string specifications of Hamiltonians."""

def __init__(self, h_str, subsystem_dims):
"""Create new quantum operator generator

Parameters:
h_str (list): list of Hamiltonian string
subsystem_dims (dict): dimension of subsystems
"""
self.h_str = h_str
self.subsystem_dims = {int(label): int(dim) for label, dim in subsystem_dims.items()}
self.__td_hams = []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why double underscore?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason, it was in the original code. I've changed it to just td_hams.

self.__tc_hams = []
self.__str2qopr = {}

@property
def compiled(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property doesnt seem necessasry if you add a return to parse

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - removed.

"""Return Hamiltonian terms."""
return self.__tc_hams + self.__td_hams

def parse(self, qubit_list=None):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to store __td_hams and __tc_hams as internal state? Can they just be local variables build and returned by this function as tc_hams + td_hams to aovid the need to also call compiled

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - just removed the compiled property and made them local variables.

"""Parse and generate Hamiltonian terms."""
self.__td_hams = []
self.__tc_hams = []

# expand sum
self._expand_sum()

# convert to reverse Polish notation
for ham in self.h_str:
if len(re.findall(r"\|\|", ham)) > 1:
raise Exception("Multiple time-dependent terms in %s" % ham)
p_td = re.search(r"(?P<opr>[\S]+)\|\|(?P<ch>[\S]+)", ham)

# find time-dependent term
if p_td:
coef, token = self._tokenizer(p_td.group("opr"), qubit_list)
if token is None:
continue
# combine coefficient to time-dependent term
if coef:
td = "*".join([coef, p_td.group("ch")])
else:
td = p_td.group("ch")
token = self._shunting_yard(token)
_td = self._token2qobj(token), td

self.__td_hams.append(_td)
else:
coef, token = self._tokenizer(ham, qubit_list)
if token is None:
continue
token = self._shunting_yard(token)

if (coef == "") or (coef is None):
coef = "1."

_tc = self._token2qobj(token), coef

self.__tc_hams.append(_tc)

def _expand_sum(self):
"""Takes a string-based Hamiltonian list and expands the _SUM action items out."""
sum_str = re.compile(r"_SUM\[(?P<itr>[a-z]),(?P<l>[a-z\d{}+-]+),(?P<u>[a-z\d{}+-]+),")
brk_str = re.compile(r"]")

ham_list = copy.copy(self.h_str)
ham_out = []

while any(ham_list):
ham = ham_list.pop(0)
p_sums = list(sum_str.finditer(ham))
p_brks = list(brk_str.finditer(ham))
if len(p_sums) != len(p_brks):
raise Exception("Missing correct number of brackets in %s" % ham)

# find correct sum-bracket correspondence
if any(p_sums) == 0:
ham_out.append(ham)
else:
itr = p_sums[0].group("itr")
_l = int(p_sums[0].group("l"))
_u = int(p_sums[0].group("u"))
for ii in range(len(p_sums) - 1):
if p_sums[ii + 1].end() > p_brks[ii].start():
break
else:
ii = len(p_sums) - 1

# substitute iterator value
_temp = []
for kk in range(_l, _u + 1):
trg_s = ham[p_sums[0].end() : p_brks[ii].start()]
# generate replacement pattern
pattern = {}
for p in re.finditer(r"\{(?P<op_str>[a-z0-9*/+-]+)\}", trg_s):
if p.group() not in pattern:
sub = parse_binop(p.group("op_str"), operands={itr: str(kk)})
if sub.isdecimal():
pattern[p.group()] = sub
else:
pattern[p.group()] = "{%s}" % sub
for key, val in pattern.items():
trg_s = trg_s.replace(key, val)
_temp.append(
"".join([ham[: p_sums[0].start()], trg_s, ham[p_brks[ii].end() :]])
)
ham_list.extend(_temp)

self.h_str = ham_out

return ham_out

def _tokenizer(self, op_str, qubit_list=None):
"""Convert string to token and coefficient
Check if the index is in qubit_list
"""

# generate token
_op_str = copy.copy(op_str)
token_list = []
prev = "none"
while any(_op_str):
for key, parser in str_elements.items():
p = parser.match(_op_str)
if p:
# find quantum operators
if key in ["QubOpr", "CavOpr"]:
_key = key
_name = p.group()
if p.group() not in self.__str2qopr.keys():
idx = int(p.group("idx"))
if qubit_list is not None and idx not in qubit_list:
return 0, None
name = p.group("opr")
opr = operator_from_string(name, idx, self.subsystem_dims)
self.__str2qopr[p.group()] = opr
elif key == "PrjOpr":
_key = key
_name = p.group()
if p.group() not in self.__str2qopr.keys():
idx = int(p.group("idx"))
name = "P"
opr = operator_from_string(name, idx, self.subsystem_dims)
self.__str2qopr[p.group()] = opr
elif key in ["Func", "Ext"]:
_name = p.group("name")
_key = key
elif key == "MathOrd1":
_name = p.group()
if prev not in ["QubOpr", "PrjOpr", "CavOpr", "Var", "Num"]:
_key = "MathUnitary"
else:
_key = key
else:
_name = p.group()
_key = key
token_list.append(Token(_key, _name))
_op_str = _op_str[p.end() :]
prev = _key
break
else:
raise Exception("Invalid input string %s is found" % op_str)

# split coefficient
coef = ""
if any(k.type == "Var" for k in token_list):
for ii, _ in enumerate(token_list):
if token_list[ii].name == "*":
if all(k.type != "Var" for k in token_list[ii + 1 :]):
coef = "".join([k.name for k in token_list[:ii]])
token_list = token_list[ii + 1 :]
break
else:
raise Exception("Invalid order of operators and coefficients in %s" % op_str)

return coef, token_list

def _shunting_yard(self, token_list):
"""Reformat token to reverse Polish notation"""
stack = []
queue = []
while any(token_list):
token = token_list.pop(0)
if token.type in ["QubOpr", "PrjOpr", "CavOpr", "Num"]:
queue.append(token)
elif token.type in ["Func", "Ext"]:
stack.append(token)
elif token.type in ["MathUnitary", "MathOrd0", "MathOrd1"]:
while stack and math_priority(token, stack[-1]):
queue.append(stack.pop(-1))
stack.append(token)
elif token.type in ["BrkL"]:
stack.append(token)
elif token.type in ["BrkR"]:
while stack[-1].type not in ["BrkL", "Func"]:
queue.append(stack.pop(-1))
if not any(stack):
raise Exception("Missing correct number of brackets")
pop = stack.pop(-1)
if pop.type == "Func":
queue.append(pop)
else:
raise Exception("Invalid token %s is found" % token.name)

while any(stack):
queue.append(stack.pop(-1))

return queue

def _token2qobj(self, tokens):
"""Generate Hamiltonian term from tokens."""
stack = []
for token in tokens:
if token.type in ["QubOpr", "PrjOpr", "CavOpr"]:
stack.append(self.__str2qopr[token.name])
elif token.type == "Num":
stack.append(float(token.name))
elif token.type in ["MathUnitary"]:
if token.name == "-":
stack.append(-stack.pop(-1))
elif token.type in ["MathOrd0", "MathOrd1"]:
op2 = stack.pop(-1)
op1 = stack.pop(-1)
if token.name == "+":
stack.append(op1 + op2)
elif token.name == "-":
stack.append(op1 - op2)
elif token.name == "*":
stack.append(op1 @ op2)
elif token.name == "/":
stack.append(op1 / op2)
elif token.type in ["Func", "Ext"]:
stack.append(apply_func(token.name, stack.pop(-1)))
else:
raise Exception("Invalid token %s is found" % token.name)

if len(stack) > 1:
raise Exception("Invalid mathematical operation in " % tokens)

return stack[0]


def math_priority(o1, o2):
"""Check priority of given math operation"""
rank = {"MathUnitary": 2, "MathOrd0": 1, "MathOrd1": 0}
diff_ops = rank.get(o1.type, -1) - rank.get(o2.type, -1)

if diff_ops > 0:
return False
else:
return True


# pylint: disable=dangerous-default-value
def parse_binop(op_str, operands={}, cast_str=True):
"""Calculate binary operation in string format"""
oprs = OrderedDict(
sum=r"(?P<v0>[a-zA-Z0-9]+)\+(?P<v1>[a-zA-Z0-9]+)",
sub=r"(?P<v0>[a-zA-Z0-9]+)\-(?P<v1>[a-zA-Z0-9]+)",
mul=r"(?P<v0>[a-zA-Z0-9]+)\*(?P<v1>[a-zA-Z0-9]+)",
div=r"(?P<v0>[a-zA-Z0-9]+)\/(?P<v1>[a-zA-Z0-9]+)",
non=r"(?P<v0>[a-zA-Z0-9]+)",
)

for key, regr in oprs.items():
p = re.match(regr, op_str)
if p:
val0 = operands.get(p.group("v0"), p.group("v0"))
if key == "non":
# substitution
retv = val0
else:
val1 = operands.get(p.group("v1"), p.group("v1"))
# binary operation
if key == "sum":
if val0.isdecimal() and val1.isdecimal():
retv = int(val0) + int(val1)
else:
retv = "+".join([str(val0), str(val1)])
elif key == "sub":
if val0.isdecimal() and val1.isdecimal():
retv = int(val0) - int(val1)
else:
retv = "-".join([str(val0), str(val1)])
elif key == "mul":
if val0.isdecimal() and val1.isdecimal():
retv = int(val0) * int(val1)
else:
retv = "*".join([str(val0), str(val1)])
elif key == "div":
if val0.isdecimal() and val1.isdecimal():
retv = int(val0) / int(val1)
else:
retv = "/".join([str(val0), str(val1)])
else:
retv = 0
break
else:
raise Exception("Invalid string %s" % op_str)

if cast_str:
return str(retv)
else:
return retv
Loading