tequila-basic 1.9.4__py3-none-any.whl → 1.9.6__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.
@@ -420,7 +420,10 @@ class QuantumChemistryMadness(QuantumChemistryBase):
420
420
 
421
421
  """
422
422
  # check if the used qubit encoding has a hcb transformation
423
- have_hcb_trafo = self.transformation.hcb_to_me() is not None
423
+ try:
424
+ have_hcb_trafo = self.transformation.hcb_to_me() is not None
425
+ except:
426
+ have_hcb_trafo = False
424
427
  name = name.upper()
425
428
 
426
429
  # Default Method
@@ -37,7 +37,7 @@ class OptimizeOrbitalsResult:
37
37
  self.iterations += 1
38
38
 
39
39
  def optimize_orbitals(molecule, circuit=None, vqe_solver=None, pyscf_arguments=None, silent=False,
40
- vqe_solver_arguments=None, initial_guess=None, return_mcscf=False, use_hcb=False, molecule_factory=None, molecule_arguments=None, *args, **kwargs):
40
+ vqe_solver_arguments=None, initial_guess=None, return_mcscf=False, use_hcb=False, molecule_factory=None, molecule_arguments=None, restrict_to_active_space=True, *args, **kwargs):
41
41
  """
42
42
 
43
43
  Parameters
@@ -78,7 +78,12 @@ def optimize_orbitals(molecule, circuit=None, vqe_solver=None, pyscf_arguments=N
78
78
  if pyscf_arguments is None:
79
79
  pyscf_arguments = {"max_cycle_macro": 10, "max_cycle_micro": 3}
80
80
  no = molecule.n_orbitals
81
- pyscf_molecule = QuantumChemistryPySCF.from_tequila(molecule=molecule, transformation=molecule.transformation)
81
+
82
+ if not isinstance(molecule, QuantumChemistryPySCF):
83
+ pyscf_molecule = QuantumChemistryPySCF.from_tequila(molecule=molecule, transformation=molecule.transformation)
84
+ else:
85
+ pyscf_molecule = molecule
86
+
82
87
  mf = pyscf_molecule._get_hf()
83
88
  result=OptimizeOrbitalsResult()
84
89
  mc = mcscf.CASSCF(mf, pyscf_molecule.n_orbitals, pyscf_molecule.n_electrons)
@@ -140,10 +145,11 @@ def optimize_orbitals(molecule, circuit=None, vqe_solver=None, pyscf_arguments=N
140
145
  mc.kernel()
141
146
  # make new molecule
142
147
 
143
- transformed_molecule = pyscf_molecule.transform_orbitals(orbital_coefficients=mc.mo_coeff)
148
+ mo_coeff = mc.mo_coeff
149
+ transformed_molecule = pyscf_molecule.transform_orbitals(orbital_coefficients=mo_coeff, name="optimized")
144
150
  result.molecule=transformed_molecule
145
151
  result.old_molecule=molecule
146
- result.mo_coeff=mc.mo_coeff
152
+ result.mo_coeff=mo_coeff
147
153
  result.energy=mc.e_tot
148
154
 
149
155
  if return_mcscf:
@@ -75,6 +75,7 @@ class QuantumChemistryPySCF(QuantumChemistryBase):
75
75
  kwargs["two_body_integrals"] = g_ao
76
76
  kwargs["one_body_integrals"] = h_ao
77
77
  kwargs["orbital_coefficients"] = mo_coeff
78
+ kwargs["orbital_type"] = "hf"
78
79
 
79
80
  if "nuclear_repulsion" not in kwargs:
80
81
  kwargs["nuclear_repulsion"] = mol.energy_nuc()
@@ -17,7 +17,7 @@ from .encodings import known_encodings
17
17
 
18
18
  import typing, numpy, numbers
19
19
  from itertools import product
20
-
20
+ import tequila.grouping.fermionic_functions as ff
21
21
 
22
22
 
23
23
  try:
@@ -32,8 +32,7 @@ except:
32
32
  except Exception as E:
33
33
  raise Exception("{}\nIssue with Tequila Chemistry: Please update openfermion".format(str(E)))
34
34
  import warnings
35
-
36
-
35
+ OPTIMIZED_ORDERING = "Optimized"
37
36
  class QuantumChemistryBase:
38
37
  """
39
38
  Base Class for tequila chemistry functionality
@@ -94,7 +93,7 @@ class QuantumChemistryBase:
94
93
  else:
95
94
  self.integral_manager = self.initialize_integral_manager(active_orbitals=active_orbitals,
96
95
  reference_orbitals=reference_orbitals,
97
- orbitals=orbitals, frozen_orbitals=frozen_orbitals, orbital_type=orbital_type, *args,
96
+ orbitals=orbitals, frozen_orbitals=frozen_orbitals, orbital_type=orbital_type, basis_name=self.parameters.basis_set, *args,
98
97
  **kwargs)
99
98
 
100
99
  if orbital_type is not None and orbital_type.lower() == "native":
@@ -109,15 +108,28 @@ class QuantumChemistryBase:
109
108
 
110
109
  @classmethod
111
110
  def from_tequila(cls, molecule, transformation=None, *args, **kwargs):
112
- c, h1, h2 = molecule.get_integrals()
111
+ c = molecule.integral_manager.constant_term
112
+ h1 = molecule.integral_manager.one_body_integrals
113
+ h2 = molecule.integral_manager.two_body_integrals
114
+ S = molecule.integral_manager.overlap_integrals
115
+ if "active_orbitals" not in kwargs:
116
+ active_orbitals = [o.idx_total for o in molecule.integral_manager.active_orbitals]
117
+ else:
118
+ active_orbitals = kwargs["active_orbitals"]
119
+ kwargs.pop("active_orbitals")
113
120
  if transformation is None:
114
121
  transformation = molecule.transformation
122
+ parameters = molecule.parameters
115
123
  return cls(nuclear_repulsion=c,
116
124
  one_body_integrals=h1,
117
125
  two_body_integrals=h2,
118
- n_electrons=molecule.n_electrons,
126
+ overlap_integrals = S,
127
+ orbital_coefficients = molecule.integral_manager.orbital_coefficients,
128
+ active_orbitals= active_orbitals,
119
129
  transformation=transformation,
120
- parameters=molecule.parameters, *args, **kwargs)
130
+ orbital_type=molecule.integral_manager._orbital_type,
131
+ parameters=parameters,
132
+ reference_orbitals= molecule.integral_manager.active_space.reference_orbitals,*args, **kwargs)
121
133
 
122
134
  def supports_ucc(self):
123
135
  """
@@ -433,10 +445,14 @@ class QuantumChemistryBase:
433
445
 
434
446
  generator = self.make_excitation_generator(indices=indices, remove_constant_term=control is None)
435
447
  p0 = self.make_excitation_generator(indices=indices, form="P0", remove_constant_term=control is None)
436
-
448
+ if self.transformation.up_then_down:
449
+ idx = []
450
+ for pair in indices:
451
+ idx.append((pair[0]//2+(pair[0]%2)*self.n_orbitals,pair[1]//2+(pair[1]%2)*self.n_orbitals))
452
+ else:idx = indices
437
453
  return QCircuit.wrap_gate(
438
454
  FermionicGateImpl(angle=angle, generator=generator, p0=p0,
439
- transformation=type(self.transformation).__name__.lower(), indices=indices,
455
+ transformation=type(self.transformation).__name__.lower(), indices=idx,
440
456
  assume_real=assume_real,
441
457
  control=control, **kwargs))
442
458
 
@@ -543,11 +559,13 @@ class QuantumChemistryBase:
543
559
 
544
560
  return manager
545
561
 
546
- def transform_orbitals(self, orbital_coefficients, *args, **kwargs):
562
+ def transform_orbitals(self, orbital_coefficients, ignore_active_space=False, name=None, *args, **kwargs):
547
563
  """
548
564
  Parameters
549
565
  ----------
550
- orbital_coefficients: second index is new orbital indes, first is old orbital index (summed over)
566
+ orbital_coefficients: second index is new orbital indes, first is old orbital index (summed over), indices are assumed to be defined on the active space
567
+ ignore_active_space: if true orbital_coefficients are not assumed to be given in the active space
568
+ name: str, name the new orbitals
551
569
  args
552
570
  kwargs
553
571
 
@@ -556,9 +574,20 @@ class QuantumChemistryBase:
556
574
  New molecule with transformed orbitals
557
575
  """
558
576
 
577
+ U = numpy.eye(self.integral_manager.orbital_coefficients.shape[0])
578
+ # mo_coeff by default only acts on the active space
579
+ active_indices = [o.idx_total for o in self.integral_manager.active_orbitals]
580
+
581
+ if ignore_active_space:
582
+ U = orbital_coefficients
583
+ else:
584
+ for kk,k in enumerate(active_indices):
585
+ for ll,l in enumerate(active_indices):
586
+ U[k][l] = orbital_coefficients[kk][ll]
587
+
559
588
  # can not be an instance of a specific backend (otherwise we get inconsistencies with classical methods in the backend)
560
589
  integral_manager = copy.deepcopy(self.integral_manager)
561
- integral_manager.transform_orbitals(U=orbital_coefficients)
590
+ integral_manager.transform_orbitals(U=U, name=name)
562
591
  result = QuantumChemistryBase(parameters=self.parameters, integral_manager=integral_manager, transformation=self.transformation)
563
592
  return result
564
593
 
@@ -583,7 +612,7 @@ class QuantumChemistryBase:
583
612
  else:
584
613
  integral_manager = copy.deepcopy(self.integral_manager)
585
614
  integral_manager.transform_to_native_orbitals()
586
- result = QuantumChemistryBase(parameters=self.parameters, integral_manager=integral_manager, orbital_type="native", transformation=self.transformation)
615
+ result = QuantumChemistryBase(parameters=self.parameters, integral_manager=integral_manager, transformation=self.transformation)
587
616
  return result
588
617
 
589
618
 
@@ -645,6 +674,68 @@ class QuantumChemistryBase:
645
674
  """
646
675
  return 2 * len(self.integral_manager.active_reference_orbitals)
647
676
 
677
+ def make_annihilation_op(self, orbital, coefficient=1.0):
678
+ """
679
+ Compute annihilation operator on spin-orbital in qubit representation
680
+ Spin-orbital order is always (up,down,up,down,...)
681
+ """
682
+ assert orbital<=self.n_orbitals*2
683
+ aop = openfermion.ops.FermionOperator(f'{orbital}', coefficient)
684
+ return self.transformation(aop)
685
+
686
+ def make_creation_op(self, orbital, coefficient=1.0):
687
+ """
688
+ Compute creation operator on spin-orbital in qubit representation
689
+ Spin-orbital order is always (up,down,up,down,...)
690
+ """
691
+ assert orbital<=self.n_orbitals*2
692
+ cop = openfermion.ops.FermionOperator(f'{orbital}^', coefficient)
693
+ return self.transformation(cop)
694
+
695
+ def make_number_op(self, orbital):
696
+ """
697
+ Compute number operator on spin-orbital in qubit representation
698
+ Spin-orbital order is always (up,down,up,down,...)
699
+ """
700
+ num_op = self.make_creation_op(orbital) * self.make_annihilation_op(orbital)
701
+ return num_op
702
+
703
+ def make_sz_op(self):
704
+ """
705
+ Compute the spin_z operator of the molecule in qubit representation
706
+ """
707
+ sz = QubitHamiltonian()
708
+ for i in range(0, self.n_orbitals * 2, 2):
709
+ one = 0.5 * self.make_creation_op(i) * self.make_annihilation_op(i)
710
+ two = 0.5 * self.make_creation_op(i+1) * self.make_annihilation_op(i+1)
711
+ sz += (one - two)
712
+ return sz
713
+
714
+ def make_sp_op(self):
715
+ """
716
+ Compute the spin+ operator of the molecule in qubit representation
717
+ """
718
+ sp = QubitHamiltonian()
719
+ for i in range(self.n_orbitals):
720
+ sp += self.make_creation_op(i*2) * self.make_annihilation_op(i*2 + 1)
721
+ return sp
722
+
723
+ def make_sm_op(self):
724
+ """
725
+ Compute the spin- operator of the molecule in qubit representation
726
+ """
727
+ sm = QubitHamiltonian()
728
+ for i in range(self.n_orbitals):
729
+ sm += self.make_creation_op(i*2 + 1) * self.make_annihilation_op(i*2)
730
+ return sm
731
+
732
+ def make_s2_op(self):
733
+ """
734
+ Compute the spin^2 operator of the molecule in qubit representation
735
+ """
736
+ s2_op = self.make_sm_op() * self.make_sp_op() + self.make_sz_op() * (self.make_sz_op() + 1)
737
+ return s2_op
738
+
648
739
  def make_hamiltonian(self, *args, **kwargs) -> QubitHamiltonian:
649
740
  """
650
741
  Parameters
@@ -805,13 +896,13 @@ class QuantumChemistryBase:
805
896
  """
806
897
  if U is None:
807
898
  U = QCircuit()
808
-
809
- # consistency
810
- consistency = [x < self.n_orbitals for x in U.qubits]
811
- if not all(consistency):
812
- warnings.warn(
813
- "hcb_to_me: given circuit is not defined on the first {} qubits. Is this a HCB circuit?".format(
814
- self.n_orbitals))
899
+ else:
900
+ ups = [self.transformation.up(i.idx) for i in self.orbitals]
901
+ consistency = [x in ups for x in U.qubits]
902
+ if not all(consistency):
903
+ warnings.warn(
904
+ "hcb_to_me: given circuit is not defined on all first {} qubits. Is this a HCB circuit?".format(
905
+ self.n_orbitals))
815
906
 
816
907
  # map to alpha qubits
817
908
  if condensed:
@@ -1156,7 +1247,13 @@ class QuantumChemistryBase:
1156
1247
  indices = self.make_upccgsd_indices(key=name)
1157
1248
 
1158
1249
  # check if the used qubit encoding has a hcb transformation
1159
- have_hcb_trafo = self.transformation.hcb_to_me() is not None
1250
+ have_hcb_trafo = True
1251
+ try:
1252
+ if self.transformation.hcb_to_me() is None:
1253
+ have_hcb_trafo = False
1254
+ except:
1255
+ have_hcb_trafo = False
1256
+
1160
1257
 
1161
1258
  # consistency checks for optimization
1162
1259
  if have_hcb_trafo and hcb_optimization is None and include_reference:
@@ -1404,7 +1501,8 @@ class QuantumChemistryBase:
1404
1501
  factor = 1.0 / trotter_steps
1405
1502
  for step in range(trotter_steps):
1406
1503
  for idx, angle in indices.items():
1407
- UCCSD += self.make_excitation_gate(indices=idx, angle=factor * angle)
1504
+ converted = [(idx[2 * i], idx[2 * i + 1]) for i in range(len(idx) // 2)]
1505
+ UCCSD += self.make_excitation_gate(indices=converted, angle=factor * angle)
1408
1506
  if hasattr(initial_amplitudes,
1409
1507
  "lower") and initial_amplitudes.lower() == "mp2" and parametrized and add_singles:
1410
1508
  # mp2 has no singles, need to initialize them here (if not parametrized initializling as 0.0 makes no sense though)
@@ -1638,7 +1736,8 @@ class QuantumChemistryBase:
1638
1736
  return None
1639
1737
 
1640
1738
  def compute_rdms(self, U: QCircuit = None, variables: Variables = None, spin_free: bool = True,
1641
- get_rdm1: bool = True, get_rdm2: bool = True, ordering="dirac", use_hcb: bool = False):
1739
+ get_rdm1: bool = True, get_rdm2: bool = True, ordering="dirac", use_hcb: bool = False,
1740
+ rdm_trafo: QubitHamiltonian = None, decompose=None):
1642
1741
  """
1643
1742
  Computes the one- and two-particle reduced density matrices (rdm1 and rdm2) given
1644
1743
  a unitary U. This method uses the standard ordering in physics as denoted below.
@@ -1666,6 +1765,9 @@ class QuantumChemistryBase:
1666
1765
  get_rdm1, get_rdm2 :
1667
1766
  Set whether either one or both rdm1, rdm2 should be computed. If both are needed at some point,
1668
1767
  it is recommended to compute them at once.
1768
+ rdm_trafo :
1769
+ The rdm operators can be transformed, e.g., a^dagger_i a_j -> U^dagger a^dagger_i a_j U,
1770
+ where U represents the transformation. The default is set to None, implying that U equas the identity.
1669
1771
 
1670
1772
  Returns
1671
1773
  -------
@@ -1910,8 +2012,18 @@ class QuantumChemistryBase:
1910
2012
  # Transform operator lists to QubitHamiltonians
1911
2013
  if (not use_hcb):
1912
2014
  qops = [_get_qop_hermitian(op) for op in qops]
2015
+
1913
2016
  # Compute expected values
1914
- evals = simulate(ExpectationValue(H=qops, U=U, shape=[len(qops)]), variables=variables)
2017
+ if rdm_trafo is None:
2018
+ if decompose is not None:
2019
+ print("MANIPULATED")
2020
+ X = decompose(H=qops, U=U)
2021
+ evals = simulate(X, variables=variables)
2022
+ else:
2023
+ evals = simulate(ExpectationValue(H=qops, U=U, shape=[len(qops)]), variables=variables)
2024
+ else:
2025
+ qops = [rdm_trafo.dagger()*qops[i]*rdm_trafo for i in range(len(qops))]
2026
+ evals = simulate(ExpectationValue(H=qops, U=U, shape=[len(qops)]), variables=variables)
1915
2027
 
1916
2028
  # Assemble density matrices
1917
2029
  # If self._rdm1, self._rdm2 exist, reset them if they are of the other spin-type
@@ -2044,6 +2156,58 @@ class QuantumChemistryBase:
2044
2156
  n_ri=n_ri, external_info=external_info, **kwargs)
2045
2157
  return correction.compute()
2046
2158
 
2159
+ def n_rotation(self, i, phi):
2160
+ '''
2161
+ Creates a quantum circuit that applies a phase rotation based on phi to both components (up and down) of a given qubit.
2162
+
2163
+ Parameters:
2164
+ - i (int): The index of the qubit to which the rotation will be applied.
2165
+ - phi (float): The rotation angle. The actual rotation applied will be multiplied with -2 for both components.
2166
+
2167
+ Returns:
2168
+ - QCircuit: A quantum circuit object containing the sequence of rotations applied to the up and down components of the specified qubit.
2169
+ '''
2170
+
2171
+ # Generate number operators for the up and down components of the qubit.
2172
+ n_up = self.make_number_op(2*i)
2173
+ n_down = self.make_number_op(2*i+1)
2174
+
2175
+ # Start a new circuit and apply rotations to each component.
2176
+ circuit = gates.GeneralizedRotation(generator = n_up, angle=-2*phi)
2177
+ circuit += gates.GeneralizedRotation(generator = n_down, angle=-2*phi)
2178
+ return circuit
2179
+
2180
+ def get_givens_circuit(self, unitary, tol = 1e-12, ordering = OPTIMIZED_ORDERING):
2181
+ '''
2182
+ Constructs a quantum circuit from a given real unitary matrix using Givens rotations.
2183
+
2184
+ This method decomposes a unitary matrix into a series of Givens and Rz (phase) rotations,
2185
+ then constructs and returns a quantum circuit that implements this sequence of rotations.
2186
+
2187
+ Parameters:
2188
+ - unitary (numpy.array): A real unitary matrix representing the transformation to implement.
2189
+ - tol (float): A tolerance threshold below which matrix elements are considered zero.
2190
+ - ordering (list of tuples or 'Optimized'): Custom ordering of indices for Givens rotations or 'Optimized' to generate them automatically.
2191
+
2192
+ Returns:
2193
+ - QCircuit: A quantum circuit implementing the series of rotations decomposed from the unitary.
2194
+ '''
2195
+ # Decompose the unitary matrix into Givens and phase (Rz) rotations.
2196
+ theta_list, phi_list = get_givens_decomposition(unitary, tol, ordering)
2197
+
2198
+ # Initialize an empty quantum circuit.
2199
+ circuit = QCircuit()
2200
+
2201
+ # Add all Rz (phase) rotations to the circuit.
2202
+ for phi in phi_list:
2203
+ circuit += self.n_rotation(phi[1], phi[0])
2204
+
2205
+ # Add all Givens rotations to the circuit.
2206
+ for theta in reversed(theta_list):
2207
+ circuit += self.UR(theta[1], theta[2], theta[0]*2)
2208
+
2209
+ return circuit
2210
+
2047
2211
 
2048
2212
  def print_basis_info(self):
2049
2213
  return self.integral_manager.print_basis_info()
@@ -2064,3 +2228,140 @@ class QuantumChemistryBase:
2064
2228
  result += "\nmore information with: self.print_basis_info()\n"
2065
2229
 
2066
2230
  return result
2231
+
2232
+ def givens_matrix(n, p, q, theta):
2233
+ '''
2234
+ Construct a complex Givens rotation matrix of dimension n by theta between rows/columns p and q.
2235
+ '''
2236
+ '''
2237
+ Generates a Givens rotation matrix of size n x n to rotate by angle theta in the (p, q) plane. This matrix can be complex
2238
+
2239
+ Parameters:
2240
+ - n (int): The size of the Givens rotation matrix.
2241
+ - p (int): The first index for the rotation plane.
2242
+ - q (int): The second index for the rotation plane.
2243
+ - theta (float): The rotation angle.
2244
+
2245
+ Returns:
2246
+ - numpy.array: The Givens rotation matrix.
2247
+ '''
2248
+ matrix = numpy.eye(n) # Matrix to hold complex numbers
2249
+ cos_theta = numpy.cos(theta)
2250
+ sin_theta = numpy.sin(theta)
2251
+
2252
+ # Directly assign cosine and sine without complex phase adjustment
2253
+ matrix[p, p] = cos_theta
2254
+ matrix[q, q] = cos_theta
2255
+ matrix[p, q] = sin_theta
2256
+ matrix[q, p] = -sin_theta
2257
+
2258
+ return matrix
2259
+
2260
+ def get_givens_decomposition(unitary, tol = 1e-12, ordering = OPTIMIZED_ORDERING, return_diagonal = False):
2261
+ '''
2262
+ Decomposes a real unitary matrix into Givens rotations (theta) and Rz rotations (phi).
2263
+
2264
+ Parameters:
2265
+ - unitary (numpy.array): A real unitary matrix to decompose. It cannot be complex.
2266
+ - tol (float): Tolerance for considering matrix elements as zero. Elements with absolute value less than tol are treated as zero.
2267
+ - ordering (list of tuples or 'Optimized'): Custom ordering of indices for Givens rotations or 'Optimized' to generate them automatically.
2268
+ - return_diagonal (bool): If True, the function also returns the diagonal matrix as part of the output.
2269
+
2270
+ Returns:
2271
+ - list: A list of tuples, each representing a Givens rotation. Each tuple contains the rotation angle theta and indices (i,j) of the rotation.
2272
+ - list: A list of tuples, each representing an Rz rotation. Each tuple contains the rotation angle phi and the index (i) of the rotation.
2273
+ - numpy.array (optional): The diagonal matrix after applying all Givens rotations, returned if return_diagonal is True.
2274
+ '''
2275
+ U = unitary # no need to copy as we don't modify the original
2276
+ U[abs(U) < tol] = 0 # Zeroing out the small elements as per the tolerance level.
2277
+ n = U.shape[0]
2278
+
2279
+ # Determine optimized ordering if specified.
2280
+ if ordering == OPTIMIZED_ORDERING:
2281
+ ordering = ff.depth_eff_order_mf(n)
2282
+
2283
+ theta_list = []
2284
+ phi_list = []
2285
+
2286
+ def calcTheta(U, c, r):
2287
+ '''Calculate and apply the Givens rotation for a specific matrix element.'''
2288
+ t = numpy.arctan2(-U[r,c], U[r-1,c])
2289
+ theta_list.append((t, r, r-1))
2290
+ g = givens_matrix(n,r,r-1,t)
2291
+ U = numpy.dot(g, U)
2292
+
2293
+ return U
2294
+
2295
+ # Apply and store Givens rotations as per the given or computed ordering.
2296
+ if ordering is None:
2297
+ for c in range(n):
2298
+ for r in range(n-1, c, -1):
2299
+ U = calcTheta(U, c, r)
2300
+ else:
2301
+ for r, c in ordering:
2302
+ U = calcTheta(U, c, r)
2303
+
2304
+ # Calculating the Rz rotations based on the phases of the diagonal elements.
2305
+ # For real elements this means a 180 degree shift, i.e. a sign change.
2306
+ for i in range(n):
2307
+ ph = numpy.angle(U[i,i])
2308
+ phi_list.append((ph, i))
2309
+
2310
+ # Filtering out rotations without significance.
2311
+ theta_list_new = []
2312
+ for i, theta in enumerate(theta_list):
2313
+ if abs(theta[0] % (2*numpy.pi)) > tol:
2314
+ theta_list_new.append(theta)
2315
+
2316
+ phi_list_new = []
2317
+ for i, phi in enumerate(phi_list):
2318
+ if abs(phi[0]) > tol:
2319
+ phi_list_new.append(phi)
2320
+
2321
+ if return_diagonal:
2322
+ # Optionally return the resulting diagonal
2323
+ return theta_list_new, phi_list_new, U
2324
+ else:
2325
+ return theta_list_new, phi_list_new
2326
+
2327
+ def reconstruct_matrix_from_givens(n, theta_list, phi_list, to_real_if_possible = True, tol = 1e-12):
2328
+ '''
2329
+ Reconstructs a matrix from given Givens rotations and Rz diagonal rotations.
2330
+ This function is effectively an inverse of get_givens_decomposition, and therefore only works with data in the same format as its output.
2331
+
2332
+ Parameters:
2333
+ - n (int): The size of the unitary matrix to be reconstructed.
2334
+ - theta_list (list of tuples): Each tuple contains (angle, i, j) representing a Givens rotation of `angle` radians, applied to rows/columns `i` and `j`.
2335
+ - phi_list (list of tuples): Each tuple contains (angle, i), representing an Rz rotation by `angle` radians applied to the `i`th diagonal element.
2336
+ - to_real_if_possible (bool): If True, converts the matrix to real if its imaginary part is effectively zero.
2337
+ - tol (float): The tolerance whether to swap a complex rotation for a sign change.
2338
+
2339
+ Returns:
2340
+ - numpy.ndarray: The reconstructed complex or real matrix, depending on the `to_real_if_possible` flag and matrix composition.
2341
+ '''
2342
+ # Start with an identity matrix
2343
+ reconstructed = numpy.eye(n, dtype=complex)
2344
+
2345
+ # Apply Rz rotations for diagonal elements
2346
+ for phi in phi_list:
2347
+ angle, i = phi
2348
+ # Directly apply a sign flip if the rotation angle is π
2349
+ if numpy.isclose(angle, numpy.pi, atol=tol):
2350
+ reconstructed[i, i] *= -1
2351
+ else:
2352
+ reconstructed[i, i] *= numpy.exp(1j * angle)
2353
+
2354
+ # Apply Givens rotations in reverse order
2355
+ for theta in reversed(theta_list):
2356
+ angle, i, j = theta
2357
+ g = givens_matrix(n, i, j, angle)
2358
+ reconstructed = numpy.dot(g.conj().T, reconstructed) # Transpose of Givens matrix applied to the left
2359
+
2360
+ # Convert matrix to real if its imaginary part is negligible unless disabled via to_real_if_possible
2361
+ if to_real_if_possible:
2362
+ # Directly apply a sign flip if the rotation angle is π
2363
+ if numpy.all(reconstructed.imag == 0):
2364
+ # Convert to real by taking the real part
2365
+ reconstructed = reconstructed.real
2366
+
2367
+ return reconstructed
@@ -36,9 +36,12 @@ try:
36
36
  HAS_QISKIT = True
37
37
  INSTALLED_SIMULATORS["qiskit"] = BackendTypes(BackendCircuitQiskit, BackendExpectationValueQiskit)
38
38
  INSTALLED_SAMPLERS["qiskit"] = BackendTypes(BackendCircuitQiskit, BackendExpectationValueQiskit)
39
- INSTALLED_NOISE_SAMPLERS["qiskit"] = BackendTypes(BackendCircuitQiskit, BackendExpectationValueQiskit)
39
+ from tequila.simulators.simulator_qiskit import HAS_NOISE as HAS_QISKIT_NOISE
40
+ if HAS_QISKIT_NOISE:
41
+ INSTALLED_NOISE_SAMPLERS["qiskit"] = BackendTypes(BackendCircuitQiskit, BackendExpectationValueQiskit)
40
42
  except ImportError:
41
43
  HAS_QISKIT = False
44
+ HAS_QISKIT_NOISE = False
42
45
 
43
46
  HAS_QIBO = True
44
47
  try:
@@ -133,6 +136,8 @@ def show_available_simulators():
133
136
  str(k in INSTALLED_SAMPLERS),
134
137
  str(k in INSTALLED_NOISE_SAMPLERS),
135
138
  str(k in INSTALLED_BACKENDS)))
139
+ if HAS_QISKIT and not HAS_QISKIT_NOISE:
140
+ print("missing qiskit_aer: no noisy simulation")
136
141
 
137
142
 
138
143
  def pick_backend(backend: str = None, samples: int = None, noise: NoiseModel = None, device=None,
@@ -3,12 +3,20 @@ from tequila.wavefunction.qubit_wavefunction import QubitWaveFunction
3
3
  from tequila import TequilaException, TequilaWarning
4
4
  from tequila import BitString, BitNumbering, BitStringLSB
5
5
  from tequila.utils.keymap import KeyMapRegisterToSubregister
6
- import qiskit, numpy, warnings
7
- import qiskit.providers.aer.noise as qiskitnoise
8
6
  from tequila.utils import to_float
9
- import qiskit.test.mock.backends
10
- from qiskit.providers.ibmq import IBMQBackend
7
+ import qiskit, numpy, warnings
11
8
 
9
+ HAS_NOISE=True
10
+ try:
11
+ from qiskit_aer import noise as qiskitnoise
12
+ except:
13
+ HAS_NOISE = False
14
+
15
+ HAS_IBMQ=True
16
+ try:
17
+ from qiskit.providers.ibmq import IBMQBackend
18
+ except:
19
+ HAS_IBMQ=False
12
20
 
13
21
  def get_bit_flip(p):
14
22
  """
@@ -137,7 +145,7 @@ class BackendCircuitQiskit(BackendCircuit):
137
145
  }
138
146
 
139
147
  numbering = BitNumbering.LSB
140
-
148
+
141
149
  def __init__(self, abstract_circuit: QCircuit, variables, qubit_map=None, noise=None,
142
150
  device=None, *args, **kwargs):
143
151
  """
@@ -169,7 +177,7 @@ class BackendCircuitQiskit(BackendCircuit):
169
177
  'Rx': (lambda c: c.rx, lambda c: c.mcrx),
170
178
  'Ry': (lambda c: c.ry, lambda c: c.mcry),
171
179
  'Rz': (lambda c: c.rz, lambda c: c.mcrz),
172
- 'Phase': (lambda c: c.u1, lambda c: c.cu1),
180
+ 'Phase': (lambda c: c.p, lambda c: c.cp),
173
181
  'SWAP': (lambda c: c.swap, lambda c: c.cswap),
174
182
  }
175
183
 
@@ -2,6 +2,7 @@ import numpy as np
2
2
  from tequila.circuit import gates
3
3
  from tequila.circuit.circuit import QCircuit
4
4
  from tequila.hamiltonian.qubit_hamiltonian import QubitHamiltonian
5
+ from scipy.stats import unitary_group, ortho_group
5
6
 
6
7
  def make_random_circuit(n_qubits: int, rotation_gates: list=['rx', 'ry', 'rz'], n_rotations: int=None,
7
8
  enable_controls: bool=None) -> QCircuit:
@@ -75,3 +76,19 @@ def make_random_hamiltonian(n_qubits: int , paulis: list=['X','Y','Z'], n_ps: in
75
76
 
76
77
  H = QubitHamiltonian(ham)
77
78
  return H
79
+
80
+ def generate_random_unitary(size, complex = False):
81
+ '''
82
+ Generates a random unitary (or furthermore orthogonal if complex is False) matrix of a specified size.
83
+
84
+ Parameters:
85
+ - size (int): The size of the unitary matrix to be generated.
86
+ - complex (bool, optional): Whether the unitary should be complex.
87
+
88
+ Returns:
89
+ - numpy.ndarray: A randomly generated unitary matrix.
90
+ '''
91
+ if complex:
92
+ return unitary_group.rvs(size)
93
+ else:
94
+ return ortho_group.rvs(size)
tequila/version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "1.9.4"
1
+ __version__ = "1.9.6"
2
2
  __author__ = "Tequila Developers "