From d91d67f784d9717731522343eb28de515c8f7ab8 Mon Sep 17 00:00:00 2001 From: pcwysoc <144378483+pcwysoc@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:38:04 -0500 Subject: [PATCH 1/7] bugfix for germ selection and qilgst problem --- pygsti/algorithms/germselection.py | 15 ++++++++++++++- pygsti/processors/processorspec.py | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pygsti/algorithms/germselection.py b/pygsti/algorithms/germselection.py index 48957b90e..82d087a7e 100644 --- a/pygsti/algorithms/germselection.py +++ b/pygsti/algorithms/germselection.py @@ -401,7 +401,20 @@ def find_germs(target_model, randomize=True, randomization_strength=1e-2, raise ValueError("'{}' is not a valid algorithm " "identifier.".format(algorithm)) - return germList + #force the line labels on each circuit to match the state space labels for the target model. + #this is suboptimal for many-qubit models, so will probably want to revisit this. #TODO + finalGermList = [] + for ckt in germList: + if ckt._static: + new_ckt = ckt.copy(editable=True) + new_ckt.line_labels = target_model.state_space.state_space_labels + print(new_ckt.line_labels) + new_ckt.done_editing() + finalGermList.append(new_ckt) + else: + ckt.line_labels = target_model.state_space.state_space_labels + finalGermList.append(ckt) + return finalGermList def compute_germ_set_score(germs, target_model=None, neighborhood=None, diff --git a/pygsti/processors/processorspec.py b/pygsti/processors/processorspec.py index d5c022b6f..b7d4a04d6 100644 --- a/pygsti/processors/processorspec.py +++ b/pygsti/processors/processorspec.py @@ -405,8 +405,8 @@ def instrument_specifier(self, name): ------- str or dict """ - if name in self.nonstd_instruments: - return self.nonstd_instruments[name] + if tuple(name) in self.nonstd_instruments.keys(): + return self.nonstd_instruments[tuple(name)] else: # assert(is_standard_instrument_name(name)) TODO return name From 1181862ebe82b87b245c18497566985a5c63ec50 Mon Sep 17 00:00:00 2001 From: Corey Ostrove Date: Mon, 16 Dec 2024 20:41:18 -0700 Subject: [PATCH 2/7] Change default ForwardSimulator casting Changes the default behavior of casting with the 'auto' keyword to use the map forward simulator. Update germ selection to automatically convert to a matrix forward simulator as needed. --- pygsti/algorithms/germselection.py | 17 +++++++++++++---- pygsti/forwardsims/forwardsim.py | 4 +--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pygsti/algorithms/germselection.py b/pygsti/algorithms/germselection.py index 48957b90e..5efeb0fd4 100644 --- a/pygsti/algorithms/germselection.py +++ b/pygsti/algorithms/germselection.py @@ -27,6 +27,7 @@ from pygsti.baseobjs.statespace import ExplicitStateSpace as _ExplicitStateSpace from pygsti.baseobjs.statespace import QuditSpace as _QuditSpace from pygsti.models import ExplicitOpModel as _ExplicitOpModel +from pygsti.forwardsims import MatrixForwardSimulator as _MatrixForwardSimulator FLOATSIZE = 8 # in bytes: TODO: a better way @@ -57,10 +58,8 @@ def find_germs(target_model, randomize=True, randomization_strength=1e-2, Parameters ---------- - target_model : Model or list of Model - The model you are aiming to implement, or a list of models that are - copies of the model you are trying to implement (either with or - without random unitary perturbations applied to the models). + target_model : Model + The model you are aiming to implement. randomize : bool, optional Whether or not to add random unitary perturbations to the model(s) @@ -188,8 +187,14 @@ def find_germs(target_model, randomize=True, randomization_strength=1e-2, A list containing the germs making up the germ set. """ printer = _baseobjs.VerbosityPrinter.create_printer(verbosity, comm) + + if not isinstance(target_model.sim, _MatrixForwardSimulator): + target_model = target_model.copy() + target_model.sim = 'matrix' + modelList = _setup_model_list(target_model, randomize, randomization_strength, num_gs_copies, seed) + gates = list(target_model.operations.keys()) availableGermsList = [] if candidate_germ_counts is None: candidate_germ_counts = {6: 'all upto'} @@ -1351,6 +1356,10 @@ def test_germ_set_finitel(model, germs_to_test, length, weights=None, eigenvalues (from small to large) of the jacobian^T * jacobian matrix used to determine parameter amplification. """ + if not isinstance(model.sim, _MatrixForwardSimulator): + model = model.copy() + model.sim = 'matrix' + # Remove any SPAM vectors from model since we only want # to consider the set of *gate* parameters for amplification # and this makes sure our parameter counting is correct diff --git a/pygsti/forwardsims/forwardsim.py b/pygsti/forwardsims/forwardsim.py index 2ae19f2f3..727cffa8f 100644 --- a/pygsti/forwardsims/forwardsim.py +++ b/pygsti/forwardsims/forwardsim.py @@ -63,9 +63,7 @@ def cast(cls, obj : ForwardSimulator.Castable, num_qubits=None): if isinstance(obj, ForwardSimulator): return obj elif isinstance(obj, str): - if obj == "auto": - return _MapFSim() if (num_qubits is None or num_qubits > 2) else _MatrixFSim() - elif obj == "map": + if obj == "auto" or obj == "map": return _MapFSim() elif obj == "matrix": return _MatrixFSim() From 332c2938915414a8093b5be8386d53309f178b1a Mon Sep 17 00:00:00 2001 From: Corey Ostrove Date: Mon, 16 Dec 2024 20:43:42 -0700 Subject: [PATCH 3/7] Fix broken unit tests Fix tests that were broken by the switch to using map as the default forward simulator. Either update tests to support map natively, or for ones that rely on matrix make sure that is the simulator being used. --- .../algorithms/test_germselection.py | 16 ++++++++++++---- test/test_packages/objects/test_evaltree.py | 4 ++++ test/test_packages/objects/test_gatesets.py | 19 +++++++++++-------- .../test_packages/objects/test_instruments.py | 2 ++ test/unit/algorithms/fixtures.py | 4 ++-- test/unit/objects/test_forwardsim.py | 13 ++++++++----- test/unit/objects/test_model.py | 1 + test/unit/objects/test_objectivefns.py | 1 + 8 files changed, 41 insertions(+), 19 deletions(-) diff --git a/test/test_packages/algorithms/test_germselection.py b/test/test_packages/algorithms/test_germselection.py index eeb7ff867..99f021400 100644 --- a/test/test_packages/algorithms/test_germselection.py +++ b/test/test_packages/algorithms/test_germselection.py @@ -121,7 +121,9 @@ def test_germsel_greedy(self): threshold = 1e6 randomizationStrength = 1e-3 neighborhoodSize = 2 - gatesetNeighborhood = pygsti.alg.randomize_model_list([std.target_model()], + model = std.target_model() + model.sim = 'matrix' + gatesetNeighborhood = pygsti.alg.randomize_model_list([model], randomization_strength=randomizationStrength, num_copies=neighborhoodSize, seed=2014) @@ -141,7 +143,9 @@ def test_germsel_greedy(self): def test_germsel_driver_greedy(self): #GREEDY options = {'threshold': 1e6 } - germs = pygsti.alg.find_germs(std.target_model(), randomize=True, randomization_strength=1e-3, + model = std.target_model() + model.sim = 'matrix' + germs = pygsti.alg.find_germs(model, randomize=True, randomization_strength=1e-3, num_gs_copies=2, seed=2017, candidate_germ_counts={3: 'all upto', 4: 10, 5:10, 6:10}, candidate_seed=2017, force="singletons", algorithm='greedy', algorithm_kwargs=options, mem_limit=None, comm=None, @@ -152,7 +156,9 @@ def test_germsel_driver_greedy(self): def test_germsel_driver_grasp(self): #more args options = {'threshold': 1e6 , 'return_all': True} - germs = pygsti.alg.find_germs(std.target_model(), randomize=True, randomization_strength=1e-3, + model = std.target_model() + model.sim = 'matrix' + germs = pygsti.alg.find_germs(model, randomize=True, randomization_strength=1e-3, num_gs_copies=2, seed=2017, candidate_germ_counts={3: 'all upto', 4: 10, 5:10, 6:10}, candidate_seed=2017, force="singletons", algorithm='grasp', algorithm_kwargs=options, mem_limit=None, @@ -166,7 +172,9 @@ def test_germsel_driver_grasp(self): def test_germsel_driver_slack(self): #SLACK options = dict(fixed_slack=False, slack_frac=0.1) - germs = pygsti.alg.find_germs(std.target_model(), randomize=True, randomization_strength=1e-3, + model = std.target_model() + model.sim = 'matrix' + germs = pygsti.alg.find_germs(model, randomize=True, randomization_strength=1e-3, num_gs_copies=2, seed=2017, candidate_germ_counts={3: 'all upto', 4: 10, 5:10, 6:10}, candidate_seed=2017, force="singletons", algorithm='slack', algorithm_kwargs=options, mem_limit=None, comm=None, diff --git a/test/test_packages/objects/test_evaltree.py b/test/test_packages/objects/test_evaltree.py index b1b1f5967..27a49db52 100644 --- a/test/test_packages/objects/test_evaltree.py +++ b/test/test_packages/objects/test_evaltree.py @@ -14,6 +14,9 @@ def setUp(self): self.circuits = pygsti.circuits.to_circuits(["Gxpi2:0", "Gypi2:0", "Gxpi2:0Gxpi2:0", "Gypi2:0Gypi2:0", "Gxpi2:0Gypi2:0"]) self.model = smq1Q_XY.target_model() + model_matrix = self.model.copy() + model_matrix.sim = 'map' + self.model_matrix = model_matrix def _test_layout(self, layout): self.assertEqual(layout.num_elements, len(self.circuits) * 2) # 2 outcomes per circuit @@ -169,6 +172,7 @@ def test_matrix_layout(self): # for i in range(4): #then number of original strings (num final strings) # self.assertArraysAlmostEqual(probs[mlookup[i]], split_probs[mlookup_splt[i]]) + self._test_layout(pygsti.layouts.matrixlayout.MatrixCOPALayout(self.circuits[:], self.model_matrix)) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/test/test_packages/objects/test_gatesets.py b/test/test_packages/objects/test_gatesets.py index b0ea5409a..6c63aa95a 100644 --- a/test/test_packages/objects/test_gatesets.py +++ b/test/test_packages/objects/test_gatesets.py @@ -41,16 +41,17 @@ def setUp(self): self.model = pygsti.models.modelconstruction.create_explicit_model_from_expressions( [('Q0',)],['Gi','Gx','Gy'], [ "I(Q0)","X(pi/8,Q0)", "Y(pi/8,Q0)"]) - + self.model.sim = 'matrix' self.tp_gateset = pygsti.models.modelconstruction.create_explicit_model_from_expressions( [('Q0',)],['Gi','Gx','Gy'], [ "I(Q0)","X(pi/8,Q0)", "Y(pi/8,Q0)"], gate_type="full TP") - + self.tp_gateset.sim = 'matrix' self.static_gateset = pygsti.models.modelconstruction.create_explicit_model_from_expressions( [('Q0',)],['Gi','Gx','Gy'], [ "I(Q0)","X(pi/8,Q0)", "Y(pi/8,Q0)"], gate_type="static") + self.static_gateset.sim = 'matrix' self.mgateset = self.model.copy() #self.mgateset._calcClass = MapForwardSimulator @@ -388,16 +389,18 @@ def test_ondemand_probabilities(self): self.assertEqual(ds[()]['2'], 0) # but we can query '2' since it's a valid outcome label gstrs = list(ds.keys()) - layout = std1Q_XYI.target_model().sim.create_layout(gstrs, dataset=ds) + model = std1Q_XYI.target_model() + model.sim = 'map' + layout = model.sim.create_layout(gstrs, dataset=ds) self.assertEqual(layout.outcomes(()), (('1',),) ) - self.assertEqual(layout.outcomes(('Gx',)), (('1',), ('0',)) ) # '1' comes first because it's the first outcome to appear - self.assertEqual(layout.outcomes(('Gx','Gy')), (('1',), ('0',)) ) + self.assertTrue(layout.outcomes(('Gx',))==(('1',), ('0',)) or layout.outcomes(('Gx',))==(('0',), ('1',))) + self.assertTrue(layout.outcomes(('Gx','Gy'))==(('1',), ('0',)) or layout.outcomes(('Gx','Gy'))==(('0',), ('1',))) self.assertEqual(layout.outcomes(('Gx',)*4), (('0',),) ) - self.assertEqual(layout.indices(()), slice(0, 1, None)) - self.assertArraysEqual(layout.indices(('Gx',)), [1,3] ) - self.assertArraysEqual(layout.indices(('Gx','Gy')), [2,4] ) + self.assertEqual(layout.indices(()), slice(0, 1, None)) + self.assertEqual(layout.indices(('Gx',)), slice(1, 3, None)) + self.assertEqual(layout.indices(('Gx','Gy')), slice(3, 5, None)) self.assertEqual(layout.indices(('Gx',)*4), slice(5, 6, None)) self.assertEqual(layout.num_elements, 6) diff --git a/test/test_packages/objects/test_instruments.py b/test/test_packages/objects/test_instruments.py index f1ab412c4..caab0997c 100644 --- a/test/test_packages/objects/test_instruments.py +++ b/test/test_packages/objects/test_instruments.py @@ -14,6 +14,7 @@ class InstrumentTestCase(BaseTestCase): def setUp(self): #Add an instrument to the standard target model self.target_model = std.target_model() + self.target_model.sim = 'matrix' E = self.target_model.povms['Mdefault']['0'] Erem = self.target_model.povms['Mdefault']['1'] Gmz_plus = np.dot(E,E.T) @@ -176,6 +177,7 @@ def testBasicGatesetOps(self): [ "I(Q0)","X(pi/8,Q0)", "Y(pi/8,Q0)"]) # prep_labels=["rho0"], prep_expressions=["0"], # effect_labels=["0","1"], effect_expressions=["0","complement"]) + model.sim= 'matrix' v0 = modelconstruction.create_spam_vector("0", "Q0", "pp") v1 = modelconstruction.create_spam_vector("1", "Q0", "pp") diff --git a/test/unit/algorithms/fixtures.py b/test/unit/algorithms/fixtures.py index a262c52de..a62ce3771 100644 --- a/test/unit/algorithms/fixtures.py +++ b/test/unit/algorithms/fixtures.py @@ -6,8 +6,8 @@ from ..util import Namespace ns = Namespace() -ns.fullTP_model = std.target_model('full TP') -ns.model = std.target_model() +ns.fullTP_model = std.target_model('full TP', simulator='matrix') +ns.model = std.target_model(simulator='matrix') ns.opLabels = list(ns.model.operations.keys()) ns.prep_fids = std.prep_fiducials() ns.meas_fids = std.meas_fiducials() diff --git a/test/unit/objects/test_forwardsim.py b/test/unit/objects/test_forwardsim.py index 5af9aa598..5c608baee 100644 --- a/test/unit/objects/test_forwardsim.py +++ b/test/unit/objects/test_forwardsim.py @@ -63,9 +63,12 @@ def setUpClass(cls): [('Q0',)], ['Gi', 'Gx', 'Gy'], ["I(Q0)", "X(pi/8,Q0)", "Y(pi/8,Q0)"] ) + cls.model_matrix = cls.model.copy() + cls.model_matrix.sim = 'matrix' def setUp(self): self.fwdsim = self.model.sim + self.fwdsim_matrix = self.model_matrix.sim self.layout = self.fwdsim.create_layout([('Gx',), ('Gx', 'Gx')], array_types=('e', 'ep', 'epp')) self.nP = self.model.num_params self.nEls = self.layout.num_elements @@ -116,13 +119,13 @@ def test_iter_hprobs_by_rectangle(self): class MatrixForwardSimTester(ForwardSimBase, BaseCase): def test_doperation(self): - dg = self.fwdsim._doperation(L('Gx'), flat=False) - dgflat = self.fwdsim._doperation(L('Gx'), flat=True) + dg = self.fwdsim_matrix._doperation(L('Gx'), flat=False) + dgflat = self.fwdsim_matrix._doperation(L('Gx'), flat=True) # TODO assert correctness def test_hoperation(self): - hg = self.fwdsim._hoperation(L('Gx'), flat=False) - hgflat = self.fwdsim._hoperation(L('Gx'), flat=True) + hg = self.fwdsim_matrix._hoperation(L('Gx'), flat=False) + hgflat = self.fwdsim_matrix._hoperation(L('Gx'), flat=True) # TODO assert correctness @@ -130,7 +133,7 @@ class CPTPMatrixForwardSimTester(MatrixForwardSimTester): @classmethod def setUpClass(cls): super(CPTPMatrixForwardSimTester, cls).setUpClass() - cls.model = cls.model.copy() + cls.model = cls.model_matrix.copy() cls.model.set_all_parameterizations("CPTPLND") # so gates have nonzero hessians diff --git a/test/unit/objects/test_model.py b/test/unit/objects/test_model.py index fdad0a22c..e7a308e6f 100644 --- a/test/unit/objects/test_model.py +++ b/test/unit/objects/test_model.py @@ -55,6 +55,7 @@ def setUpClass(cls): def setUp(self): self.model = self._model.copy() + self.model.sim = 'matrix' super(ModelBase, self).setUp() def test_construction(self): diff --git a/test/unit/objects/test_objectivefns.py b/test/unit/objects/test_objectivefns.py index b7116f75e..6e2e5549c 100644 --- a/test/unit/objects/test_objectivefns.py +++ b/test/unit/objects/test_objectivefns.py @@ -15,6 +15,7 @@ class ObjectiveFunctionData(object): def setUp(self): self.model = smqfixtures.ns.datagen_model.copy() + self.model.sim = 'matrix' self.circuits = smqfixtures.ns.circuits self.dataset = smqfixtures.ns.dataset.copy() self.sparse_dataset = smqfixtures.ns.sparse_dataset.copy() From d7d18f9f9dbffa16fb59faf8b3a131ab3af2c1c3 Mon Sep 17 00:00:00 2001 From: Corey Ostrove Date: Mon, 16 Dec 2024 20:45:11 -0700 Subject: [PATCH 4/7] Unit test spring cleaning Remove a few unit tests that made reference or relied on an old model method that no longer exists. These tests were already being skipped (or had the sections referencing this old method commented out), so this doesn't actually affect anything other than making stuff cleaner. --- scripts/api_names.yaml | 7 -- test/test_packages/objects/test_evaltree.py | 114 ------------------ test/test_packages/objects/test_gatesets.py | 57 --------- .../test_packages/objects/test_instruments.py | 6 - test/unit/objects/test_model.py | 62 +--------- 5 files changed, 2 insertions(+), 244 deletions(-) diff --git a/scripts/api_names.yaml b/scripts/api_names.yaml index c09dfd954..c07036445 100644 --- a/scripts/api_names.yaml +++ b/scripts/api_names.yaml @@ -1261,8 +1261,6 @@ objects: Model: # XXX note dereference to forward simulator methods -- better design? __name__: null bulk_dprobs: null # XXX parameter `circuit_list` -> `circuits` - bulk_evaltree: null # XXX parameter `circuit_list` -> `circuits` - bulk_evaltree_from_resources: null # XXX parameter `circuit_list` -> `circuits` bulk_fill_dprobs: null # XXX parameter `eval_tree` -> `evaltree` bulk_fill_hprobs: null # XXX parameter `eval_tree` -> `evaltree` bulk_fill_probs: null # XXX parameter `eval_tree` -> `evaltree` @@ -1282,8 +1280,6 @@ objects: __name__: null basis: null bulk_dprobs: null # XXX parameter `circuit_list` -> `circuits` - bulk_evaltree: null # XXX parameter `circuit_list` -> `circuits` - bulk_evaltree_from_resources: null # XXX parameter `circuit_list` -> `circuits` bulk_fill_dprobs: null # XXX parameter `eval_tree` -> `evaltree` bulk_fill_hprobs: null # XXX parameter `eval_tree` -> `evaltree` bulk_fill_probs: null # XXX parameter `eval_tree` -> `evaltree` @@ -1837,8 +1833,6 @@ objects: OplessModel: __name__: null bulk_dprobs: null # XXX parameter `circuit_list` -> `circuits` - bulk_evaltree: null # XXX parameter `circuit_list` -> `circuits` - bulk_evaltree_from_resources: null # XXX parameter `circuit_list` -> `circuits` bulk_fill_dprobs: null # XXX parameter `eval_tree` -> `evaltree` bulk_fill_probs: null # XXX parameter `eval_tree` -> `evaltree` bulk_probs: null # XXX parameter `circuit_list` -> `circuits` @@ -1850,7 +1844,6 @@ objects: __name__: null SuccessFailModel: __name__: null - bulk_evaltree: null # XXX parameter `circuit_list` -> `circuits` dprobs: null get_num_outcomes: compute_num_outcomes poly_probs: polynomial_probs diff --git a/test/test_packages/objects/test_evaltree.py b/test/test_packages/objects/test_evaltree.py index 27a49db52..16041ec47 100644 --- a/test/test_packages/objects/test_evaltree.py +++ b/test/test_packages/objects/test_evaltree.py @@ -58,120 +58,6 @@ def test_map_layout(self): #TODO: test split layouts def test_matrix_layout(self): - self._test_layout(pygsti.layouts.matrixlayout.MatrixCOPALayout(self.circuits[:], self.model)) - - #SCRATCH - # # An additional specific test added from debugging mapevaltree splitting - # mgateset = pygsti.construction.create_explicit_model( - # [('Q0',)],['Gi','Gx','Gy'], - # [ "I(Q0)","X(pi/8,Q0)", "Y(pi/8,Q0)"]) - # mgateset._calcClass = MapForwardSimulator - # - # gatestring1 = ('Gx','Gy') - # gatestring2 = ('Gx','Gy','Gy') - # gatestring3 = ('Gx',) - # gatestring4 = ('Gy','Gy') - # #mevt,mlookup,moutcome_lookup = mgateset.bulk_evaltree( [gatestring1,gatestring2] ) - # #mevt,mlookup,moutcome_lookup = mgateset.bulk_evaltree( [gatestring1,gatestring4] ) - # mevt,mlookup,moutcome_lookup = mgateset.bulk_evaltree( [gatestring1,gatestring2,gatestring3,gatestring4] ) - # print("Tree = ",mevt) - # print("Cache size = ",mevt.cache_size()) - # print("lookup = ",mlookup) - # print() - # - # self.assertEqual(mevt[:], [(0, ('Gy',), 1), - # (1, ('Gy',), None), - # (None, ('rho0', 'Gx',), 0), - # (None, ('rho0', 'Gy', 'Gy'), None)]) - # self.assertEqual(mevt.cache_size(),2) - # self.assertEqual(mevt.evaluation_order(),[2, 0, 1, 3]) - # self.assertEqual(mevt.num_final_circuits(),4) - # - # ## COPY - # mevt_copy = mevt.copy() - # print("Tree copy = ",mevt_copy) - # print("Cache size = ",mevt_copy.cache_size()) - # print("Eval order = ",mevt_copy.evaluation_order()) - # print("Num final = ",mevt_copy.num_final_circuits()) - # print() - # - # self.assertEqual(mevt_copy[:], [(0, ('Gy',), 1), - # (1, ('Gy',), None), - # (None, ('rho0', 'Gx',), 0), - # (None, ('rho0', 'Gy', 'Gy'), None)]) - # self.assertEqual(mevt_copy.cache_size(),2) - # self.assertEqual(mevt_copy.evaluation_order(),[2, 0, 1, 3]) - # self.assertEqual(mevt_copy.num_final_circuits(),4) - # - # ## SQUEEZE - # maxCacheSize = 1 - # mevt_squeeze = mevt.copy() - # mevt_squeeze.squeeze(maxCacheSize) - # print("Squeezed Tree = ",mevt_squeeze) - # print("Cache size = ",mevt_squeeze.cache_size()) - # print("Eval order = ",mevt_squeeze.evaluation_order()) - # print("Num final = ",mevt_squeeze.num_final_circuits()) - # print() - # - # self.assertEqual(mevt_squeeze[:], [(0, ('Gy',), None), - # (0, ('Gy','Gy'), None), - # (None, ('rho0', 'Gx',), 0), - # (None, ('rho0', 'Gy', 'Gy'), None)]) - # - # self.assertEqual(mevt_squeeze.cache_size(),maxCacheSize) - # self.assertEqual(mevt_squeeze.evaluation_order(),[2, 0, 1, 3]) - # self.assertEqual(mevt_squeeze.num_final_circuits(),4) - # - # #SPLIT - # mevt_split = mevt.copy() - # mlookup_splt = mevt_split.split(mlookup,num_sub_trees=4) - # print("Split tree = ",mevt_split) - # print("new lookup = ",mlookup_splt) - # print() - # - # self.assertEqual(mevt_split[:], [(None, ('rho0', 'Gx',), 0), - # (0, ('Gy',), 1), - # (1, ('Gy',), None), - # (None, ('rho0', 'Gy', 'Gy'), None)]) - # self.assertEqual(mevt_split.cache_size(),2) - # self.assertEqual(mevt_split.evaluation_order(),[0, 1, 2, 3]) - # self.assertEqual(mevt_split.num_final_circuits(),4) - # - # - # subtrees = mevt_split.sub_trees() - # print("%d subtrees" % len(subtrees)) - # self.assertEqual(len(subtrees),4) - # for i,subtree in enumerate(subtrees): - # print("Sub tree %d = " % i,subtree, - # " csize = ",subtree.cache_size(), - # " eval = ",subtree.evaluation_order(), - # " nfinal = ",subtree.num_final_circuits()) - # self.assertEqual(subtree.cache_size(),0) - # self.assertEqual(subtree.evaluation_order(),[0]) - # self.assertEqual(subtree.num_final_circuits(),1) - # - # probs = np.zeros( mevt.num_final_elements(), 'd') - # mgateset.bulk_fill_probs(probs, mevt) - # print("probs = ",probs) - # print("lookup = ",mlookup) - # self.assertArraysAlmostEqual(probs, np.array([ 0.9267767,0.0732233,0.82664074, - # 0.17335926,0.96193977,0.03806023, - # 0.85355339,0.14644661],'d')) - # - # - # squeezed_probs = np.zeros( mevt_squeeze.num_final_elements(), 'd') - # mgateset.bulk_fill_probs(squeezed_probs, mevt_squeeze) - # print("squeezed probs = ",squeezed_probs) - # print("lookup = ",mlookup) - # self.assertArraysAlmostEqual(probs, squeezed_probs) - # - # split_probs = np.zeros( mevt_split.num_final_elements(), 'd') - # mgateset.bulk_fill_probs(split_probs, mevt_split) - # print("split probs = ",split_probs) - # print("lookup = ",mlookup_splt) - # for i in range(4): #then number of original strings (num final strings) - # self.assertArraysAlmostEqual(probs[mlookup[i]], split_probs[mlookup_splt[i]]) - self._test_layout(pygsti.layouts.matrixlayout.MatrixCOPALayout(self.circuits[:], self.model_matrix)) if __name__ == '__main__': diff --git a/test/test_packages/objects/test_gatesets.py b/test/test_packages/objects/test_gatesets.py index 6c63aa95a..9b8b1d223 100644 --- a/test/test_packages/objects/test_gatesets.py +++ b/test/test_packages/objects/test_gatesets.py @@ -54,7 +54,6 @@ def setUp(self): self.static_gateset.sim = 'matrix' self.mgateset = self.model.copy() - #self.mgateset._calcClass = MapForwardSimulator self.mgateset.sim = 'map' @@ -289,62 +288,6 @@ def Split(self, color, key): return self except MemoryError: pass #OK - when memlimit is too small and splitting is unproductive - #balanced not implemented - #with self.assertRaises(NotImplementedError): - # evt,_,_,lookup,outcome_lookup = self.model.bulk_evaltree_from_resources( - # circuits, mem_limit=memLimit, distribute_method="balanced", subcalls=['bulk_fill_hprobs']) - - - @unittest.skip("Need to add a way to force layout splitting") - def test_layout_splitting(self): - circuits = [('Gx',), - ('Gy',), - ('Gx','Gy'), - ('Gy','Gy'), - ('Gy','Gx'), - ('Gx','Gx','Gx'), - ('Gx','Gy','Gx'), - ('Gx','Gy','Gy'), - ('Gy','Gy','Gy'), - ('Gy','Gx','Gx') ] - evtA,lookupA,outcome_lookupA = self.model.bulk_evaltree( circuits ) - - evtB,lookupB,outcome_lookupB = self.model.bulk_evaltree( circuits ) - lookupB = evtB.split(lookupB, max_sub_tree_size=4) - - evtC,lookupC,outcome_lookupC = self.model.bulk_evaltree( circuits ) - lookupC = evtC.split(lookupC, num_sub_trees=3) - - with self.assertRaises(ValueError): - evtBad,lkup,_ = self.model.bulk_evaltree( circuits ) - evtBad.split(lkup, num_sub_trees=3, max_sub_tree_size=4) #can't specify both - - self.assertFalse(evtA.is_split()) - self.assertTrue(evtB.is_split()) - self.assertTrue(evtC.is_split()) - self.assertEqual(len(evtA.sub_trees()), 1) - self.assertEqual(len(evtB.sub_trees()), 5) #empirically - self.assertEqual(len(evtC.sub_trees()), 3) - self.assertLessEqual(max([len(subTree) - for subTree in evtB.sub_trees()]), 4) - - #print "Lenghts = ",len(evtA.sub_trees()),len(evtB.sub_trees()),len(evtC.sub_trees()) - #print "SubTree sizes = ",[len(subTree) for subTree in evtC.sub_trees()] - - bulk_probsA = np.empty( evtA.num_final_elements(), 'd') - bulk_probsB = np.empty( evtB.num_final_elements(), 'd') - bulk_probsC = np.empty( evtC.num_final_elements(), 'd') - self.model.bulk_fill_probs(bulk_probsA, evtA) - self.model.bulk_fill_probs(bulk_probsB, evtB) - self.model.bulk_fill_probs(bulk_probsC, evtC) - - for i,opstr in enumerate(circuits): - self.assertArraysAlmostEqual(bulk_probsA[ lookupA[i] ], - bulk_probsB[ lookupB[i] ]) - self.assertArraysAlmostEqual(bulk_probsA[ lookupA[i] ], - bulk_probsC[ lookupC[i] ]) - - @unittest.skip("TODO: add backward compatibility for old gatesets?") def test_load_old_gateset(self): #pygsti.baseobjs.results.enable_old_python_results_unpickling() diff --git a/test/test_packages/objects/test_instruments.py b/test/test_packages/objects/test_instruments.py index caab0997c..b3f201fcc 100644 --- a/test/test_packages/objects/test_instruments.py +++ b/test/test_packages/objects/test_instruments.py @@ -175,18 +175,12 @@ def testBasicGatesetOps(self): model = pygsti.models.modelconstruction.create_explicit_model_from_expressions( [('Q0',)],['Gi','Gx','Gy'], [ "I(Q0)","X(pi/8,Q0)", "Y(pi/8,Q0)"]) - # prep_labels=["rho0"], prep_expressions=["0"], - # effect_labels=["0","1"], effect_expressions=["0","complement"]) model.sim= 'matrix' v0 = modelconstruction.create_spam_vector("0", "Q0", "pp") v1 = modelconstruction.create_spam_vector("1", "Q0", "pp") P0 = np.dot(v0,v0.T) P1 = np.dot(v1,v1.T) - print("v0 = ",v0) - print("P0 = ",P0) - print("P1 = ",P0) - #print("P0+P1 = ",P0+P1) model.instruments["Itest"] = pygsti.modelmembers.instruments.Instrument([('0', P0), ('1', P1)]) diff --git a/test/unit/objects/test_model.py b/test/unit/objects/test_model.py index e7a308e6f..ac67312bf 100644 --- a/test/unit/objects/test_model.py +++ b/test/unit/objects/test_model.py @@ -463,26 +463,6 @@ def test_bulk_fill_probs(self): self.assertAlmostEqual(1 - expected_1, actual_1[1-zero_outcome_index1]) self.assertAlmostEqual(1 - expected_2, actual_2[1-zero_outcome_index2]) - def test_bulk_fill_probs_with_split_tree(self): - self.skipTest("Need a way to manually create 'split' layouts") - # XXX is this correct? EGN: looks right to me. - evt, lookup, _ = self.model.bulk_evaltree([self.gatestring1, self.gatestring2]) - nElements = evt.num_final_elements() - probs_to_fill = np.empty(nElements, 'd') - lookup_split = evt.split(lookup, num_sub_trees=2) - - with self.assertNoWarns(): - self.model.bulk_fill_probs(probs_to_fill, evt) - - expected_1 = self._expected_probs[self.gatestring1] - expected_2 = self._expected_probs[self.gatestring2] - actual_1 = probs_to_fill[lookup_split[0]] - actual_2 = probs_to_fill[lookup_split[1]] - self.assertAlmostEqual(expected_1, actual_1[0]) - self.assertAlmostEqual(expected_2, actual_2[0]) - self.assertAlmostEqual(1 - expected_1, actual_1[1]) - self.assertAlmostEqual(1 - expected_2, actual_2[1]) - def test_bulk_dprobs(self): with self.assertNoWarns(): bulk_dprobs = self.model.sim.bulk_dprobs([self.gatestring1, self.gatestring2]) @@ -519,17 +499,6 @@ def test_bulk_fill_dprobs_with_high_smallness_threshold(self): self.model.sim.bulk_fill_dprobs(dprobs_to_fill, layout) # TODO assert correctness - def test_bulk_fill_dprobs_with_split_tree(self): - self.skipTest("Need a way to manually create 'split' layouts") - evt, lookup, _ = self.model.bulk_evaltree([self.gatestring1, self.gatestring2]) - nElements = evt.num_final_elements() - nParams = self.model.num_params - dprobs_to_fill = np.empty((nElements, nParams), 'd') - lookup_split = evt.split(lookup, num_sub_trees=2) - with self.assertNoWarns(): - self.model.bulk_fill_dprobs(dprobs_to_fill, evt) - # TODO assert correctness - def test_bulk_hprobs(self): # call normally #with self.assertNoWarns(): # - now *can* warn about inefficient evaltree (ok) @@ -582,17 +551,6 @@ def test_bulk_fill_hprobs_with_high_smallness_threshold(self): self.model.sim.bulk_fill_hprobs(hprobs_to_fill, layout) # TODO assert correctness - def test_bulk_fill_hprobs_with_split_tree(self): - self.skipTest("Need a way to manually create 'split' layouts") - evt, lookup, _ = self.model.bulk_evaltree([self.gatestring1, self.gatestring2]) - nElements = evt.num_final_elements() - nParams = self.model.num_params - hprobs_to_fill = np.empty((nElements, nParams, nParams), 'd') - lookup_split = evt.split(lookup, num_sub_trees=2) - #with self.assertNoWarns(): # - now *can* warn about inefficient evaltree (ok) - self.model.bulk_fill_hprobs(hprobs_to_fill, evt) - # TODO assert correctness - def test_iter_hprobs_by_rectangle(self): layout = self.model.sim.create_layout([self.gatestring1, self.gatestring2], array_types=('epp',)) nP = self.model.num_params @@ -608,7 +566,7 @@ def test_iter_hprobs_by_rectangle(self): all_d12cols = np.concatenate(d12cols, axis=2) # TODO assert correctness - def test_bulk_evaltree(self): + def test_layout_construction(self): # Test tree construction circuits = pc.to_circuits( [('Gx',), @@ -624,14 +582,6 @@ def test_bulk_evaltree(self): layout = self.model.sim.create_layout(circuits) - #TODO: test different forced splittings, once this is possible to do... - #evt, lookup, outcome_lookup = self.model.bulk_evaltree(circuits, max_tree_size=4) - #evt, lookup, outcome_lookup = self.model.bulk_evaltree(circuits, min_subtrees=2, max_tree_size=4) - #with self.assertWarns(Warning): - # self.model.bulk_evaltree(circuits, min_subtrees=3, max_tree_size=8) - # #balanced to trigger 2 re-splits! (Warning: could not create a tree ...) - - class StandardMethodBase(GeneralMethodBase, SimMethodBase, ThresholdMethodBase): pass @@ -722,7 +672,7 @@ def setUp(self): super(FullMapSimMethodTester, self).setUp() self.model.sim = mapforwardsim.MapForwardSimulator(self.model) - def test_bulk_evaltree(self): + def test_layout_construction(self): # Test tree construction circuits = pc.to_circuits( [('Gx',), @@ -738,14 +688,6 @@ def test_bulk_evaltree(self): layout = self.model.sim.create_layout(circuits) - #TODO: test different forced splittings, once this is possible to do... - #evt, lookup, outcome_lookup = self.model.bulk_evaltree(circuits, max_tree_size=4) - #evt, lookup, outcome_lookup = self.model.bulk_evaltree(circuits, min_subtrees=2, max_tree_size=4) - #with self.assertNoWarns(): - # self.model.bulk_evaltree(circuits, min_subtrees=3, max_tree_size=8) - # #balanced to trigger 2 re-splits! (Warning: could not create a tree ...) - - class FullHighThresholdMethodTester(FullModelBase, ThresholdMethodBase, BaseCase): def setUp(self): super(FullHighThresholdMethodTester, self).setUp() From 0f9a4fcf3a6d7e2fdc25afc6a52bc1c3fd34c551 Mon Sep 17 00:00:00 2001 From: Corey Ostrove Date: Tue, 17 Dec 2024 21:37:49 -0700 Subject: [PATCH 5/7] Start processor spec cleanup Start the process of fleshing out missing documentation related to processor specification requirements for state preparations and POVMs, and also introduce some minor updates to the model construction code to allow for more general application to qudit models, and more synched up string specifier conventions. --- pygsti/modelmembers/povms/__init__.py | 139 ++++++++++++++++++-- pygsti/models/modelconstruction.py | 27 ++-- pygsti/processors/processorspec.py | 178 +++++++++++++++++--------- 3 files changed, 261 insertions(+), 83 deletions(-) diff --git a/pygsti/modelmembers/povms/__init__.py b/pygsti/modelmembers/povms/__init__.py index 3fc28cc29..b484b0bae 100644 --- a/pygsti/modelmembers/povms/__init__.py +++ b/pygsti/modelmembers/povms/__init__.py @@ -9,7 +9,6 @@ # in compliance with the License. You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file in the root pyGSTi directory. #*************************************************************************************************** -import _collections import functools as _functools import itertools as _itertools @@ -42,10 +41,40 @@ def create_from_pure_vectors(pure_vectors, povm_type, basis='pp', evotype='default', state_space=None, on_construction_error='warn'): - """ TODO: docstring -- create a POVM from a list/dict of (key, pure-vector) pairs """ + """ + Creates a Positive Operator-Valued Measure (POVM) from a list or dictionary of (key, pure-vector) pairs. + + Parameters + ---------- + pure_vectors : list or dict + A list of (key, pure-vector) pairs or a dictionary where keys are labels and values are pure state vectors. + + povm_type : str or tuple + The type of POVM to create. This can be a single string or a tuple of strings indicating the preferred types. + Supported types include 'computational', 'static pure', 'full pure', 'static', 'full', 'full TP', and any valid + Lindblad parameterization type. + + basis : str, optional + The basis in which the pure vectors are expressed. Default is 'pp'. + + evotype : str, optional + The evolution type. Default is 'default'. + + state_space : StateSpace, optional + The state space in which the POVM operates. Default is None. + + on_construction_error : str, optional + Specifies the behavior when an error occurs during POVM construction. Options are 'raise' to raise the error, + 'warn' to print a warning message, or any other value to silently ignore the error. Default is 'warn'. + + Returns + ------- + POVM + The constructed POVM object. + """ povm_type_preferences = (povm_type,) if isinstance(povm_type, str) else povm_type if not isinstance(pure_vectors, dict): # then assume it's a list of (key, value) pairs - pure_vectors = _collections.OrderedDict(pure_vectors) + pure_vectors = dict(pure_vectors) if state_space is None: state_space = _statespace.default_space_for_udim(len(next(iter(pure_vectors.values())))) @@ -94,10 +123,41 @@ def create_from_pure_vectors(pure_vectors, povm_type, basis='pp', evotype='defau def create_from_dmvecs(superket_vectors, povm_type, basis='pp', evotype='default', state_space=None, on_construction_error='warn'): - """ TODO: docstring -- create a POVM from a list/dict of (key, pure-vector) pairs """ + """ + Creates a Positive Operator-Valued Measure (POVM) from a list or dictionary of (key, superket) pairs. + + Parameters + ---------- + superket_vectors : list or dict + A list of (key, pure-vector) pairs or a dictionary where keys are labels and values are superket vectors. + i.e. vectorized density matrices. + + povm_type : str or tuple + The type of POVM to create. This can be a single string or a tuple of strings indicating the preferred types. + Supported types include 'full', 'static', 'full TP', 'computational', 'static pure', 'full pure', and any valid + Lindblad parameterization type. + + basis : str or `Basis`, optional + The basis in which the density matrix vectors are expressed. Default is 'pp'. + + evotype : str, optional + The evolution type. Default is 'default'. + + state_space : StateSpace, optional + The state space in which the POVM operates. Default is None. + + on_construction_error : str, optional + Specifies the behavior when an error occurs during POVM construction. Options are 'raise' to raise the error, + 'warn' to print a warning message, or any other value to silently ignore the error. Default is 'warn'. + + Returns + ------- + POVM + The constructed POVM object. + """ povm_type_preferences = (povm_type,) if isinstance(povm_type, str) else povm_type if not isinstance(superket_vectors, dict): # then assume it's a list of (key, value) pairs - superket_vectors = _collections.OrderedDict(superket_vectors) + superket_vectors = dict(superket_vectors) for typ in povm_type_preferences: try: @@ -140,12 +200,42 @@ def create_from_dmvecs(superket_vectors, povm_type, basis='pp', evotype='default print('Failed to construct povm with type "{}" with error: {}'.format(typ, str(err))) pass # move on to next type - raise ValueError("Could not create a POVM of type(s) %s from the given pure vectors!" % (str(povm_type))) + raise ValueError("Could not create a POVM of type(s) %s from the given density matrix vectors!" % (str(povm_type))) def create_effect_from_pure_vector(pure_vector, effect_type, basis='pp', evotype='default', state_space=None, on_construction_error='warn'): - """ TODO: docstring -- create a State from a state vector """ + """ + Creates a POVM effect from a pure state vector. + + Parameters + ---------- + pure_vector : array-like + The pure state vector from which to create the POVM effect. + + effect_type : str or tuple + The type of effect to create. This can be a single string or a tuple of strings indicating the preferred types. + Supported types include 'computational', 'static pure', 'full pure', 'static', 'full', 'static clifford', and + any valid Lindblad parameterization type. + + basis : str or `Basis` optional + The basis in which the pure vector is expressed. Default is 'pp'. + + evotype : str, optional + The evolution type. Default is 'default'. + + state_space : StateSpace, optional + The state space in which the effect operates. Default is None. + + on_construction_error : str, optional + Specifies the behavior when an error occurs during effect construction. Options are 'raise' to raise the error, + 'warn' to print a warning message, or any other value to silently ignore the error. Default is 'warn'. + + Returns + ------- + POVMEffect + The constructed POVM effect object. + """ effect_type_preferences = (effect_type,) if isinstance(effect_type, str) else effect_type if state_space is None: state_space = _statespace.default_space_for_udim(len(pure_vector)) @@ -196,6 +286,38 @@ def create_effect_from_pure_vector(pure_vector, effect_type, basis='pp', evotype def create_effect_from_dmvec(superket_vector, effect_type, basis='pp', evotype='default', state_space=None, on_construction_error='warn'): + """ + Creates a POVM effect from a density matrix vector (superket). + + Parameters + ---------- + superket_vector : array-like + The density matrix vector (superket) from which to create the POVM effect. + + effect_type : str or tuple + The type of effect to create. This can be a single string or a tuple of strings indicating the preferred types. + Supported types include 'static', 'full', and any valid Lindblad parameterization type. For other types + we first try to convert to a pure state vector and then utilize `create_effect_from_pure_vector` + + basis : str or `Basis` optional + The basis in which the superket vector is expressed. Default is 'pp'. + + evotype : str, optional + The evolution type. Default is 'default'. + + state_space : StateSpace, optional + The state space in which the effect operates. Default is None. + + on_construction_error : str, optional + Specifies the behavior when an error occurs during effect construction. Options are 'raise' to raise the error, + 'warn' to print a warning message, or any other value to silently ignore the error. Default is 'warn'. + + Returns + ------- + POVMEffect + The constructed POVM effect object. + """ + effect_type_preferences = (effect_type,) if isinstance(effect_type, str) else effect_type if state_space is None: state_space = _statespace.default_space_for_dim(len(superket_vector)) @@ -241,7 +363,8 @@ def create_effect_from_dmvec(superket_vector, effect_type, basis='pp', evotype=' def povm_type_from_op_type(op_type): - """Decode an op type into an appropriate povm type. + """ + Decode an op type into an appropriate povm type. Parameters: ----------- diff --git a/pygsti/models/modelconstruction.py b/pygsti/models/modelconstruction.py index 3bfff16b0..fc6d3f6e4 100644 --- a/pygsti/models/modelconstruction.py +++ b/pygsti/models/modelconstruction.py @@ -974,7 +974,7 @@ def _spec_to_densevec(spec, is_prep): local_noise = False; independent_gates = True; independent_spam = True prep_layers, povm_layers = _create_spam_layers(processor_spec, modelnoise, local_noise, ideal_prep_type, ideal_povm_type, evotype, - state_space, independent_gates, independent_spam, basis) + state_space, independent_spam, basis) for k, v in prep_layers.items(): ret.preps[k] = v for k, v in povm_layers.items(): @@ -997,7 +997,7 @@ def _spec_to_densevec(spec, is_prep): def _create_spam_layers(processor_spec, modelnoise, local_noise, - ideal_prep_type, ideal_povm_type, evotype, state_space, independent_gates, independent_spam, + ideal_prep_type, ideal_povm_type, evotype, state_space, independent_spam, basis='pp'): """ local_noise=True creates lindblad ops that are embedded & composed 1Q ops, and assumes that modelnoise specifies 1Q noise. local_noise=False assumes modelnoise specifies n-qudit noise""" @@ -1099,7 +1099,8 @@ def _decomp_index_to_digits(i, bases): or ideal_prep_type.startswith('1+(') or ideal_prep_type.startswith('lindblad ')): if isinstance(prep_spec, str): - # Notes on conventions: When there are multiple qubits, the leftmost in a string (or, intuitively, + """ + Notes on conventions: When there are multiple qubits, the leftmost in a string (or, intuitively, # the first element in a list, e.g. [Q0_item, Q1_item, etc]) is "qubit 0". For example, in the # outcome string "01" qubit0 is 0 and qubit1 is 1. To create the full state/projector, 1Q operations # are tensored together in the same order, i.e., kron(Q0_item, Q1_item, ...). When a state is specified @@ -1108,6 +1109,7 @@ def _decomp_index_to_digits(i, bases): # where i is written normally, with the least significant bit on the right (but, perhaps # counterintuitively, this bit corresponds to the highest-indexed qubit). For example, "rho6" in a # 3-qubit system corresponds to "rho_110", that is |1> otimes |1> otimes |0> or |110>. + """ if not all([udim == 2 for udim in processor_spec.qudit_udims]): raise NotImplementedError(("State preps can currently only be constructed on a space of *qubits*" " when `ideal_prep_type == 'computational'` or is a Lindblad type")) @@ -1153,10 +1155,11 @@ def _decomp_index_to_digits(i, bases): def _create_ideal_1Q_prep(ud, i): v = _np.zeros(ud, 'd'); v[i] = 1.0 - return _state.create_from_pure_vector(v, vectype, 'pp', evotype, state_space=None) + return _state.create_from_pure_vector(v, vectype, basis, evotype, state_space=None) if isinstance(prep_spec, str): - if prep_spec.startswith('rho_') and all([l in ('0', '1') for l in prep_spec[len('rho_'):]]): + if prep_spec.startswith('rho_') and \ + all([l in [str(j) for j in processor_spec.qudit_udims[i]] for i,l in enumerate(prep_spec[len('rho_'):])]): bydigit_index = prep_spec[len('rho_'):] assert (len(bydigit_index) == num_qudits), \ "Wrong number of qudits in '%s': expected %d" % (prep_spec, num_qudits) @@ -1269,7 +1272,7 @@ def _1vec(ud, i): # constructs a vector of length `ud` with a single 1 at index def _create_ideal_1Q_povm(ud): effect_vecs = [(str(i), _1vec(ud, i)) for i in range(ud)] - return _povm.create_from_pure_vectors(effect_vecs, vectype, 'pp', + return _povm.create_from_pure_vectors(effect_vecs, vectype, basis, evotype, state_space=None) if isinstance(povm_spec, str): @@ -1322,11 +1325,12 @@ def _create_ideal_1Q_povm(ud): "You must provide at least one component effect specifier for each POVM effect!" effect_components = [] - if len(effect_spec) > 1: convert_to_dmvecs = True + if len(effect_spec) > 1: + convert_to_dmvecs = True for comp_espec in effect_spec: if isinstance(comp_espec, str): - if comp_espec.isdigit(): # all([l in ('0', '1') for l in comp_espec]) for qubits - bydigit_index = comp_espec + if comp_espec.isdigit() or (comp_espec.startswith("E_") and comp_espec[len('E_'):].isdigit()): # all([l in ('0', '1') for l in comp_espec]) for qubits + bydigit_index = comp_espec if comp_espec.isdigit() else comp_espec[len('E_'):] assert (len(bydigit_index) == num_qudits), \ "Wrong number of qudits in '%s': expected %d" % (comp_espec, num_qudits) v = _np.zeros(state_space.udim) @@ -1351,7 +1355,6 @@ def _create_ideal_1Q_povm(ud): else: raise ValueError("Invalid POVM effect spec: %s" % str(comp_espec)) effects_components.append((k, effect_components)) - if convert_to_dmvecs: effects = [] for k, effect_components in effects_components: @@ -1695,7 +1698,7 @@ def _create_crosstalk_free_model(processor_spec, modelnoise, custom_gates=None, local_noise = True prep_layers, povm_layers = _create_spam_layers(processor_spec, modelnoise, local_noise, ideal_prep_type, ideal_povm_type, evotype, - state_space, independent_gates, independent_spam, basis) + state_space, independent_spam, basis) modelnoise.warn_about_zero_counters() return _LocalNoiseModel(processor_spec, gatedict, prep_layers, povm_layers, @@ -1884,7 +1887,7 @@ def _create_cloud_crosstalk_model(processor_spec, modelnoise, custom_gates=None, local_noise = False prep_layers, povm_layers = _create_spam_layers(processor_spec, modelnoise, local_noise, 'computational', 'computational', evotype, state_space, - independent_gates, independent_spam, basis) + independent_spam, basis) if errcomp_type == 'gates': create_stencil_fn = modelnoise.create_errormap_stencil diff --git a/pygsti/processors/processorspec.py b/pygsti/processors/processorspec.py index b7d4a04d6..9fc6070d2 100644 --- a/pygsti/processors/processorspec.py +++ b/pygsti/processors/processorspec.py @@ -45,74 +45,126 @@ def __init__(self): class QuditProcessorSpec(ProcessorSpec): """ The device specification for a one or more qudit quantum computer. - - Parameters - ---------- - num_qubits : int - The number of qubits in the device. - - gate_names : list of strings - The names of gates in the device. This may include standard gate - names known by pyGSTi (see below) or names which appear in the - `nonstd_gate_unitaries` argument. The set of standard gate names - includes, but is not limited to: - - - 'Gi' : the 1Q idle operation - - 'Gx','Gy','Gz' : 1-qubit pi/2 rotations - - 'Gxpi','Gypi','Gzpi' : 1-qubit pi rotations - - 'Gh' : Hadamard - - 'Gp' : phase or S-gate (i.e., ((1,0),(0,i))) - - 'Gcphase','Gcnot','Gswap' : standard 2-qubit gates - - Alternative names can be used for all or any of these gates, but - then they must be explicitly defined in the `nonstd_gate_unitaries` - dictionary. Including any standard names in `nonstd_gate_unitaries` - overrides the default (builtin) unitary with the one supplied. - - nonstd_gate_unitaries: dictionary of numpy arrays - A dictionary with keys that are gate names (strings) and values that are numpy arrays specifying - quantum gates in terms of unitary matrices. This is an additional "lookup" database of unitaries - - to add a gate to this `QubitProcessorSpec` its names still needs to appear in the `gate_names` list. - This dictionary's values specify additional (target) native gates that can be implemented in the device - as unitaries acting on ordinary pure-state-vectors, in the standard computationl basis. These unitaries - need not, and often should not, be unitaries acting on all of the qubits. E.g., a CNOT gate is specified - by a key that is the desired name for CNOT, and a value that is the standard 4 x 4 complex matrix for CNOT. - All gate names must start with 'G'. As an advanced behavior, a unitary-matrix-returning function which - takes a single argument - a tuple of label arguments - may be given instead of a single matrix to create - an operation *factory* which allows continuously-parameterized gates. This function must also return - an empty/dummy unitary when `None` is given as it's argument. - - availability : dict, optional - A dictionary whose keys are some subset of the keys (which are gate names) `nonstd_gate_unitaries` and the - strings (which are gate names) in `gate_names` and whose values are lists of qubit-label-tuples. Each - qubit-label-tuple must have length equal to the number of qubits the corresponding gate acts upon, and - causes that gate to be available to act on the specified qubits. Instead of a list of tuples, values of - `availability` may take the special values `"all-permutations"` and `"all-combinations"`, which as their - names imply, equate to all possible permutations and combinations of the appropriate number of qubit labels - (deterined by the gate's dimension). If a gate name is not present in `availability`, the default is - `"all-permutations"`. So, the availability of a gate only needs to be specified when it cannot act in every - valid way on the qubits (e.g., the device does not have all-to-all connectivity). - - geometry : {"line","ring","grid","torus"} or QubitGraph, optional - The type of connectivity among the qubits, specifying a graph used to - define neighbor relationships. Alternatively, a :class:`QubitGraph` - object with `qubit_labels` as the node labels may be passed directly. - This argument is only used as a convenient way of specifying gate - availability (edge connections are used for gates whose availability - is unspecified by `availability` or whose value there is `"all-edges"`). - - qubit_labels : list or tuple, optional - The labels (integers or strings) of the qubits. If `None`, then the integers starting with zero are used. - - aux_info : dict, optional - Any additional information that should be attached to this processor spec. - - TODO: update this docstring for qudits """ def __init__(self, qudit_labels, qudit_udims, gate_names, nonstd_gate_unitaries=None, availability=None, geometry=None, prep_names=('rho0',), povm_names=('Mdefault',), instrument_names=(), nonstd_preps=None, nonstd_povms=None, nonstd_instruments=None, aux_info=None): + """ + Parameters + ---------- + num_qubits : int + The number of qubits in the device. + + gate_names : list of strings + The names of gates in the device. This may include standard gate + names known by pyGSTi (see below) or names which appear in the + `nonstd_gate_unitaries` argument. The set of standard gate names + includes, but is not limited to: + + - 'Gi' : the 1Q idle operation + - 'Gx','Gy','Gz' : 1-qubit pi/2 rotations + - 'Gxpi','Gypi','Gzpi' : 1-qubit pi rotations + - 'Gh' : Hadamard + - 'Gp' : phase or S-gate (i.e., ((1,0),(0,i))) + - 'Gcphase','Gcnot','Gswap' : standard 2-qubit gates + + Alternative names can be used for all or any of these gates, but + then they must be explicitly defined in the `nonstd_gate_unitaries` + dictionary. Including any standard names in `nonstd_gate_unitaries` + overrides the default (builtin) unitary with the one supplied. + + nonstd_gate_unitaries: dictionary of numpy arrays + A dictionary with keys that are gate names (strings) and values that are numpy arrays specifying + quantum gates in terms of unitary matrices. This is an additional "lookup" database of unitaries - + to add a gate to this `QuditProcessorSpec` its names still needs to appear in the `gate_names` list. + This dictionary's values specify additional (target) native gates that can be implemented in the device + as unitaries acting on ordinary pure-state-vectors, in the standard computationl basis. These unitaries + need not, and often should not, be unitaries acting on all of the qubits. E.g., a CNOT gate is specified + by a key that is the desired name for CNOT, and a value that is the standard 4 x 4 complex matrix for CNOT. + All gate names must start with 'G'. As an advanced behavior, a unitary-matrix-returning function which + takes a single argument - a tuple of label arguments - may be given instead of a single matrix to create + an operation *factory* which allows continuously-parameterized gates. This function must also return + an empty/dummy unitary when `None` is given as it's argument. + + availability : dict, optional + A dictionary whose keys are some subset of the keys (which are gate names) `nonstd_gate_unitaries` and the + strings (which are gate names) in `gate_names` and whose values are lists of qubit-label-tuples. Each + qubit-label-tuple must have length equal to the number of qubits the corresponding gate acts upon, and + causes that gate to be available to act on the specified qubits. Instead of a list of tuples, values of + `availability` may take the special values `"all-permutations"` and `"all-combinations"`, which as their + names imply, equate to all possible permutations and combinations of the appropriate number of qubit labels + (deterined by the gate's dimension). If a gate name is not present in `availability`, the default is + `"all-permutations"`. So, the availability of a gate only needs to be specified when it cannot act in every + valid way on the qubits (e.g., the device does not have all-to-all connectivity). + + geometry : {"line","ring","grid","torus"} or QubitGraph, optional + The type of connectivity among the qubits, specifying a graph used to + define neighbor relationships. Alternatively, a :class:`QubitGraph` + object with `qubit_labels` as the node labels may be passed directly. + This argument is only used as a convenient way of specifying gate + availability (edge connections are used for gates whose availability + is unspecified by `availability` or whose value there is `"all-edges"`). + + prep_names : list or tuple of str, optional (default ('rho0',)) + List of strings corresponding to the names of the native state preparation + operations supported by this processor specification. State preparation names + must start with 'rho'. + + povm_names : list or tuple of str, optional (default ('Mdefault',)) + List of strings corresponding to the names of the native POVMs + supported by this processor specification. POVM names must start with + 'M'. + + instrument_names : list or tuple of str, optional (default ()) + List of strings corresponding to the names of the quantum instruments + supported by this processor specification. Instrument names must start with + 'I'. + + nonstd_preps : dict, optional (default None) + Dictionary mapping preparation names (as specified in `prep_names`) to corresponding + state preparations. The values of this dictionary can be the following specifiers: + + - numpy ndarray: numpy vector corresponding to the dense representation of the pure + state corresponding to this state preparation, written in the standard/computational + basis. + - string specifiers: For string state preparation specifiers there are two prefixes supported + which determine the parsing applied for conversion to a corresponding state preparation.: + The first prefix is 'rho_'. When this prefix is used, any digits proceeding in the string + are interpreted as digits of the base d number (where d is appropriate dimensions of the qudit + subsystems, though note this dimension might vary for each subsystem) labeling a standard basis state. + E.g. 'rho_01' when both subsystems are qubits corresponds to the state |01>. 'rho_12' when the two subsystems + are qutrits is the state |12>, etc. The second prefix is 'rho' (w/o the underscore). When this + prefix is used the following digits are interpreted as an integer. This integer is then converted + into a base d (see comment above about mixed dimensions) number labeling the corresponding standard + basis state (with the conversion using right-LSB convention). + + nonstd_povms : dict, optional (default None) + Dictionary mapping POVM names (as specified in `povm_names`) to corresponding POVMs. The values of + this dictionary can be the following specifiers: + + - string specifiers: Presently two special string specifiers are supported, 'Mdefault' and 'Mz', + both of which map to POVMs for computational-basis readout with the appropriate dimensions. + - dictionary: A dictionary whose keys are effect labels, and whose values are specifiers used to construct + corresponding effects. Effect specifiers can themselves take two forms: + - list of numpy arrays: List of numpy arrays corresponding to the component pure states whose corresponding + projectors (density matrix vectors) are summed to produce this POVM effect. When there is only a single + such pure state then one can alternatively use that numpy array directly as the value (without wrapping + in a list) for convenience. E.g. to construct a two-qubit, two-outcome, POVM corresponding to parity readout wherein + each POVM effect corresponds to a rank-2 projector onto the even or odd computational subspace the complete POVM + specifier would be: + {'even': [np.array([1,0,0,0], np.array([0,0,0,1])], 'odd': [np.array([0,1,0,0], np.array([0,0,1,0])]} + -list of string specifiers: Alternatively one can specify a list of effect specifiers using special string + notation. Effect specifier strings are always prefixed by 'E' + + nonstd_instruments : dict, optional (default None) + + qubit_labels : list or tuple, optional + The labels (integers or strings) of the qubits. If `None`, then the integers starting with zero are used. + + aux_info : dict, optional + Any additional information that should be attached to this processor spec. + """ num_qudits = len(qudit_labels) assert(len(qudit_udims) == num_qudits), "length of `qudit_labels` must equal that of `qubit_udims`!" assert(not (len(qudit_labels) > 1 and availability is None and geometry is None)), \ From 551b1570af101130b37ad88c0fd085aa0f7d102a Mon Sep 17 00:00:00 2001 From: Corey Ostrove Date: Tue, 17 Dec 2024 23:18:56 -0700 Subject: [PATCH 6/7] More processorspec cleanup (for instruments) Continue the quest to properly document the convention expected for various non-standard operation specifiers. In this commit there are additional details added regarding instruments specifically. There are also changes made to the implementation of the instrument builder in the model construction code which fills in gaps between the supported specifier options for preps and POVM effects to ensure parity/consistency. Also does away with some limitations in the specifier length when using 2-tuples of prep and effect specifiers. --- pygsti/algorithms/germselection.py | 1 - pygsti/forwardsims/mapforwardsim.py | 1 - pygsti/models/modelconstruction.py | 41 ++- pygsti/processors/processorspec.py | 240 ++++++++++++------ .../construction/test_modelconstruction.py | 5 +- 5 files changed, 201 insertions(+), 87 deletions(-) diff --git a/pygsti/algorithms/germselection.py b/pygsti/algorithms/germselection.py index 5c862895c..bd8fd488a 100644 --- a/pygsti/algorithms/germselection.py +++ b/pygsti/algorithms/germselection.py @@ -413,7 +413,6 @@ def find_germs(target_model, randomize=True, randomization_strength=1e-2, if ckt._static: new_ckt = ckt.copy(editable=True) new_ckt.line_labels = target_model.state_space.state_space_labels - print(new_ckt.line_labels) new_ckt.done_editing() finalGermList.append(new_ckt) else: diff --git a/pygsti/forwardsims/mapforwardsim.py b/pygsti/forwardsims/mapforwardsim.py index 2a42f8891..89681605f 100644 --- a/pygsti/forwardsims/mapforwardsim.py +++ b/pygsti/forwardsims/mapforwardsim.py @@ -103,7 +103,6 @@ def __getstate__(self): # and this is done by the parent model which will cause _set_evotype to be called. return state - class MapForwardSimulator(_DistributableForwardSimulator, SimpleMapForwardSimulator): """ Computes circuit outcome probabilities using circuit layer maps that act on a state. diff --git a/pygsti/models/modelconstruction.py b/pygsti/models/modelconstruction.py index fc6d3f6e4..28e4428f4 100644 --- a/pygsti/models/modelconstruction.py +++ b/pygsti/models/modelconstruction.py @@ -883,7 +883,7 @@ def _embed_unitary(statespace, target_labels, unitary): key = _label.Label(instrument_name, inds) if isinstance(instrument_spec, str): - if instrument_spec == "Iz": + if instrument_spec == "Iz": #TODO: Create a set of standard instruments the same way we handle gates. #NOTE: this is very inefficient currently - there should be a better way of # creating an Iz instrument in the FUTURE inst_members = {} @@ -903,7 +903,15 @@ def _spec_to_densevec(spec, is_prep): num_qudits = len(qudit_labels) if isinstance(spec, str): if spec.isdigit(): # all([l in ('0', '1') for l in spec]): for qubits - bydigit_index = effect_spec + bydigit_index = spec + assert (len(bydigit_index) == num_qudits), \ + "Wrong number of qudits in '%s': expected %d" % (spec, num_qudits) + v = _np.zeros(state_space.udim) + inc = _np.flip(_np.cumprod(list(reversed(processor_spec.qudit_udims[1:] + (1,))))) + index = _np.dot(inc, list(map(int, bydigit_index))) + v[index] = 1.0 + elif (not is_prep) and spec.startswith("E_") and spec[len('E_'):].isdigit(): + bydigit_index = spec[len('E_'):] assert (len(bydigit_index) == num_qudits), \ "Wrong number of qudits in '%s': expected %d" % (spec, num_qudits) v = _np.zeros(state_space.udim) @@ -915,6 +923,14 @@ def _spec_to_densevec(spec, is_prep): assert (0 <= index < state_space.udim), \ "Index in '%s' out of bounds for state space with udim %d" % (spec, state_space.udim) v = _np.zeros(state_space.udim); v[index] = 1.0 + elif is_prep and spec.startswith("rho_") and spec[len('rho_'):].isdigit(): + bydigit_index = spec[len('rho_'):] + assert (len(bydigit_index) == num_qudits), \ + "Wrong number of qudits in '%s': expected %d" % (spec, num_qudits) + v = _np.zeros(state_space.udim) + inc = _np.flip(_np.cumprod(list(reversed(processor_spec.qudit_udims[1:] + (1,))))) + index = _np.dot(inc, list(map(int, bydigit_index))) + v[index] = 1.0 elif is_prep and spec.startswith("rho") and spec[len('rho'):].isdigit(): index = int(effect_spec[len('rho'):]) assert (0 <= index < state_space.udim), \ @@ -932,22 +948,27 @@ def _spec_to_densevec(spec, is_prep): return _bt.change_basis(_ot.state_to_dmvec(v), 'std', basis) - # elements are key, list-of-2-tuple pairs + # elements are key, list-of-2-tuple pairs or numpy array inst_members = {} - for k, lst in instrument_spec.items(): + for k, inst_effect_spec in instrument_spec.items(): + #one option is to specify the full dense instrument effect as a numpy array. + if isinstance(inst_effect_spec, _np.ndarray): + inst_members[k] = inst_effect_spec.copy() + continue member = None - if len(lst) == 2: - for effect_spec, prep_spec in lst: + if isinstance(inst_effect_spec, tuple): + inst_effect_spec = [inst_effect_spec] + elif isinstance(inst_effect_spec, list): + #elements should be 2-tuples corresponding to effect specs and prep effects respectively. + for (effect_spec, prep_spec) in inst_effect_spec: effect_vec = _spec_to_densevec(effect_spec, is_prep=False) prep_vec = _spec_to_densevec(prep_spec, is_prep=True) if member is None: member = _np.outer(effect_vec, prep_vec) else: member += _np.outer(effect_vec, prep_vec) - else: # elements are key, array of outer product already - # TODO: This appears to be the new standard format. Deprecate outer prod code above? - # But old code could still be useful. - member = lst.copy() + else: + raise ValueError('Unsupported instrument effect specification. See documentation of `QuditProcessorSpec` or `QubitProcessorSpec` for supported formats.') assert (member is not None), \ "You must provide at least one rank-1 specifier for each instrument member!" diff --git a/pygsti/processors/processorspec.py b/pygsti/processors/processorspec.py index 9fc6070d2..7d9a27df1 100644 --- a/pygsti/processors/processorspec.py +++ b/pygsti/processors/processorspec.py @@ -155,9 +155,30 @@ def __init__(self, qudit_labels, qudit_udims, gate_names, nonstd_gate_unitaries= specifier would be: {'even': [np.array([1,0,0,0], np.array([0,0,0,1])], 'odd': [np.array([0,1,0,0], np.array([0,0,1,0])]} -list of string specifiers: Alternatively one can specify a list of effect specifiers using special string - notation. Effect specifier strings are always prefixed by 'E' + notation. String specifiers can take three formats: + 1. They can be strings which directly correspond to the desired output bit/ditstring in the standard basis. + E.g. "0000" or "2212" + 2. Strings prefixed by either 'E_' or 'E' (w/o an underscore). In the first case any digits proceeding the + "E_" are interpretted as a bit/ditstring written in whatever base is appropriate given the qudit dimensions. + If prefixed by "E" (w/o an underscore) the proceeding digits are interpreted as an integer and converted into + a base d number using right-LSB convention. E.g. 'E_0000' corresponds to the state |0000>, and E15 corresponds + to |1111> (assuming this was acting on 4-qubits). nonstd_instruments : dict, optional (default None) + Dictionary mapping instrument names (as specified in `instrument_names`) to corresponding instruments. The values + of this dictionary can be the following specifiers: + + - string specifiers: Presently only one special string specifier is supported, 'Iz'. This corresponds to a quantum + instrument for computational-basis readout. + - dictionary: A dictionary whose keys are instrument effect labels, and whose values are specifiers used to construct the + corresponding instrument effects. Instrument effect specifiers can take the following form: + - numpy array: A numpy array corresponding to the dense representation of the instrument effect. + - lists of 2-tuples: Each tuple in this list of 2-tuples is such that the first element corresponds to a POVM effect + specifier (see `nonstd_povms` for supported options), and the second element is a state preparation specifier + (see `nonstd_preps` for supported options). These specifiers are used to construct appropriate effect and preparation + representations which are then have their outer product taken. This is done for each 2-tuple, and the outer products are + then summed to get the overall instrument effect. In the case where there is only a single 2-tuple for an instrument effect + this tuple can be used directly as the dictionary value (w/o being wrapped in a list) for convenience. qubit_labels : list or tuple, optional The labels (integers or strings) of the qubits. If `None`, then the integers starting with zero are used. @@ -183,7 +204,7 @@ def __init__(self, qudit_labels, qudit_udims, gate_names, nonstd_gate_unitaries= self.nonstd_instruments = nonstd_instruments.copy() if (nonstd_instruments is not None) else {} # Store the unitary matrices defining the gates, as it is convenient to have these easily accessable. - self.gate_unitaries = _collections.OrderedDict() + self.gate_unitaries = dict() std_gate_unitaries = _itgs.standard_gatename_unitaries() for gname in gate_names: if gname in nonstd_gate_unitaries: @@ -234,9 +255,9 @@ def __init__(self, qudit_labels, qudit_udims, gate_names, nonstd_gate_unitaries= assert(len(self.qudit_labels) == len(self.qudit_udims)) # Set availability - if availability is None: availability = {} - self.availability = _collections.OrderedDict([(gatenm, availability.get(gatenm, 'all-edges')) - for gatenm in self.gate_names]) + if availability is None: + availability = {} + self.availability = {gatenm: availability.get(gatenm, 'all-edges') for gatenm in self.gate_names} # if _Lbl(gatenm).sslbls is not None NEEDED? self.compiled_from = None # could hold (QuditProcessorSpec, compilations) tuple if not None @@ -515,8 +536,8 @@ def rename(nm): self.gate_names = tuple([rename(nm) for nm in self.gate_names]) self.nonstd_gate_unitaries = {rename(k): v for k, v in self.nonstd_gate_unitaries} - self.gate_unitaries = _collections.OrderedDict([(rename(k), v) for k, v in self.gate_unitaries]) - self.availability = _collections.OrderedDict([(rename(k), v) for k, v in self.availability]) + self.gate_unitaries = {rename(k): v for k, v in self.gate_unitaries} + self.availability = {rename(k): v for k, v in self.availability} def resolved_availability(self, gate_name, tuple_or_function="auto"): """ @@ -807,77 +828,149 @@ def global_idle_layer_label(self): class QubitProcessorSpec(QuditProcessorSpec): """ The device specification for a one or more qudit quantum computer. - - Parameters - ---------- - num_qubits : int - The number of qubits in the device. - - gate_names : list of strings - The names of gates in the device. This may include standard gate - names known by pyGSTi (see below) or names which appear in the - `nonstd_gate_unitaries` argument. The set of standard gate names - includes, but is not limited to: - - - 'Gi' : the 1Q idle operation - - 'Gx','Gy','Gz' : 1-qubit pi/2 rotations - - 'Gxpi','Gypi','Gzpi' : 1-qubit pi rotations - - 'Gh' : Hadamard - - 'Gp' : phase or S-gate (i.e., ((1,0),(0,i))) - - 'Gcphase','Gcnot','Gswap' : standard 2-qubit gates - - Alternative names can be used for all or any of these gates, but - then they must be explicitly defined in the `nonstd_gate_unitaries` - dictionary. Including any standard names in `nonstd_gate_unitaries` - overrides the default (builtin) unitary with the one supplied. - - nonstd_gate_unitaries: dictionary of numpy arrays - A dictionary with keys that are gate names (strings) and values that are numpy arrays specifying - quantum gates in terms of unitary matrices. This is an additional "lookup" database of unitaries - - to add a gate to this `QubitProcessorSpec` its names still needs to appear in the `gate_names` list. - This dictionary's values specify additional (target) native gates that can be implemented in the device - as unitaries acting on ordinary pure-state-vectors, in the standard computationl basis. These unitaries - need not, and often should not, be unitaries acting on all of the qubits. E.g., a CNOT gate is specified - by a key that is the desired name for CNOT, and a value that is the standard 4 x 4 complex matrix for CNOT. - All gate names must start with 'G'. As an advanced behavior, a unitary-matrix-returning function which - takes a single argument - a tuple of label arguments - may be given instead of a single matrix to create - an operation *factory* which allows continuously-parameterized gates. This function must also return - an empty/dummy unitary when `None` is given as it's argument. - - availability : dict, optional - A dictionary whose keys are some subset of the keys (which are gate names) `nonstd_gate_unitaries` and the - strings (which are gate names) in `gate_names` and whose values are lists of qubit-label-tuples. Each - qubit-label-tuple must have length equal to the number of qubits the corresponding gate acts upon, and - causes that gate to be available to act on the specified qubits. Instead of a list of tuples, values of - `availability` may take the special values `"all-permutations"` and `"all-combinations"`, which as their - names imply, equate to all possible permutations and combinations of the appropriate number of qubit labels - (deterined by the gate's dimension). If a gate name is not present in `availability`, the default is - `"all-permutations"`. So, the availability of a gate only needs to be specified when it cannot act in every - valid way on the qubits (e.g., the device does not have all-to-all connectivity). - - geometry : {"line","ring","grid","torus"} or QubitGraph, optional - The type of connectivity among the qubits, specifying a graph used to - define neighbor relationships. Alternatively, a :class:`QubitGraph` - object with `qubit_labels` as the node labels may be passed directly. - This argument is only used as a convenient way of specifying gate - availability (edge connections are used for gates whose availability - is unspecified by `availability` or whose value there is `"all-edges"`). - - qubit_labels : list or tuple, optional - The labels (integers or strings) of the qubits. If `None`, then the integers starting with zero are used. - - nonstd_gate_symplecticreps : dict, optional - A dictionary similar to `nonstd_gate_unitaries` that supplies, instead of a unitary matrix, the symplectic - representation of a Clifford operations, given as a 2-tuple of numpy arrays. - - aux_info : dict, optional - Any additional information that should be attached to this processor spec. """ - def __init__(self, num_qubits, gate_names, nonstd_gate_unitaries=None, availability=None, geometry=None, qubit_labels=None, nonstd_gate_symplecticreps=None, prep_names=('rho0',), povm_names=('Mdefault',), instrument_names=(), nonstd_preps=None, nonstd_povms=None, nonstd_instruments=None, aux_info=None): + """ + Parameters + ---------- + num_qubits : int + The number of qubits in the device. + + gate_names : list of strings + The names of gates in the device. This may include standard gate + names known by pyGSTi (see below) or names which appear in the + `nonstd_gate_unitaries` argument. The set of standard gate names + includes, but is not limited to: + + - 'Gi' : the 1Q idle operation + - 'Gx','Gy','Gz' : 1-qubit pi/2 rotations + - 'Gxpi','Gypi','Gzpi' : 1-qubit pi rotations + - 'Gh' : Hadamard + - 'Gp' : phase or S-gate (i.e., ((1,0),(0,i))) + - 'Gcphase','Gcnot','Gswap' : standard 2-qubit gates + + Alternative names can be used for all or any of these gates, but + then they must be explicitly defined in the `nonstd_gate_unitaries` + dictionary. Including any standard names in `nonstd_gate_unitaries` + overrides the default (builtin) unitary with the one supplied. + + nonstd_gate_unitaries: dictionary of numpy arrays + A dictionary with keys that are gate names (strings) and values that are numpy arrays specifying + quantum gates in terms of unitary matrices. This is an additional "lookup" database of unitaries - + to add a gate to this `QubitProcessorSpec` its names still needs to appear in the `gate_names` list. + This dictionary's values specify additional (target) native gates that can be implemented in the device + as unitaries acting on ordinary pure-state-vectors, in the standard computationl basis. These unitaries + need not, and often should not, be unitaries acting on all of the qubits. E.g., a CNOT gate is specified + by a key that is the desired name for CNOT, and a value that is the standard 4 x 4 complex matrix for CNOT. + All gate names must start with 'G'. As an advanced behavior, a unitary-matrix-returning function which + takes a single argument - a tuple of label arguments - may be given instead of a single matrix to create + an operation *factory* which allows continuously-parameterized gates. This function must also return + an empty/dummy unitary when `None` is given as it's argument. + + availability : dict, optional + A dictionary whose keys are some subset of the keys (which are gate names) `nonstd_gate_unitaries` and the + strings (which are gate names) in `gate_names` and whose values are lists of qubit-label-tuples. Each + qubit-label-tuple must have length equal to the number of qubits the corresponding gate acts upon, and + causes that gate to be available to act on the specified qubits. Instead of a list of tuples, values of + `availability` may take the special values `"all-permutations"` and `"all-combinations"`, which as their + names imply, equate to all possible permutations and combinations of the appropriate number of qubit labels + (deterined by the gate's dimension). If a gate name is not present in `availability`, the default is + `"all-permutations"`. So, the availability of a gate only needs to be specified when it cannot act in every + valid way on the qubits (e.g., the device does not have all-to-all connectivity). + + geometry : {"line","ring","grid","torus"} or QubitGraph, optional + The type of connectivity among the qubits, specifying a graph used to + define neighbor relationships. Alternatively, a :class:`QubitGraph` + object with `qubit_labels` as the node labels may be passed directly. + This argument is only used as a convenient way of specifying gate + availability (edge connections are used for gates whose availability + is unspecified by `availability` or whose value there is `"all-edges"`). + + qubit_labels : list or tuple, optional + The labels (integers or strings) of the qubits. If `None`, then the integers starting with zero are used. + + nonstd_gate_symplecticreps : dict, optional + A dictionary similar to `nonstd_gate_unitaries` that supplies, instead of a unitary matrix, the symplectic + representation of a Clifford operations, given as a 2-tuple of numpy arrays. + #TODO: Better explanation of this specifier. + + prep_names : list or tuple of str, optional (default ('rho0',)) + List of strings corresponding to the names of the native state preparation + operations supported by this processor specification. State preparation names + must start with 'rho'. + + povm_names : list or tuple of str, optional (default ('Mdefault',)) + List of strings corresponding to the names of the native POVMs + supported by this processor specification. POVM names must start with + 'M'. + + instrument_names : list or tuple of str, optional (default ()) + List of strings corresponding to the names of the quantum instruments + supported by this processor specification. Instrument names must start with + 'I'. + + nonstd_preps : dict, optional (default None) + Dictionary mapping preparation names (as specified in `prep_names`) to corresponding + state preparations. The values of this dictionary can be the following specifiers: + + - numpy ndarray: numpy vector corresponding to the dense representation of the pure + state corresponding to this state preparation, written in the standard/computational + basis. + - string specifiers: For string state preparation specifiers there are two prefixes supported + which determine the parsing applied for conversion to a corresponding state preparation.: + The first prefix is 'rho_'. When this prefix is used, any digits proceeding in the string + are interpreted as the bitstring labeling a standard basis state. + E.g. 'rho_01' corresponds to the state |01>. The second prefix is 'rho' (w/o the underscore). When this + prefix is used the following digits are interpreted as an integer. This integer is then converted + into a bitstring labeling the corresponding standard basis state (with the conversion using right-LSB convention). + + nonstd_povms : dict, optional (default None) + Dictionary mapping POVM names (as specified in `povm_names`) to corresponding POVMs. The values of + this dictionary can be the following specifiers: + + - string specifiers: Presently two special string specifiers are supported, 'Mdefault' and 'Mz', + both of which map to POVMs for computational-basis readout with the appropriate dimensions. + - dictionary: A dictionary whose keys are effect labels, and whose values are specifiers used to construct + corresponding effects. Effect specifiers can themselves take two forms: + - list of numpy arrays: List of numpy arrays corresponding to the component pure states whose corresponding + projectors (density matrix vectors) are summed to produce this POVM effect. When there is only a single + such pure state then one can alternatively use that numpy array directly as the value (without wrapping + in a list) for convenience. E.g. to construct a two-qubit, two-outcome, POVM corresponding to parity readout wherein + each POVM effect corresponds to a rank-2 projector onto the even or odd computational subspace the complete POVM + specifier would be: + {'even': [np.array([1,0,0,0], np.array([0,0,0,1])], 'odd': [np.array([0,1,0,0], np.array([0,0,1,0])]} + -list of string specifiers: Alternatively one can specify a list of effect specifiers using special string + notation. String specifiers can take three formats: + 1. They can be strings which directly correspond to the desired output bit/ditstring in the standard basis. + E.g. "0000" or "1001". + 2. Strings prefixed by either 'E_' or 'E' (w/o an underscore). In the first case any digits proceeding the + "E_" are interpretted as a bitstring. + If prefixed by "E" (w/o an underscore) the proceeding digits are interpreted as an integer and converted into + a base d number using right-LSB convention. E.g. 'E_0000' corresponds to the state |0000>, and E15 corresponds + to |1111> (assuming this was acting on 4-qubits). + + nonstd_instruments : dict, optional (default None) + Dictionary mapping instrument names (as specified in `instrument_names`) to corresponding instruments. The values + of this dictionary can be the following specifiers: + + - string specifiers: Presently only one special string specifier is supported, 'Iz'. This corresponds to a quantum + instrument for computational-basis readout. + - dictionary: A dictionary whose keys are instrument effect labels, and whose values are specifiers used to construct the + corresponding instrument effects. Instrument effect specifiers can take the following form: + - numpy array: A numpy array corresponding to the dense representation of the instrument effect. + - lists of 2-tuples: Each tuple in this list of 2-tuples is such that the first element corresponds to a POVM effect + specifier (see `nonstd_povms` for supported options), and the second element is a state preparation specifier + (see `nonstd_preps` for supported options). These specifiers are used to construct appropriate effect and preparation + representations which are then have their outer product taken. This is done for each 2-tuple, and the outer products are + then summed to get the overall instrument effect. In the case where there is only a single 2-tuple for an instrument effect + this tuple can be used directly as the dictionary value (w/o being wrapped in a list) for convenience. + + aux_info : dict, optional + Any additional information that should be attached to this processor spec. + """ assert(type(num_qubits) is int), "The number of qubits, n, should be an integer!" assert(not (num_qubits > 1 and availability is None and geometry is None)), \ "For multi-qubit processors you must specify either the geometry or the availability!" @@ -1273,3 +1366,4 @@ def compute_2Q_connectivity(self): twoQ_connectivity[qubit_labels.index(sslbls[0]), qubit_labels.index(sslbls[1])] = True return _qgraph.QubitGraph(qubit_labels, twoQ_connectivity) + diff --git a/test/unit/construction/test_modelconstruction.py b/test/unit/construction/test_modelconstruction.py index 77f2717e8..cff5fad14 100644 --- a/test/unit/construction/test_modelconstruction.py +++ b/test/unit/construction/test_modelconstruction.py @@ -523,7 +523,7 @@ def test_spamvecs_in_processorspecs(self): prep_names=("rhoA", "rhoC"), povm_names=("Ma", "Mc"), nonstd_preps={'rhoA': "rho0", 'rhoC': prep_vec}, nonstd_povms={'Ma': {'0': "0000", '1': EA}, - 'Mc': {'OutA': "0000", 'OutB': [EA, EB]}}) + 'Mc': {'OutA': "E_0000", 'OutB': [EA, EB], 'OutC': 'E13'}}) pspec_vecs = save_and_load_pspec(pspec_vecs) # make sure serialization works too self.assertEqual(pspec_vecs.prep_names, ('rhoA', 'rhoC')) @@ -540,7 +540,7 @@ def test_spamvecs_in_processorspecs(self): self.assertArraysAlmostEqual(mdl_vecs.prep_blks['layers']['rhoC'].to_dense(), prep_supervec) self.assertEqual(list(mdl_vecs.povm_blks['layers']['Ma'].keys()), ['0', '1']) - self.assertEqual(list(mdl_vecs.povm_blks['layers']['Mc'].keys()), ['OutA', 'OutB']) + self.assertEqual(list(mdl_vecs.povm_blks['layers']['Mc'].keys()), ['OutA', 'OutB', 'OutC']) def Zeffect(index): v = np.zeros(2**4, complex); v[index] = 1.0 @@ -551,6 +551,7 @@ def Zeffect(index): self.assertArraysAlmostEqual(mdl_vecs.povm_blks['layers']['Mc']['OutA'].to_dense(), Zeffect(0)) self.assertArraysAlmostEqual(mdl_vecs.povm_blks['layers']['Mc']['OutB'].to_dense(), Zeffect(14) + Zeffect(15)) + self.assertArraysAlmostEqual(mdl_vecs.povm_blks['layers']['Mc']['OutC'].to_dense(), Zeffect(13)) def test_instruments_in_processorspecs(self): #Instruments From c2e6de4314335b3fa189c27647470d4706f51600 Mon Sep 17 00:00:00 2001 From: pcwysoc <144378483+pcwysoc@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:25:37 -0500 Subject: [PATCH 7/7] processorspec fix --- pygsti/processors/processorspec.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pygsti/processors/processorspec.py b/pygsti/processors/processorspec.py index 7d9a27df1..801ea0ed0 100644 --- a/pygsti/processors/processorspec.py +++ b/pygsti/processors/processorspec.py @@ -310,7 +310,6 @@ def _serialize_instrument(obj): nonstd_preps = {k: _serialize_state(obj) for k, obj in self.nonstd_preps.items()} nonstd_povms = {k: _serialize_povm(obj) for k, obj in self.nonstd_povms.items()} nonstd_instruments = {':'.join(k): _serialize_instrument(obj) for k, obj in self.nonstd_instruments.items()} - state.update({'qudit_labels': list(self.qudit_labels), 'qudit_udims': list(self.qudit_udims), 'gate_names': list(self.gate_names), # Note: not labels, just strings, so OK @@ -400,7 +399,6 @@ def _tuplize(x): availability = {k: _tuplize(v) for k, v in state['availability'].items()} geometry = _qgraph.QubitGraph.from_nice_serialization(state['geometry']) - return cls(state['qudit_labels'], state['qudit_udims'], state['gate_names'], nonstd_gate_unitaries, availability, geometry, state['prep_names'], state['povm_names'], [tuple(iname) for iname in state['instrument_names']], @@ -1019,10 +1017,11 @@ def _tuplize(x): _warnings.warn(("Loading an old-format QubitProcessorSpec that doesn't contain SPAM information." " You should check to make sure you don't want/need to add this information and" " then re-save this processor spec.")) - + instrument_names = state.get('instrument_names', []) + instrument_names = [tuple(name) if isinstance(name,list) else name for name in instrument_names] return cls(len(state['qubit_labels']), state['gate_names'], nonstd_gate_unitaries, availability, geometry, state['qubit_labels'], symplectic_reps, state.get('prep_names', []), - state.get('povm_names', []), state.get('instrument_names', []), nonstd_preps, nonstd_povms, + state.get('povm_names', []), instrument_names, nonstd_preps, nonstd_povms, nonstd_instruments, state['aux_info']) @property