tequila-basic 1.9.7__py3-none-any.whl → 1.9.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,429 @@
1
+ from tequila.simulators.simulator_base import BackendExpectationValue, BackendCircuit
2
+ from tequila.wavefunction.qubit_wavefunction import QubitWaveFunction
3
+ from tequila.utils import TequilaException
4
+ from tequila.hamiltonian import PauliString
5
+ from tequila.circuit._gates_impl import ExponentialPauliGateImpl, QGateImpl, RotationGateImpl, QubitHamiltonian
6
+ from tequila.circuit.gates import QubitExcitationImpl
7
+ from tequila import BitNumbering
8
+
9
+
10
+ import hashlib
11
+ import numpy
12
+ import os
13
+ import spex_tequila
14
+ import gc
15
+
16
+ numbering = BitNumbering.MSB
17
+
18
+ class TequilaSpexException(TequilaException):
19
+ """Custom exception for SPEX simulator errors"""
20
+ pass
21
+
22
+ def extract_pauli_dict(ps):
23
+ """
24
+ Extract qubit:operator mapping from PauliString/QubitHamiltonian
25
+ Args:
26
+ ps: PauliString or single-term QubitHamiltonian
27
+ Returns:
28
+ dict: {qubit: 'X'/'Y'/'Z'}
29
+ """
30
+
31
+ if isinstance(ps, PauliString):
32
+ return dict(ps.items())
33
+ if isinstance(ps, QubitHamiltonian) and len(ps.paulistrings) == 1:
34
+ return dict(ps.paulistrings[0].items())
35
+ raise TequilaSpexException("Unsupported generator type")
36
+
37
+ def circuit_hash(abstract_circuit, variables=None):
38
+ """
39
+ Create MD5 hash for circuit caching
40
+ Uses gate types, targets, controls and generators for uniqueness
41
+ """
42
+ sha = hashlib.md5()
43
+ if abstract_circuit is None:
44
+ return None
45
+ for g in abstract_circuit.gates:
46
+ gate_str = f"{type(g).__name__}:{g.name}:{g.target}:{g.control}:{g.generator}:{getattr(g, 'parameter', None)}\n"
47
+ sha.update(gate_str.encode('utf-8'))
48
+ if variables:
49
+ for key, value in sorted(variables.items()):
50
+ sha.update(f"{key}:{value}\n".encode('utf-8'))
51
+ return sha.hexdigest()
52
+
53
+ class BackendCircuitSpex(BackendCircuit):
54
+ """SPEX circuit implementation using sparse state representation"""
55
+
56
+ # Circuit compilation configuration
57
+ compiler_arguments = {
58
+ "multitarget": True,
59
+ "multicontrol": True,
60
+ "trotterized": True,
61
+ "generalized_rotation": True,
62
+ "exponential_pauli": False,
63
+ "controlled_exponential_pauli": True,
64
+ "hadamard_power": True,
65
+ "controlled_power": True,
66
+ "power": True,
67
+ "toffoli": True,
68
+ "controlled_phase": True,
69
+ "phase": True,
70
+ "phase_to_z": True,
71
+ "controlled_rotation": True,
72
+ "swap": True,
73
+ "cc_max": True,
74
+ "ry_gate": True,
75
+ "y_gate": True,
76
+ "ch_gate": True
77
+ }
78
+
79
+ def __init__(self,
80
+ abstract_circuit=None,
81
+ variables=None,
82
+ num_threads=-1,
83
+ amplitude_threshold=1e-14,
84
+ angle_threshold=1e-14,
85
+ compress_qubits=True,
86
+ *args, **kwargs):
87
+
88
+ # Circuit chaching
89
+ self.circuit_cache = {}
90
+
91
+ # Performance parameters
92
+ self.num_threads = num_threads
93
+ self.amplitude_threshold = amplitude_threshold
94
+ self.angle_threshold = angle_threshold
95
+
96
+ # State compression
97
+ self.compress_qubits = compress_qubits
98
+ self.n_qubits_compressed = None
99
+ self.hamiltonians = None
100
+
101
+ super().__init__(abstract_circuit=abstract_circuit, variables=variables, *args, **kwargs)
102
+
103
+ @property
104
+ def n_qubits(self):
105
+ """Get number of qubits after compression (if enabled)"""
106
+ used = set()
107
+ if hasattr(self, "circuit") and self.circuit:
108
+ for term in self.circuit:
109
+ used.update(term.pauli_map.keys())
110
+
111
+ if self.abstract_circuit is not None and hasattr(self.abstract_circuit, "gates"):
112
+ for gate in self.abstract_circuit.gates:
113
+ if hasattr(gate, "target"):
114
+ if isinstance(gate.target, (list, tuple)):
115
+ used.update(gate.target)
116
+ else:
117
+ used.add(gate.target)
118
+ if hasattr(gate, "control") and gate.control:
119
+ if isinstance(gate.control, (list, tuple)):
120
+ used.update(gate.control)
121
+ else:
122
+ used.add(gate.control)
123
+ computed = max(used) + 1 if used else 0
124
+ return max(super().n_qubits, computed)
125
+
126
+ def initialize_circuit(self, *args, **kwargs):
127
+ return []
128
+
129
+ def create_circuit(self, abstract_circuit=None, variables=None, *args, **kwargs):
130
+ """Compile circuit with caching using MD5 hash"""
131
+ if abstract_circuit is None:
132
+ abstract_circuit = self.abstract_circuit
133
+
134
+ key = circuit_hash(abstract_circuit, variables)
135
+
136
+ if key in self.circuit_cache:
137
+ return self.circuit_cache[key]
138
+
139
+ circuit = super().create_circuit(abstract_circuit=abstract_circuit, variables=variables, *args, **kwargs)
140
+
141
+ self.circuit_cache[key] = circuit
142
+
143
+ return circuit
144
+
145
+ def compress_qubit_indices(self):
146
+ """
147
+ Optimize qubit indices by mapping used qubits to contiguous range
148
+ Reduces memory usage by eliminating unused qubit dimensions
149
+ """
150
+ if not self.compress_qubits or not (hasattr(self, "circuit") and self.circuit):
151
+ return
152
+
153
+ # Collect all qubits used in circuit and Hamiltonians
154
+ used_qubits = set()
155
+ for term in self.circuit:
156
+ used_qubits.update(term.pauli_map.keys())
157
+ for ham in self.hamiltonians:
158
+ for term, _ in ham:
159
+ used_qubits.update(term.pauli_map.keys())
160
+
161
+ if not used_qubits:
162
+ self.n_qubits_compressed = 0
163
+ return
164
+
165
+ # Create qubit mapping and remap all terms
166
+ qubit_map = {old: new for new, old in enumerate(sorted(used_qubits))}
167
+
168
+ for term in self.circuit:
169
+ term.pauli_map = {qubit_map[old]: op for old, op in term.pauli_map.items()}
170
+
171
+ if self.hamiltonians is not None:
172
+ for ham in self.hamiltonians:
173
+ for term, _ in ham:
174
+ term.pauli_map = {qubit_map[old]: op for old, op in term.pauli_map.items()}
175
+
176
+ self.n_qubits_compressed = len(used_qubits)
177
+
178
+ def update_variables(self, variables, *args, **kwargs):
179
+ if variables is None:
180
+ variables = {}
181
+ super().update_variables(variables)
182
+ self.circuit = self.create_circuit(abstract_circuit=self.abstract_circuit, variables=variables)
183
+
184
+ def assign_parameter(self, param, variables):
185
+ if isinstance(param, (int, float, complex)):
186
+ return float(param)
187
+ if isinstance(param, str):
188
+ if param in variables:
189
+ return float(variables[param])
190
+ else:
191
+ raise TequilaSpexException(f"Variable '{param}' not found in variables")
192
+ if callable(param):
193
+ result = param(variables)
194
+ return float(result)
195
+
196
+ raise TequilaSpexException(f"Can't assign parameter '{param}'.")
197
+
198
+
199
+ def add_basic_gate(self, gate, circuit, *args, **kwargs):
200
+ """Convert Tequila gates to SPEX exponential Pauli terms"""
201
+ exp_term = spex_tequila.ExpPauliTerm()
202
+ if isinstance(gate, ExponentialPauliGateImpl):
203
+ if self.angle_threshold is not None and abs(gate.parameter) < self.angle_threshold:
204
+ return
205
+ exp_term.pauli_map = extract_pauli_dict(gate.paulistring)
206
+ exp_term.angle = gate.parameter
207
+ circuit.append(exp_term)
208
+
209
+ elif isinstance(gate, RotationGateImpl):
210
+ if self.angle_threshold is not None and abs(gate.parameter) < self.angle_threshold:
211
+ return
212
+ exp_term.pauli_map = extract_pauli_dict(gate.generator)
213
+ exp_term.angle = gate.parameter
214
+ circuit.append(exp_term)
215
+
216
+ elif isinstance(gate, QubitExcitationImpl):
217
+ compiled_gate = gate.compile(exponential_pauli=True)
218
+ for sub_gate in compiled_gate.abstract_circuit.gates:
219
+ self.add_basic_gate(sub_gate, circuit, *args, **kwargs)
220
+
221
+ elif isinstance(gate, QGateImpl):
222
+ if gate.name.lower() in ["x","y","z"]:
223
+ # Convert standard gates to Pauli rotations
224
+ for ps in gate.make_generator(include_controls=True).paulistrings:
225
+ angle = numpy.pi * ps.coeff
226
+ if self.angle_threshold is not None and abs(angle) < self.angle_threshold:
227
+ continue
228
+ exp_term = spex_tequila.ExpPauliTerm()
229
+ exp_term.pauli_map = dict(ps.items())
230
+ exp_term.angle = angle
231
+ circuit.append(exp_term)
232
+ elif gate.name.lower() in ["h", "hadamard"]:
233
+ assert len(gate.target)==1
234
+ target = gate.target[0]
235
+ for ps in ["-0.25*Y({q})", "Z({q})", "0.25*Y({q})"]:
236
+ ps = QubitHamiltonian(ps.format(q=gate.target[0])).paulistrings[0]
237
+ angle = numpy.pi * ps.coeff
238
+ exp_term = spex_tequila.ExpPauliTerm()
239
+ exp_term.pauli_map = dict(ps.items())
240
+ exp_term.angle = angle
241
+ circuit.append(exp_term)
242
+ else:
243
+ raise TequilaSpexException("{} not supported. Only x,y,z,h".format(gate.name.lower()))
244
+
245
+ else:
246
+ raise TequilaSpexException(f"Unsupported gate object type: {type(gate)}. "
247
+ "All gates should be compiled to exponential pauli or rotation gates.")
248
+
249
+
250
+
251
+ def add_parametrized_gate(self, gate, circuit, *args, **kwargs):
252
+ """Convert Tequila parametrized gates to SPEX exponential Pauli terms"""
253
+ exp_term = spex_tequila.ExpPauliTerm()
254
+ if isinstance(gate, ExponentialPauliGateImpl):
255
+ angle = self.assign_parameter(gate.parameter, kwargs.get("variables", {}))
256
+ if self.angle_threshold is not None and abs(angle) < self.angle_threshold:
257
+ return
258
+ exp_term.pauli_map = extract_pauli_dict(gate.paulistring)
259
+ exp_term.angle = angle
260
+ circuit.append(exp_term)
261
+
262
+ elif isinstance(gate, RotationGateImpl):
263
+ angle = self.assign_parameter(gate.parameter, kwargs.get("variables", {}))
264
+ if self.angle_threshold is not None and abs(angle) < self.angle_threshold:
265
+ return
266
+ exp_term.pauli_map = extract_pauli_dict(gate.generator)
267
+ exp_term.angle = angle
268
+ circuit.append(exp_term)
269
+
270
+ elif isinstance(gate, QubitExcitationImpl):
271
+ compiled_gate = gate.compile(exponential_pauli=True)
272
+ for sub_gate in compiled_gate.abstract_circuit.gates:
273
+ self.add_parametrized_gate(sub_gate, circuit, *args, **kwargs)
274
+
275
+ elif isinstance(gate, QGateImpl):
276
+ for ps in gate.make_generator(include_controls=True).paulistrings:
277
+ if self.angle_threshold is not None and abs(gate.parameter) < self.angle_threshold:
278
+ continue
279
+ exp_term = spex_tequila.ExpPauliTerm()
280
+ exp_term.pauli_map = dict(ps.items())
281
+ exp_term.angle = gate.parameter
282
+ circuit.append(exp_term)
283
+
284
+ else:
285
+ raise TequilaSpexException(f"Unsupported gate type: {type(gate)}. "
286
+ "Only Exponential Pauli and Rotation gates are allowed after compilation.")
287
+
288
+
289
+ def do_simulate(self, variables, initial_state=0, *args, **kwargs) -> QubitWaveFunction:
290
+ """
291
+ Simulate circuit and return final state
292
+ Args:
293
+ initial_state: Starting state (int or QubitWaveFunction)
294
+ Returns:
295
+ QubitWaveFunction: Sparse state representation
296
+ """
297
+
298
+ if self.compress_qubits and self.n_qubits_compressed is not None and self.n_qubits_compressed > 0:
299
+ n_qubits = self.n_qubits_compressed
300
+ else:
301
+ n_qubits = self.n_qubits
302
+
303
+ # Initialize state
304
+ if isinstance(initial_state, (int, numpy.integer)):
305
+ if initial_state == 0:
306
+ state = spex_tequila.initialize_zero_state(n_qubits)
307
+ else:
308
+ state = {initial_state: 1.0 + 0j}
309
+ else:
310
+ # initial_state is already a QubitWaveFunction
311
+ state = {k: v for k, v in initial_state.raw_items()}
312
+
313
+ # Apply circuit with amplitude thresholding, -1.0 disables threshold in spex_tequila
314
+ threshold = self.amplitude_threshold if self.amplitude_threshold is not None else -1.0
315
+ final_state = spex_tequila.apply_U(self.circuit, state, threshold, n_qubits)
316
+
317
+ wfn_MSB = QubitWaveFunction(n_qubits=n_qubits, numbering=BitNumbering.MSB)
318
+ for state, amplitude in final_state.items():
319
+ wfn_MSB[state] = amplitude
320
+
321
+ del final_state
322
+ gc.collect()
323
+
324
+ return wfn_MSB
325
+
326
+ def simulate(self, variables, initial_state=0, *args, **kwargs) -> QubitWaveFunction:
327
+ """Override simulate to avoid automatic mapping by KeyMapSubregisterToRegister"""
328
+ self.update_variables(variables)
329
+ result = self.do_simulate(variables=variables, initial_state=initial_state, *args, **kwargs)
330
+ return result
331
+
332
+
333
+ class BackendExpectationValueSpex(BackendExpectationValue):
334
+ """SPEX expectation value calculator using sparse simulations"""
335
+ BackendCircuitType = BackendCircuitSpex
336
+
337
+ def __init__(self, *args,
338
+ num_threads=-1,
339
+ amplitude_threshold=1e-14,
340
+ angle_threshold=1e-14,
341
+ compress_qubits=True,
342
+ **kwargs):
343
+ super().__init__(*args, **kwargs)
344
+
345
+ self.num_threads = num_threads
346
+ self.amplitude_threshold = amplitude_threshold
347
+ self.angle_threshold = angle_threshold
348
+
349
+ # Configure circuit parameters
350
+ if isinstance(self.U, BackendCircuitSpex):
351
+ self.U.num_threads = num_threads
352
+ self.U.amplitude_threshold = amplitude_threshold
353
+ self.U.angle_threshold = angle_threshold
354
+ self.U.compress_qubits = compress_qubits
355
+
356
+ def initialize_hamiltonian(self, hamiltonians):
357
+ """
358
+ Initializes the Hamiltonian terms for the simulation.
359
+ Args:
360
+ hamiltonians: A list of Hamiltonian objects.
361
+ Returns:
362
+ tuple: A converted list of (pauli_string, coefficient) tuples.
363
+ """
364
+ # Convert Tequila Hamiltonians into a list of (pauli_string, coeff) tuples for spex_tequila.
365
+ converted = []
366
+ for H in hamiltonians:
367
+ terms = []
368
+ for ps in H.paulistrings:
369
+ # Construct Pauli string like "X(0)Y(1)"
370
+ pauli_map = dict(ps.items())
371
+ term = spex_tequila.ExpPauliTerm()
372
+ term.pauli_map = pauli_map
373
+ terms.append((term, ps.coeff))
374
+ converted.append(terms)
375
+
376
+ if isinstance(self.U, BackendCircuitSpex):
377
+ self.U.hamiltonians = converted
378
+
379
+ return tuple(converted)
380
+
381
+
382
+ def simulate(self, variables, initial_state=0, *args, **kwargs):
383
+ """
384
+ Calculate expectation value through sparse simulation
385
+ Returns:
386
+ numpy.ndarray: Expectation values for each Hamiltonian term
387
+ """
388
+
389
+ # Prepare simulation
390
+ self.update_variables(variables)
391
+ if self.U.compress_qubits:
392
+ self.U.compress_qubit_indices()
393
+
394
+ if self.U.compress_qubits and self.U.n_qubits_compressed is not None and self.U.n_qubits_compressed > 0:
395
+ n_qubits = self.U.n_qubits_compressed
396
+ else:
397
+ n_qubits = self.U.n_qubits
398
+
399
+ # Prepare the initial state
400
+ if isinstance(initial_state, int):
401
+ if initial_state == 0:
402
+ state = spex_tequila.initialize_zero_state(n_qubits)
403
+ else:
404
+ state = {initial_state: 1.0 + 0j}
405
+ else:
406
+ # initial_state is a QubitWaveFunction
407
+ state = {k: v for k, v in initial_state.raw_items()}
408
+
409
+ self.U.circuit = [t for t in self.U.circuit if abs(t.angle) >= self.U.angle_threshold]
410
+
411
+ threshold = self.amplitude_threshold if self.amplitude_threshold is not None else -1.0
412
+ final_state = spex_tequila.apply_U(self.U.circuit, state, threshold, n_qubits)
413
+ del state
414
+
415
+ if "SPEX_NUM_THREADS" in os.environ:
416
+ self.num_threads = int(os.environ["SPEX_NUM_THREADS"])
417
+ elif "OMP_NUM_THREADS" in os.environ:
418
+ self.num_threads = int(os.environ["OMP_NUM_THREADS"])
419
+
420
+ # Calculate the expectation value for each Hamiltonian
421
+ results = []
422
+ for H_terms in self.H:
423
+ val = spex_tequila.expectation_value_parallel(final_state, final_state, H_terms, n_qubits, num_threads=-1)
424
+ results.append(val.real)
425
+
426
+ del final_state
427
+ gc.collect()
428
+
429
+ return numpy.array(results)
@@ -42,37 +42,30 @@ class BackendCircuitSymbolic(BackendCircuit):
42
42
 
43
43
  @classmethod
44
44
  def apply_gate(cls, state: QubitWaveFunction, gate: QGate, qubits: dict, variables) -> QubitWaveFunction:
45
- result = QubitWaveFunction()
46
45
  n_qubits = len(qubits.keys())
46
+ result = sympy.Integer(1) * QubitWaveFunction(n_qubits)
47
47
  for s, v in state.items():
48
- s.nbits = n_qubits
49
- result += v * cls.apply_on_standard_basis(gate=gate, basisfunction=s, qubits=qubits, variables=variables)
48
+ result += v * cls.apply_on_standard_basis(gate=gate, basis_state=s, qubits=qubits, variables=variables)
50
49
  return result
51
50
 
52
51
  @classmethod
53
- def apply_on_standard_basis(cls, gate: QGate, basisfunction: BitString, qubits:dict, variables) -> QubitWaveFunction:
54
-
55
- basis_array = basisfunction.array
56
- if gate.is_controlled():
57
- do_apply = True
58
- check = [basis_array[qubits[c]] == 1 for c in gate.control]
59
- for c in check:
60
- if not c:
61
- do_apply = False
62
- if not do_apply:
63
- return QubitWaveFunction.from_int(basisfunction)
52
+ def apply_on_standard_basis(cls, gate: QGate, basis_state: BitString, qubits:dict, variables) -> QubitWaveFunction:
53
+ n_qubits = len(qubits.keys())
54
+ basis_array = basis_state.array
55
+ if gate.is_controlled() and not all(basis_array[qubits[c]] == 1 for c in gate.control):
56
+ return QubitWaveFunction.from_basis_state(n_qubits, basis_state)
64
57
 
65
58
  if len(gate.target) > 1:
66
59
  raise Exception("Multi-targets not supported for symbolic simulators")
67
60
 
68
- result = QubitWaveFunction()
61
+ result = sympy.Integer(1) * QubitWaveFunction(n_qubits)
69
62
  for tt in gate.target:
70
63
  t = qubits[tt]
71
64
  qt = basis_array[t]
72
65
  a_array = copy.deepcopy(basis_array)
73
66
  a_array[t] = (a_array[t] + 1) % 2
74
- current_state = QubitWaveFunction.from_int(basisfunction)
75
- altered_state = QubitWaveFunction.from_int(BitString.from_array(a_array))
67
+ current_state = QubitWaveFunction.from_basis_state(n_qubits, basis_state)
68
+ altered_state = QubitWaveFunction.from_basis_state(n_qubits, BitString.from_array(a_array))
76
69
 
77
70
  fac1 = None
78
71
  fac2 = None
@@ -115,22 +108,21 @@ class BackendCircuitSymbolic(BackendCircuit):
115
108
  count = 0
116
109
  for q in self.abstract_circuit.qubits:
117
110
  qubits[q] = count
118
- count +=1
111
+ count += 1
119
112
 
120
113
  n_qubits = len(self.abstract_circuit.qubits)
121
114
 
122
115
  if initial_state is None:
123
- initial_state = QubitWaveFunction.from_int(i=0, n_qubits=n_qubits)
124
- elif isinstance(initial_state, int):
125
- initial_state = QubitWaveFunction.from_int(initial_state, n_qubits=n_qubits)
116
+ initial_state = 0
117
+ initial_state = QubitWaveFunction.from_basis_state(n_qubits, initial_state)
126
118
 
127
119
  result = initial_state
128
120
  for g in self.abstract_circuit.gates:
129
121
  result = self.apply_gate(state=result, gate=g, qubits=qubits, variables=variables)
130
122
 
131
- wfn = QubitWaveFunction()
123
+ wfn = QubitWaveFunction(n_qubits)
132
124
  if self.convert_to_numpy:
133
- for k,v in result.items():
125
+ for k, v in result.items():
134
126
  wfn[k] = complex(v)
135
127
  else:
136
128
  wfn = result