Add frozen orbital support for tdscf (pyscf/pyscf#2886)

This addresses #2853 ... I *think* it's correct, but another developer should give it a careful review.

I added support for frozen orbitals to all TDSCF methods except `dhf`/`dks`. If support for those methods is of interest, perhaps I or someone else can add it later.

The main breaking change here is that the signature of `_get_x_sym_table()` has changed: it now takes an instance of a `td` object instead of `mf`, because it needs access to the frozen mask. Since it's an implied private function with its leading underscore, I assume this change isn't an issue.

[FEATURE REQUEST] Interest in core-valence separation for TDSCF?

**Describe the feature you'd like to see implemented**
I would like to add core-valence separation for TDSCF calculations in PySCF. I have working code but it's in plugin form.

**Is your feature request related to a scientific problem? Please describe.**
Computing core-level spectra greatly benefits from the core-valence separation. Excitations out of core orbitals can be approximated well by practically limiting the occupied space to those selected core orbitals. This can massively decrease computation time and memory use.

**Describe alternatives you've considered**
I've already written a [plugin](https://github.com/NathanGillispie/core-spec-pyscf/) and tests for RHF, UHF, GHF and the KS counterparts; but it's been a nightmare getting the monkeypatching to work the way I want it to. It would just be far better to implement directly in the source.

**Additional context**
So far, it works by literally deleting the non-core occupied MO coefficients, energies, and occupation numbers. It's not a great solution, but PySCF doesn't have a consistent method of determining which orbitals are occupied and which are virtual.

Perhaps another solution would be to have public `occ_idx` and `virt_idx` attributes in the TD object to avoid this issue? I don't mind submiting a PR, but I don't want to edit the source if there is no interest.

### Minimal API referenced by tests (typed signatures)
_Added only when tests call brand‑new symbols introduced by the fix; types reflect annotations present in the repo or `Unknown` if not annotated._

- Function in `pyscf/tdscf/__init__.py`: `def TDA(mf, frozen) -> Union[tdscf.rhf.TDA, tdscf.uhf.TDA]: ...`
  **Inputs:** mf: Union[scf.RHF, scf.UHF, scf.RKS, scf.UKS, dft.RKS, dft.UKS], frozen: Union[int, list, tuple]
  **Output:** Union[tdscf.rhf.TDA, tdscf.uhf.TDA]

- Function in `pyscf/tdscf/rhf.py`: `def get_frozen_mask(td) -> numpy.ndarray: ...`
  **Inputs:** td: Union[tdscf.rhf.TDHF, tdscf.uhf.TDUHF]
  **Output:** numpy.ndarray

- Function in `pyscf/tdscf/uhf.py`: `def get_frozen_mask(td) -> numpy.ndarray: ...`
  **Inputs:** td: Union[tdscf.rhf.TDHF, tdscf.uhf.TDUHF]
  **Output:** numpy.ndarray

```python
def TDA(mf, frozen) -> Union[tdscf.rhf.TDA, tdscf.uhf.TDA]: ...
def get_frozen_mask(td) -> numpy.ndarray: ...
def get_frozen_mask(td) -> numpy.ndarray: ...
```

diff --git a/examples/tddft/06-frozen.py b/examples/tddft/06-frozen.py
new file mode 100644
index 0000000000..b89fc9fc4d
--- /dev/null
+++ b/examples/tddft/06-frozen.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+'''
+A few examples to run TDDFT calculations with frozen orbitals.
+'''
+
+from pyscf import gto, dft, tddft
+
+mol = gto.Mole()
+mol.build(
+    atom = 'H 0 0 0; F 0 0 1.1',  # in Angstrom
+    basis = '631g',
+    symmetry = True,
+)
+
+mf = dft.RKS(mol)
+mf.xc = 'b3lyp'
+mf.kernel()
+
+mytd = tddft.TDDFT(mf)
+mytd.kernel()
+mytd.analyze()
+
+# Freeze the core
+mytd = tddft.TDDFT(mf, frozen=1)
+mytd.kernel()
+mytd.analyze()
+
+# Freeze high energy virtuals
+mytd = tddft.TDDFT(mf, frozen=[9, 10])
+mytd.kernel()
+mytd.analyze()
+
+# Freeze valence (CVS) to access core spectra
+mytd = tddft.TDDFT(mf, frozen=[1, 2, 3, 4])
+mytd.kernel()
+mytd.analyze()
+
+# UKS
+
+mol.spin = 2
+mol.symmetry = False
+mol.build()
+mf = dft.UKS(mol)
+mf.xc = 'b3lyp'
+mf.kernel()
+
+# Freeze the core
+mytd = tddft.TDDFT(mf, frozen=1)
+mytd.kernel()
+mytd.analyze()
diff --git a/examples/tddft/21-matrix_A_B.py b/examples/tddft/21-matrix_A_B.py
index 176da238b8..317d8e0d6e 100644
--- a/examples/tddft/21-matrix_A_B.py
+++ b/examples/tddft/21-matrix_A_B.py
@@ -43,6 +43,24 @@ def diagonalize(a, b, nroots=5):
 print('Direct diagoanlization:', diagonalize(a, b))
 print('Reference:', tddft.TDDFT(mf).kernel(nstates=5)[0])

+#
+# with frozen orbitals
+#
+frozen = 1
+mf = scf.RHF(mol).run()
+a, b = tddft.TDHF(mf, frozen).get_ab()
+print('Direct diagoanlization:', diagonalize(a, b))
+print('Reference:', tddft.TDHF(mf, frozen).kernel(nstates=5)[0])
+
+mf = dft.RKS(mol).run(xc='lda,vwn')
+a, b = tddft.TDDFT(mf, frozen).get_ab()
+print('Direct diagoanlization:', diagonalize(a, b))
+print('Reference:', tddft.TDDFT(mf, frozen).kernel(nstates=5)[0])
+
+mf = dft.RKS(mol).run(xc='b3lyp')
+a, b = tddft.TDDFT(mf, frozen).get_ab()
+print('Direct diagoanlization:', diagonalize(a, b))
+print('Reference:', tddft.TDDFT(mf, frozen).kernel(nstates=5)[0])

 #
 # UHF/UKS-TDDFT
@@ -81,3 +99,22 @@ def diagonalize(a, b, nroots=4):
 a, b = tddft.TDDFT(mf).get_ab()
 print('Direct diagoanlization:', diagonalize(a, b))
 print('Reference:', tddft.TDDFT(mf).kernel(nstates=4)[0])
+
+#
+# with frozen orbitals
+#
+frozen = 1
+mf = scf.UHF(mol).run()
+a, b = tddft.TDHF(mf, frozen).get_ab()
+print('Direct diagoanlization:', diagonalize(a, b))
+print('Reference:', tddft.TDHF(mf, frozen).kernel(nstates=4)[0])
+
+mf = dft.UKS(mol).run(xc='lda,vwn')
+a, b = tddft.TDDFT(mf, frozen).get_ab()
+print('Direct diagoanlization:', diagonalize(a, b))
+print('Reference:', tddft.TDDFT(mf, frozen).kernel(nstates=4)[0])
+
+mf = dft.UKS(mol).run(xc='b3lyp')
+a, b = tddft.TDDFT(mf, frozen).get_ab()
+print('Direct diagoanlization:', diagonalize(a, b))
+print('Reference:', tddft.TDDFT(mf, frozen).kernel(nstates=4)[0])
diff --git a/pyscf/grad/tdrhf.py b/pyscf/grad/tdrhf.py
index d4b8cff474..6ca07c69ff 100644
--- a/pyscf/grad/tdrhf.py
+++ b/pyscf/grad/tdrhf.py
@@ -45,6 +45,8 @@ def grad_elec(td_grad, x_y, singlet=True, atmlst=None,
     log = logger.new_logger(td_grad, verbose)
     time0 = logger.process_clock(), logger.perf_counter()

+    assert td_grad.base.frozen is None
+
     mol = td_grad.mol
     mf = td_grad.base._scf
     mo_coeff = mf.mo_coeff
diff --git a/pyscf/grad/tdrks.py b/pyscf/grad/tdrks.py
index eab9f88e19..d8c60d6705 100644
--- a/pyscf/grad/tdrks.py
+++ b/pyscf/grad/tdrks.py
@@ -49,6 +49,8 @@ def grad_elec(td_grad, x_y, singlet=True, atmlst=None,
     log = logger.new_logger(td_grad, verbose)
     time0 = logger.process_clock(), logger.perf_counter()

+    assert td_grad.base.frozen is None
+
     mol = td_grad.mol
     mf = td_grad.base._scf
     mo_coeff = mf.mo_coeff
diff --git a/pyscf/pbc/tdscf/rhf.py b/pyscf/pbc/tdscf/rhf.py
index 8923fca076..b35e0a85d5 100644
--- a/pyscf/pbc/tdscf/rhf.py
+++ b/pyscf/pbc/tdscf/rhf.py
@@ -139,8 +139,8 @@ def add_hf_(a, b, hyb=1):
 class TDBase(rhf.TDBase):
     _keys = {'cell'}

-    def __init__(self, mf):
-        rhf.TDBase.__init__(self, mf)
+    def __init__(self, mf, frozen=None):
+        rhf.TDBase.__init__(self, mf, frozen)
         self.cell = mf.cell

     def get_ab(self, mf=None):
diff --git a/pyscf/pbc/tdscf/rks.py b/pyscf/pbc/tdscf/rks.py
index e204fde334..fa4181724e 100644
--- a/pyscf/pbc/tdscf/rks.py
+++ b/pyscf/pbc/tdscf/rks.py
@@ -32,13 +32,13 @@ class CasidaTDDFT(TDDFT):

 TDDFTNoHybrid = CasidaTDDFT

-def tddft(mf):
+def tddft(mf, frozen=None):
     '''Driver to create TDDFT or CasidaTDDFT object'''
     kpt = getattr(mf, 'kpt', None)
     if not mf._numint.libxc.is_hybrid_xc(mf.xc) and gamma_point(kpt):
-        return CasidaTDDFT(mf)
+        return CasidaTDDFT(mf, frozen)
     else:
-        return TDDFT(mf)
+        return TDDFT(mf, frozen)

 dft.rks.RKS.TDA           = lib.class_as_method(rhf.TDA)
 dft.rks.RKS.TDHF          = None
diff --git a/pyscf/pbc/tdscf/uhf.py b/pyscf/pbc/tdscf/uhf.py
index 9304d131bb..85c77a20c3 100644
--- a/pyscf/pbc/tdscf/uhf.py
+++ b/pyscf/pbc/tdscf/uhf.py
@@ -242,6 +242,8 @@ def get_ab(self, mf=None):
     _gen_vind = uhf.TDA.gen_vind
     gen_vind = td_rhf.TDA.gen_vind

+    get_frozen_mask = uhf.TDA.get_frozen_mask
+
 CIS = TDA

@@ -256,6 +258,8 @@ class TDHF(TDBase):
     _gen_vind = uhf.TDHF.gen_vind
     gen_vind = td_rhf.TDA.gen_vind

+    get_frozen_mask = uhf.TDHF.get_frozen_mask
+
 RPA = TDUHF = TDHF

diff --git a/pyscf/pbc/tdscf/uks.py b/pyscf/pbc/tdscf/uks.py
index 01b94b546a..39d05b1ac6 100644
--- a/pyscf/pbc/tdscf/uks.py
+++ b/pyscf/pbc/tdscf/uks.py
@@ -33,12 +33,12 @@ class CasidaTDDFT(TDDFT):

 TDDFTNoHybrid = CasidaTDDFT

-def tddft(mf):
+def tddft(mf, frozen=None):
     '''Driver to create TDDFT or CasidaTDDFT object'''
     if mf._numint.libxc.is_hybrid_xc(mf.xc):
-        return TDDFT(mf)
+        return TDDFT(mf, frozen)
     else:
-        return CasidaTDDFT(mf)
+        return CasidaTDDFT(mf, frozen)

 dft.uks.UKS.TDA           = lib.class_as_method(TDA)
 dft.uks.UKS.TDHF          = None
diff --git a/pyscf/tdscf/__init__.py b/pyscf/tdscf/__init__.py
index 5435eeb303..c522130e3e 100644
--- a/pyscf/tdscf/__init__.py
+++ b/pyscf/tdscf/__init__.py
@@ -37,53 +37,53 @@
     pass

-def TDHF(mf):
+def TDHF(mf, frozen=None):
     if isinstance(mf, scf.hf.KohnShamDFT):
         raise RuntimeError('TDHF does not support DFT object %s' % mf)
     mf = mf.remove_soscf()
     if isinstance(mf, scf.rohf.ROHF):
         # Is it correct to call TDUHF for ROHF?
         mf = mf.to_uhf()
-    return mf.TDHF()
+    return mf.TDHF(frozen)

-def TDA(mf):
+def TDA(mf, frozen=None):
     mf = mf.remove_soscf()
     if isinstance(mf, scf.rohf.ROHF):
         if isinstance(mf, KohnShamDFT):
             mf = mf.to_uks()
         else:
             mf = mf.to_uhf()
-    return mf.TDA()
+    return mf.TDA(frozen)

-def TDDFT(mf):
+def TDDFT(mf, frozen=None):
     if isinstance(mf, KohnShamDFT):
         mf = mf.remove_soscf()
         if isinstance(mf, scf.rohf.ROHF):
             mf = mf.to_uks()
-        return mf.TDDFT()
+        return mf.TDDFT(frozen)
     else:
-        return TDHF(mf)
+        return TDHF(mf, frozen)

 TD = TDDFT

-def RPA(mf):
-    return TDDFT(mf)
+def RPA(mf, frozen=None):
+    return TDDFT(mf, frozen)

-def dRPA(mf):
+def dRPA(mf, frozen=None):
     mf = mf.remove_soscf()
     if isinstance(mf, scf.rohf.ROHF):
         if isinstance(mf, KohnShamDFT):
             mf = mf.to_uks()
         else:
             mf = mf.to_uhf()
-    return mf.dRPA()
+    return mf.dRPA(frozen)

-def dTDA(mf):
+def dTDA(mf, frozen=None):
     mf = mf.remove_soscf()
     if isinstance(mf, scf.rohf.ROHF):
         if isinstance(mf, KohnShamDFT):
             mf = mf.to_uks()
         else:
             mf = mf.to_uhf()
-    return mf.dTDA()
+    return mf.dTDA(frozen)
diff --git a/pyscf/tdscf/ghf.py b/pyscf/tdscf/ghf.py
index e32b8da1a9..726baf899f 100644
--- a/pyscf/tdscf/ghf.py
+++ b/pyscf/tdscf/ghf.py
@@ -54,9 +54,10 @@ def _gen_tda_operation(td, fock_ao=None, wfnsym=None):
     assert fock_ao is None
     mf = td._scf
     mol = mf.mol
-    mo_coeff = mf.mo_coeff
-    mo_energy = mf.mo_energy
-    mo_occ = mf.mo_occ
+    mask = td.get_frozen_mask()
+    mo_coeff = mf.mo_coeff[:, mask]
+    mo_energy = mf.mo_energy[mask]
+    mo_occ = mf.mo_occ[mask]
     nao, nmo = mo_coeff.shape
     occidx = numpy.where(mo_occ == 1)[0]
     viridx = numpy.where(mo_occ == 0)[0]
@@ -69,7 +70,7 @@ def _gen_tda_operation(td, fock_ao=None, wfnsym=None):
         if isinstance(wfnsym, str):
             wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
         wfnsym = wfnsym % 10  # convert to D2h subgroup
-        sym_forbid = _get_x_sym_table(mf) != wfnsym
+        sym_forbid = _get_x_sym_table(td) != wfnsym

     e_ia = hdiag = mo_energy[viridx] - mo_energy[occidx,None]
     if wfnsym is not None and mol.symmetry:
@@ -95,15 +96,17 @@ def vind(zs):

     return vind, hdiag

-def _get_x_sym_table(mf):
+def _get_x_sym_table(td):
     '''Irrep (up to D2h symmetry) of each coefficient in X[nocc,nvir]'''
+    mf = td._scf
     mol = mf.mol
-    mo_occ = mf.mo_occ
-    orbsym = ghf_symm.get_orbsym(mol, mf.mo_coeff)
+    mask = td.get_frozen_mask()
+    mo_occ = mf.mo_occ[mask]
+    orbsym = ghf_symm.get_orbsym(mol, mf.mo_coeff[:, mask])
     orbsym = numpy.asarray(orbsym) % 10  # convert to D2h irreps
     return orbsym[mo_occ==1,None] ^ orbsym[mo_occ==0]

-def get_ab(mf, mo_energy=None, mo_coeff=None, mo_occ=None):
+def get_ab(mf, frozen=None, mo_energy=None, mo_coeff=None, mo_occ=None):
     r'''A and B matrices for TDDFT response function.

     A[i,a,j,b] = \delta_{ab}\delta_{ij}(E_a - E_i) + (ai||jb)
@@ -113,6 +116,22 @@ def get_ab(mf, mo_energy=None, mo_coeff=None, mo_occ=None):
     if mo_coeff is None: mo_coeff = mf.mo_coeff
     if mo_occ is None: mo_occ = mf.mo_occ

+    mo_coeff0 = numpy.copy(mo_coeff)
+    mo_occ0 = numpy.copy(mo_occ)
+
+    if frozen is not None:
+        # see get_frozen_mask()
+        moidx = numpy.ones(mf.mo_occ.size, dtype=bool)
+        if isinstance(frozen, (int, numpy.integer)):
+            moidx[:frozen] = False
+        elif hasattr(frozen, '__len__'):
+            moidx[list(frozen)] = False
+        else:
+            raise NotImplementedError
+        mo_energy = mo_energy[moidx]
+        mo_coeff = mo_coeff[:, moidx]
+        mo_occ = mo_occ[moidx]
+
     mol = mf.mol
     nmo = mo_occ.size
     nao = mol.nao
@@ -172,7 +191,7 @@ def add_hf_(a, b, hyb=1):
             b = b.astype(numpy.complex128)

         xctype = ni._xc_type(mf.xc)
-        dm0 = mf.make_rdm1(mo_coeff, mo_occ)
+        dm0 = mf.make_rdm1(mo_coeff0, mo_occ0)
         mem_now = lib.current_memory()[0]
         max_memory = max(2000, mf.max_memory*.8-mem_now)

@@ -383,8 +402,9 @@ def get_init_guess(self, mf, nstates=None, wfnsym=None, return_symmetry=False):
         if nstates is None: nstates = self.nstates
         if wfnsym is None: wfnsym = self.wfnsym

-        mo_energy = mf.mo_energy
-        mo_occ = mf.mo_occ
+        mask = self.get_frozen_mask()
+        mo_energy = mf.mo_energy[mask]
+        mo_occ = mf.mo_occ[mask]
         occidx = numpy.where(mo_occ==1)[0]
         viridx = numpy.where(mo_occ==0)[0]
         e_ia = (mo_energy[viridx] - mo_energy[occidx,None]).ravel()
@@ -392,7 +412,7 @@ def get_init_guess(self, mf, nstates=None, wfnsym=None, return_symmetry=False):
         nstates = min(nstates, nov)

         if (wfnsym is not None or return_symmetry) and mf.mol.symmetry:
-            x_sym = _get_x_sym_table(mf).ravel()
+            x_sym = _get_x_sym_table(self).ravel()
             if wfnsym is not None:
                 if isinstance(wfnsym, str):
                     wfnsym = symm.irrep_name2id(mf.mol.groupname, wfnsym)
@@ -444,7 +464,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym = _get_x_sym_table(self._scf).ravel()
+            x_sym = _get_x_sym_table(self).ravel()
             x0sym = [rhf._guess_wfnsym_id(self, x_sym, x) for x in x0]

         self.converged, self.e, x1 = lr_eigh(
@@ -452,8 +472,9 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        nocc = (self._scf.mo_occ>0).sum()
-        nmo = self._scf.mo_occ.size
+        mo_occ = self._scf.mo_occ[self.get_frozen_mask()]
+        nocc = (mo_occ>0).sum()
+        nmo = mo_occ.size
         nvir = nmo - nocc
         self.xy = [(xi.reshape(nocc,nvir), 0) for xi in x1]

@@ -481,9 +502,10 @@ def gen_tdhf_operation(mf, fock_ao=None, wfnsym=None, with_nlc=True):
 def _gen_tdhf_operation(td, fock_ao=None, wfnsym=None):
     mf = td._scf
     mol = mf.mol
-    mo_coeff = mf.mo_coeff
-    mo_energy = mf.mo_energy
-    mo_occ = mf.mo_occ
+    mask = td.get_frozen_mask()
+    mo_coeff = mf.mo_coeff[:, mask]
+    mo_energy = mf.mo_energy[mask]
+    mo_occ = mf.mo_occ[mask]
     nao, nmo = mo_coeff.shape
     occidx = numpy.where(mo_occ == 1)[0]
     viridx = numpy.where(mo_occ == 0)[0]
@@ -496,7 +518,7 @@ def _gen_tdhf_operation(td, fock_ao=None, wfnsym=None):
         if isinstance(wfnsym, str):
             wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
         wfnsym = wfnsym % 10  # convert to D2h subgroup
-        sym_forbid = _get_x_sym_table(mf) != wfnsym
+        sym_forbid = _get_x_sym_table(td) != wfnsym

     assert fock_ao is None

@@ -591,7 +613,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym = y_sym = _get_x_sym_table(self._scf).ravel()
+            x_sym = y_sym = _get_x_sym_table(self).ravel()
             x_sym = numpy.append(x_sym, y_sym)
             x0sym = [rhf._guess_wfnsym_id(self, x_sym, x) for x in x0]

@@ -600,8 +622,9 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        nocc = (self._scf.mo_occ>0).sum()
-        nmo = self._scf.mo_occ.size
+        mo_occ = self._scf.mo_occ[self.get_frozen_mask()]
+        nocc = (mo_occ>0).sum()
+        nmo = mo_occ.size
         nvir = nmo - nocc
         self.e = w
         def norm_xy(z):
diff --git a/pyscf/tdscf/gks.py b/pyscf/tdscf/gks.py
index 4f389b5739..3f2141acaa 100644
--- a/pyscf/tdscf/gks.py
+++ b/pyscf/tdscf/gks.py
@@ -45,10 +45,11 @@ def gen_vind(self, mf=None):
             mf = self._scf
         wfnsym = self.wfnsym
         mol = mf.mol
-        mo_coeff = mf.mo_coeff
+        mask = self.get_frozen_mask()
+        mo_coeff = mf.mo_coeff[:, mask]
         assert mo_coeff.dtype == numpy.double
-        mo_energy = mf.mo_energy
-        mo_occ = mf.mo_occ
+        mo_energy = mf.mo_energy[mask]
+        mo_occ = mf.mo_occ[mask]
         nao, nmo = mo_coeff.shape
         occidx = numpy.where(mo_occ==1)[0]
         viridx = numpy.where(mo_occ==0)[0]
@@ -61,7 +62,7 @@ def gen_vind(self, mf=None):
             if isinstance(wfnsym, str):
                 wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
             wfnsym = wfnsym % 10  # convert to D2h subgroup
-            sym_forbid = ghf._get_x_sym_table(mf) != wfnsym
+            sym_forbid = ghf._get_x_sym_table(self) != wfnsym

         e_ia = (mo_energy[viridx].reshape(-1,1) - mo_energy[occidx]).T
         if wfnsym is not None and mol.symmetry:
@@ -124,7 +125,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym = ghf._get_x_sym_table(self._scf).ravel()
+            x_sym = ghf._get_x_sym_table(self).ravel()
             x0sym = [rhf._guess_wfnsym_id(self, x_sym, x) for x in x0]

         self.converged, w2, x1 = lr_eigh(
@@ -132,8 +133,9 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        mo_energy = self._scf.mo_energy
-        mo_occ = self._scf.mo_occ
+        mask = self.get_frozen_mask()
+        mo_energy = self._scf.mo_energy[mask]
+        mo_occ = self._scf.mo_occ[mask]
         occidx = numpy.where(mo_occ==1)[0]
         viridx = numpy.where(mo_occ==0)[0]
         e_ia = (mo_energy[viridx,None] - mo_energy[occidx]).T
@@ -167,14 +169,14 @@ def nuc_grad_method(self):
 TDDFTNoHybrid = CasidaTDDFT

-def tddft(mf):
+def tddft(mf, frozen=None):
     '''Driver to create TDDFT or CasidaTDDFT object'''
     if (not mf._numint.libxc.is_hybrid_xc(mf.xc) and
         # Casida formula can be applied for real orbitals only
         mf.mo_coeff.dtype == numpy.double and mf.collinear[0] != 'm'):
-        return CasidaTDDFT(mf)
+        return CasidaTDDFT(mf, frozen)
     else:
-        return TDDFT(mf)
+        return TDDFT(mf, frozen)

 from pyscf import dft
 dft.gks.GKS.TDA           = dft.gks_symm.GKS.TDA           = lib.class_as_method(TDA)
diff --git a/pyscf/tdscf/rhf.py b/pyscf/tdscf/rhf.py
index 42b51f2dbc..e09cdb3098 100644
--- a/pyscf/tdscf/rhf.py
+++ b/pyscf/tdscf/rhf.py
@@ -60,10 +60,11 @@ def _gen_tda_operation(td, fock_ao=None, singlet=True, wfnsym=None):
     assert fock_ao is None
     mf = td._scf
     mol = mf.mol
-    mo_coeff = mf.mo_coeff
+    mask = td.get_frozen_mask()
+    mo_coeff = mf.mo_coeff[:, mask]
     # assert (mo_coeff.dtype == numpy.double)
-    mo_energy = mf.mo_energy
-    mo_occ = mf.mo_occ
+    mo_energy = mf.mo_energy[mask]
+    mo_occ = mf.mo_occ[mask]
     nao, nmo = mo_coeff.shape
     occidx = numpy.where(mo_occ==2)[0]
     viridx = numpy.where(mo_occ==0)[0]
@@ -76,7 +77,7 @@ def _gen_tda_operation(td, fock_ao=None, singlet=True, wfnsym=None):
         if isinstance(wfnsym, str):
             wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
         wfnsym = wfnsym % 10  # convert to D2h subgroup
-        x_sym = _get_x_sym_table(mf)
+        x_sym = _get_x_sym_table(td)
         sym_forbid = x_sym != wfnsym

     e_ia = hdiag = mo_energy[viridx] - mo_energy[occidx,None]
@@ -84,7 +85,6 @@ def _gen_tda_operation(td, fock_ao=None, singlet=True, wfnsym=None):
         hdiag[sym_forbid] = 0
     hdiag = hdiag.ravel()

-    mo_coeff = numpy.asarray(numpy.hstack((orbo,orbv)), order='F')
     vresp = td.gen_response(singlet=singlet, hermi=0)

     def vind(zs):
@@ -104,15 +104,36 @@ def vind(zs):

     return vind, hdiag

-def _get_x_sym_table(mf):
+def get_frozen_mask(td):
+    '''Get boolean mask for the restricted reference orbitals.
+
+    In the returned boolean (mask) array of frozen orbital indices, the
+    element is False if it corresponds to the frozen orbital.
+
+    See mp2.get_frozen_mask
+    '''
+    moidx = numpy.ones(td._scf.mo_occ.size, dtype=bool)
+    if td.frozen is None:
+        pass
+    elif isinstance(td.frozen, (int, numpy.integer)):
+        moidx[:td.frozen] = False
+    elif hasattr(td.frozen, '__len__'):
+        moidx[list(td.frozen)] = False
+    else:
+        raise NotImplementedError
+    return moidx
+
+def _get_x_sym_table(td):
     '''Irrep (up to D2h symmetry) of each coefficient in X[nocc,nvir]'''
+    mf = td._scf
     mol = mf.mol
-    mo_occ = mf.mo_occ
-    orbsym = hf_symm.get_orbsym(mol, mf.mo_coeff)
+    mask = td.get_frozen_mask()
+    mo_occ = mf.mo_occ[mask]
+    orbsym = hf_symm.get_orbsym(mol, mf.mo_coeff[:, mask])
     orbsym = orbsym % 10  # convert to D2h irreps
     return orbsym[mo_occ==2,None] ^ orbsym[mo_occ==0]

-def get_ab(mf, mo_energy=None, mo_coeff=None, mo_occ=None):
+def get_ab(mf, frozen=None, mo_energy=None, mo_coeff=None, mo_occ=None):
     r'''A and B matrices for TDDFT response function.

     A[i,a,j,b] = \delta_{ab}\delta_{ij}(E_a - E_i) + (ai||jb)
@@ -125,6 +146,22 @@ def get_ab(mf, mo_energy=None, mo_coeff=None, mo_occ=None):
     if mo_occ is None: mo_occ = mf.mo_occ
     # assert (mo_coeff.dtype == numpy.double)

+    mo_coeff0 = numpy.copy(mo_coeff)
+    mo_occ0 = numpy.copy(mo_occ)
+
+    if frozen is not None:
+        # see get_frozen_mask()
+        moidx = numpy.ones(mf.mo_occ.size, dtype=bool)
+        if isinstance(frozen, (int, numpy.integer)):
+            moidx[:frozen] = False
+        elif hasattr(frozen, '__len__'):
+            moidx[list(frozen)] = False
+        else:
+            raise NotImplementedError
+        mo_energy = mo_energy[moidx]
+        mo_coeff = mo_coeff[:, moidx]
+        mo_occ = mo_occ[moidx]
+
     assert mo_coeff.dtype == numpy.float64
     mol = mf.mol
     nao, nmo = mo_coeff.shape
@@ -164,7 +201,7 @@ def add_hf_(a, b, hyb=1):
                 b -= numpy.einsum('jaib->iajb', eri_mo[:nocc,nocc:,:nocc,nocc:]) * k_fac

         xctype = ni._xc_type(mf.xc)
-        dm0 = mf.make_rdm1(mo_coeff, mo_occ)
+        dm0 = mf.make_rdm1(mo_coeff0, mo_occ0)
         make_rho = ni._gen_rho_evaluator(mol, dm0, hermi=1, with_lapl=False)[0]
         mem_now = lib.current_memory()[0]
         max_memory = max(2000, mf.max_memory*.8-mem_now)
@@ -277,8 +314,9 @@ def get_nto(tdobj, state=1, threshold=OUTPUT_THRESHOLD, verbose=None):
         state_id = state - 1

     mol = tdobj.mol
-    mo_coeff = tdobj._scf.mo_coeff
-    mo_occ = tdobj._scf.mo_occ
+    mask = tdobj.get_frozen_mask()
+    mo_coeff = tdobj._scf.mo_coeff[:, mask]
+    mo_occ = tdobj._scf.mo_occ[mask]
     orbo = mo_coeff[:,mo_occ==2]
     orbv = mo_coeff[:,mo_occ==0]
     nocc = orbo.shape[1]
@@ -369,8 +407,9 @@ def get_nto(tdobj, state=1, threshold=OUTPUT_THRESHOLD, verbose=None):
 def analyze(tdobj, verbose=None):
     log = logger.new_logger(tdobj, verbose)
     mol = tdobj.mol
-    mo_coeff = tdobj._scf.mo_coeff
-    mo_occ = tdobj._scf.mo_occ
+    mask = tdobj.get_frozen_mask()
+    mo_coeff = tdobj._scf.mo_coeff[:, mask]
+    mo_occ = tdobj._scf.mo_occ[mask]
     nocc = numpy.count_nonzero(mo_occ == 2)

     e_ev = numpy.asarray(tdobj.e) * nist.HARTREE2EV
@@ -544,8 +583,9 @@ def _contract_multipole(tdobj, ints, hermi=True, xy=None):
     if not tdobj.singlet:
         return numpy.zeros((nstates,) + pol_shape)

-    mo_coeff = tdobj._scf.mo_coeff
-    mo_occ = tdobj._scf.mo_occ
+    mask = tdobj.get_frozen_mask()
+    mo_coeff = tdobj._scf.mo_coeff[:, mask]
+    mo_occ = tdobj._scf.mo_occ[mask]
     orbo = mo_coeff[:,mo_occ==2]
     orbv = mo_coeff[:,mo_occ==0]

@@ -673,10 +713,10 @@ class TDBase(lib.StreamObject):

     _keys = {
         'conv_tol', 'nstates', 'singlet', 'lindep', 'level_shift',
-        'max_cycle', 'mol', 'chkfile', 'wfnsym', 'converged', 'e', 'xy',
+        'max_cycle', 'mol', 'chkfile', 'frozen', 'wfnsym', 'converged', 'e', 'xy',
     }

-    def __init__(self, mf):
+    def __init__(self, mf, frozen=None):
         self.verbose = mf.verbose
         self.stdout = mf.stdout
         self.mol = mf.mol
@@ -684,6 +724,7 @@ def __init__(self, mf):
         self.max_memory = mf.max_memory
         self.chkfile = mf.chkfile

+        self.frozen = frozen
         self.wfnsym = None

         # xy = (X,Y), normalized to 1/2: 2(XX-YY) = 1
@@ -699,6 +740,12 @@ def nroots(self):
     def nroots(self, x):
         self.nstates = x

+    def set_frozen(self, method='auto', window=(-1000.0, 1000.0)):
+        from pyscf.cc.ccsd import set_frozen
+        from pyscf.tdscf import ghf, dhf
+        is_ghf = isinstance(self, (ghf.TDA, ghf.TDHF, dhf.TDA, dhf.TDHF))
+        return set_frozen(self, method=method, window=window, is_gcc=is_ghf)
+
     @property
     def e_tot(self):
         '''Excited state energies'''
@@ -722,6 +769,8 @@ def dump_flags(self, verbose=None):
         log.info('eigh level_shift = %g', self.level_shift)
         log.info('eigh max_cycle = %d', self.max_cycle)
         log.info('chkfile = %s', self.chkfile)
+        if self.frozen is not None:
+            log.info('frozen orbitals %s', self.frozen)
         log.info('max_memory %d MB (current use %d MB)',
                  self.max_memory, lib.current_memory()[0])
         if not self._scf.converged:
@@ -739,6 +788,8 @@ def reset(self, mol=None):
         self._scf.reset(mol)
         return self

+    get_frozen_mask = get_frozen_mask
+
     def gen_vind(self, mf=None):
         raise NotImplementedError

@@ -752,9 +803,10 @@ def gen_response(self, *args, **kwargs):
         return mf.gen_response(*args, with_nlc=not self.exclude_nlc, **kwargs)

     @lib.with_doc(get_ab.__doc__)
-    def get_ab(self, mf=None):
+    def get_ab(self, mf=None, frozen=None):
         if mf is None: mf = self._scf
-        return get_ab(mf)
+        if frozen is None: frozen = self.frozen
+        return get_ab(mf, frozen=frozen)

     def get_precond(self, hdiag):
         def precond(x, e, *args):
@@ -839,8 +891,9 @@ def get_init_guess(self, mf, nstates=None, wfnsym=None, return_symmetry=False):
         if nstates is None: nstates = self.nstates
         if wfnsym is None: wfnsym = self.wfnsym

-        mo_energy = mf.mo_energy
-        mo_occ = mf.mo_occ
+        mask = self.get_frozen_mask()
+        mo_energy = mf.mo_energy[mask]
+        mo_occ = mf.mo_occ[mask]
         occidx = numpy.where(mo_occ==2)[0]
         viridx = numpy.where(mo_occ==0)[0]
         e_ia = (mo_energy[viridx] - mo_energy[occidx,None]).ravel()
@@ -848,7 +901,7 @@ def get_init_guess(self, mf, nstates=None, wfnsym=None, return_symmetry=False):
         nstates = min(nstates, nov)

         if (wfnsym is not None or return_symmetry) and mf.mol.symmetry:
-            x_sym = _get_x_sym_table(mf).ravel()
+            x_sym = _get_x_sym_table(self).ravel()
             if wfnsym is not None:
                 if isinstance(wfnsym, str):
                     wfnsym = symm.irrep_name2id(mf.mol.groupname, wfnsym)
@@ -905,7 +958,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym = _get_x_sym_table(self._scf).ravel()
+            x_sym = _get_x_sym_table(self).ravel()
             x0sym = [_guess_wfnsym_id(self, x_sym, x) for x in x0]

         self.converged, self.e, x1 = lr_eigh(
@@ -913,8 +966,9 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        nocc = (self._scf.mo_occ>0).sum()
-        nmo = self._scf.mo_occ.size
+        mo_occ = self._scf.mo_occ[self.get_frozen_mask()]
+        nocc = (mo_occ>0).sum()
+        nmo = mo_occ.size
         nvir = nmo - nocc
         # 1/sqrt(2) because self.x is for alpha excitation and 2(X^+*X) = 1
         self.xy = [(xi.reshape(nocc,nvir)*numpy.sqrt(.5),0) for xi in x1]
@@ -946,10 +1000,11 @@ def gen_tdhf_operation(mf, fock_ao=None, singlet=True, wfnsym=None,
 def _gen_tdhf_operation(td, fock_ao=None, singlet=True, wfnsym=None):
     mf = td._scf
     mol = mf.mol
-    mo_coeff = mf.mo_coeff
+    mask = td.get_frozen_mask()
+    mo_coeff = mf.mo_coeff[:, mask]
     # assert (mo_coeff.dtype == numpy.double)
-    mo_energy = mf.mo_energy
-    mo_occ = mf.mo_occ
+    mo_energy = mf.mo_energy[mask]
+    mo_occ = mf.mo_occ[mask]
     nao, nmo = mo_coeff.shape
     occidx = numpy.where(mo_occ==2)[0]
     viridx = numpy.where(mo_occ==0)[0]
@@ -962,7 +1017,7 @@ def _gen_tdhf_operation(td, fock_ao=None, singlet=True, wfnsym=None):
         if isinstance(wfnsym, str):
             wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
         wfnsym = wfnsym % 10  # convert to D2h subgroup
-        sym_forbid = _get_x_sym_table(mf) != wfnsym
+        sym_forbid = _get_x_sym_table(td) != wfnsym

     assert fock_ao is None

@@ -1092,7 +1147,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym = y_sym = _get_x_sym_table(self._scf).ravel()
+            x_sym = y_sym = _get_x_sym_table(self).ravel()
             x_sym = numpy.append(x_sym, y_sym)
             x0sym = [_guess_wfnsym_id(self, x_sym, x) for x in x0]

@@ -1101,8 +1156,9 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        nocc = numpy.count_nonzero(self._scf.mo_occ)
-        nmo = self._scf.mo_occ.size
+        mo_occ = self._scf.mo_occ[self.get_frozen_mask()]
+        nocc = numpy.count_nonzero(mo_occ)
+        nmo = mo_occ.size
         nvir = nmo - nocc

         def norm_xy(z):
diff --git a/pyscf/tdscf/rks.py b/pyscf/tdscf/rks.py
index a507efb850..90e403df20 100644
--- a/pyscf/tdscf/rks.py
+++ b/pyscf/tdscf/rks.py
@@ -56,10 +56,11 @@ def gen_vind(self, mf=None):
         singlet = self.singlet

         mol = mf.mol
-        mo_coeff = mf.mo_coeff
+        mask = self.get_frozen_mask()
+        mo_coeff = mf.mo_coeff[:, mask]
         assert mo_coeff.dtype == numpy.double
-        mo_energy = mf.mo_energy
-        mo_occ = mf.mo_occ
+        mo_energy = mf.mo_energy[mask]
+        mo_occ = mf.mo_occ[mask]
         nao, nmo = mo_coeff.shape
         occidx = numpy.where(mo_occ==2)[0]
         viridx = numpy.where(mo_occ==0)[0]
@@ -72,7 +73,7 @@ def gen_vind(self, mf=None):
             if isinstance(wfnsym, str):
                 wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
             wfnsym = wfnsym % 10  # convert to D2h subgroup
-            sym_forbid = rhf._get_x_sym_table(mf) != wfnsym
+            sym_forbid = rhf._get_x_sym_table(self) != wfnsym

         e_ia = (mo_energy[viridx].reshape(-1,1) - mo_energy[occidx]).T
         if wfnsym is not None and mol.symmetry:
@@ -130,7 +131,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym = rhf._get_x_sym_table(mf).ravel()
+            x_sym = rhf._get_x_sym_table(self).ravel()
             x0sym = [rhf._guess_wfnsym_id(self, x_sym, x) for x in x0]

         self.converged, w2, x1 = lr_eigh(
@@ -138,8 +139,9 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        mo_energy = self._scf.mo_energy
-        mo_occ = self._scf.mo_occ
+        mask = self.get_frozen_mask()
+        mo_energy = self._scf.mo_energy[mask]
+        mo_occ = self._scf.mo_occ[mask]
         occidx = numpy.where(mo_occ==2)[0]
         viridx = numpy.where(mo_occ==0)[0]
         e_ia = (mo_energy[viridx,None] - mo_energy[occidx]).T
@@ -174,7 +176,7 @@ def nuc_grad_method(self):
 TDDFTNoHybrid = CasidaTDDFT

 class dRPA(TDDFTNoHybrid):
-    def __init__(self, mf):
+    def __init__(self, mf, frozen=None):
         if not isinstance(mf, KohnShamDFT):
             raise RuntimeError("direct RPA can only be applied with DFT; for HF+dRPA, use .xc='hf'")
         mf = mf.to_rks()
@@ -182,12 +184,12 @@ def __init__(self, mf):
         # xc='0*LDA' is equivalent to xc=''
         #mf.xc = '0.0*LDA'
         mf.xc = ''
-        TDDFTNoHybrid.__init__(self, mf)
+        TDDFTNoHybrid.__init__(self, mf, frozen)

 TDH = dRPA

 class dTDA(TDA):
-    def __init__(self, mf):
+    def __init__(self, mf, frozen=None):
         if not isinstance(mf, KohnShamDFT):
             raise RuntimeError("direct TDA can only be applied with DFT; for HF+dTDA, use .xc='hf'")
         mf = mf.to_rks()
@@ -195,15 +197,15 @@ def __init__(self, mf):
         # xc='0*LDA' is equivalent to xc=''
         #mf.xc = '0.0*LDA'
         mf.xc = ''
-        TDA.__init__(self, mf)
+        TDA.__init__(self, mf, frozen)

-def tddft(mf):
+def tddft(mf, frozen=None):
     '''Driver to create TDDFT or CasidaTDDFT object'''
     if mf._numint.libxc.is_hybrid_xc(mf.xc):
-        return TDDFT(mf)
+        return TDDFT(mf, frozen)
     else:
-        return CasidaTDDFT(mf)
+        return CasidaTDDFT(mf, frozen)

 from pyscf import dft
 dft.rks.RKS.TDA           = dft.rks_symm.RKS.TDA           = lib.class_as_method(TDA)
diff --git a/pyscf/tdscf/uhf.py b/pyscf/tdscf/uhf.py
index 26c6c8adf0..21f4b831bc 100644
--- a/pyscf/tdscf/uhf.py
+++ b/pyscf/tdscf/uhf.py
@@ -50,10 +50,11 @@ def _gen_tda_operation(td, fock_ao=None, wfnsym=None):
     assert fock_ao is None
     mf = td._scf
     mol = mf.mol
-    mo_coeff = mf.mo_coeff
+    maska, maskb = td.get_frozen_mask()
+    mo_coeff = (mf.mo_coeff[0][:, maska], mf.mo_coeff[1][:, maskb])
     assert (mo_coeff[0].dtype == numpy.double)
-    mo_energy = mf.mo_energy
-    mo_occ = mf.mo_occ
+    mo_energy = (mf.mo_energy[0][maska], mf.mo_energy[1][maskb])
+    mo_occ = (mf.mo_occ[0][maska], mf.mo_occ[1][maskb])
     nao, nmo = mo_coeff[0].shape
     occidxa = numpy.where(mo_occ[0]>0)[0]
     occidxb = numpy.where(mo_occ[1]>0)[0]
@@ -72,7 +73,7 @@ def _gen_tda_operation(td, fock_ao=None, wfnsym=None):
         if isinstance(wfnsym, str):
             wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
         wfnsym = wfnsym % 10  # convert to D2h subgroup
-        x_sym_a, x_sym_b = _get_x_sym_table(mf)
+        x_sym_a, x_sym_b = _get_x_sym_table(td)
         sym_forbid = numpy.append(x_sym_a.ravel(), x_sym_b.ravel()) != wfnsym

     e_ia_a = mo_energy[0][viridxa] - mo_energy[0][occidxa,None]
@@ -112,18 +113,47 @@ def vind(zs):

     return vind, hdiag

-def _get_x_sym_table(mf):
+def get_frozen_mask(td):
+    '''Get boolean mask for the unrestricted reference orbitals.
+
+    In the returned boolean (mask) array of frozen orbital indices, the
+    element is False if it corresponds to the frozen orbital.
+    '''
+    moidxa = numpy.ones(td._scf.mo_occ[0].size, dtype=bool)
+    moidxb = numpy.ones(td._scf.mo_occ[1].size, dtype=bool)
+
+    frozen = td.frozen
+    if frozen is None:
+        pass
+    elif isinstance(frozen, (int, numpy.integer)):
+        moidxa[:frozen] = False
+        moidxb[:frozen] = False
+    elif hasattr(frozen, '__len__'):
+        if len(frozen) > 0:
+            if isinstance(frozen[0], (int, numpy.integer)):
+                frozen = (frozen, frozen)
+            moidxa[list(frozen[0])] = False
+            moidxb[list(frozen[1])] = False
+    else:
+        raise NotImplementedError
+    return moidxa,moidxb
+
+def _get_x_sym_table(td):
     '''Irrep (up to D2h symmetry) of each coefficient in X amplitude'''
+    mf = td._scf
     mol = mf.mol
-    mo_occa, mo_occb = mf.mo_occ
-    orbsyma, orbsymb = uhf_symm.get_orbsym(mol, mf.mo_coeff)
+    maska, maskb = td.get_frozen_mask()
+    mo_coeff = (mf.mo_coeff[0][:, maska], mf.mo_coeff[1][:, maskb])
+    mo_occ = (mf.mo_occ[0][maska], mf.mo_occ[1][maskb])
+    mo_occa, mo_occb = mo_occ
+    orbsyma, orbsymb = uhf_symm.get_orbsym(mol, mo_coeff)
     orbsyma = orbsyma % 10
     orbsymb = orbsymb % 10
     x_sym_a = orbsyma[mo_occa>0,None] ^ orbsyma[mo_occa==0]
     x_sym_b = orbsymb[mo_occb>0,None] ^ orbsymb[mo_occb==0]
     return x_sym_a, x_sym_b

-def get_ab(mf, mo_energy=None, mo_coeff=None, mo_occ=None):
+def get_ab(mf, frozen=None,  mo_energy=None, mo_coeff=None, mo_occ=None):
     r'''A and B matrices for TDDFT response function.

     A[i,a,j,b] = \delta_{ab}\delta_{ij}(E_a - E_i) + (ai||jb)
@@ -138,6 +168,28 @@ def get_ab(mf, mo_energy=None, mo_coeff=None, mo_occ=None):
     if mo_coeff is None: mo_coeff = mf.mo_coeff
     if mo_occ is None: mo_occ = mf.mo_occ

+    mo_coeff0 = numpy.copy(mo_coeff)
+    mo_occ0 = numpy.copy(mo_occ)
+
+    if frozen is not None:
+        # see get_frozen_mask()
+        moidxa = numpy.ones(mf.mo_occ[0].size, dtype=bool)
+        moidxb = numpy.ones(mf.mo_occ[1].size, dtype=bool)
+        if isinstance(frozen, (int, numpy.integer)):
+            moidxa[:frozen] = False
+            moidxb[:frozen] = False
+        elif hasattr(frozen, '__len__'):
+            if len(frozen) > 0:
+                if isinstance(frozen[0], (int, numpy.integer)):
+                    frozen = (frozen, frozen)
+                moidxa[list(frozen[0])] = False
+                moidxb[list(frozen[1])] = False
+        else:
+            raise NotImplementedError
+        mo_energy = (mo_energy[0][moidxa], mo_energy[1][moidxb])
+        mo_coeff = (mo_coeff[0][:, moidxa], mo_coeff[1][:, moidxb])
+        mo_occ = (mo_occ[0][moidxa], mo_occ[1][moidxb])
+
     assert mo_coeff[0].dtype == numpy.float64
     mol = mf.mol
     nao = mol.nao_nr()
@@ -214,7 +266,7 @@ def add_hf_(a, b, hyb=1):
                 b_bb -= numpy.einsum('jaib->iajb', eri_bb[:nocc_b,nocc_b:,:nocc_b,nocc_b:]) * k_fac

         xctype = ni._xc_type(mf.xc)
-        dm0 = mf.make_rdm1(mo_coeff, mo_occ)
+        dm0 = mf.make_rdm1(mo_coeff0, mo_occ0)
         make_rho = ni._gen_rho_evaluator(mol, dm0, hermi=1, with_lapl=False)[0]
         mem_now = lib.current_memory()[0]
         max_memory = max(2000, mf.max_memory*.8-mem_now)
@@ -377,8 +429,9 @@ def get_nto(tdobj, state=1, threshold=OUTPUT_THRESHOLD, verbose=None):
         state_id = state - 1

     mol = tdobj.mol
-    mo_coeff = tdobj._scf.mo_coeff
-    mo_occ = tdobj._scf.mo_occ
+    maska, maskb = tdobj.get_frozen_mask()
+    mo_coeff = (tdobj._scf.mo_coeff[0][:, maska], tdobj._scf.mo_coeff[1][:, maskb])
+    mo_occ = (tdobj._scf.mo_occ[0][maska], tdobj._scf.mo_occ[1][maskb])
     orbo_a = mo_coeff[0][:,mo_occ[0]==1]
     orbv_a = mo_coeff[0][:,mo_occ[0]==0]
     orbo_b = mo_coeff[1][:,mo_occ[1]==1]
@@ -511,8 +564,9 @@ def _set_phase_(c):
 def analyze(tdobj, verbose=None):
     log = logger.new_logger(tdobj, verbose)
     mol = tdobj.mol
-    mo_coeff = tdobj._scf.mo_coeff
-    mo_occ = tdobj._scf.mo_occ
+    maska, maskb = tdobj.get_frozen_mask()
+    mo_coeff = (tdobj._scf.mo_coeff[0][:, maska], tdobj._scf.mo_coeff[1][:, maskb])
+    mo_occ = (tdobj._scf.mo_occ[0][maska], tdobj._scf.mo_occ[1][maskb])
     nocc_a = numpy.count_nonzero(mo_occ[0] == 1)
     nocc_b = numpy.count_nonzero(mo_occ[1] == 1)

@@ -584,8 +638,9 @@ def analyze(tdobj, verbose=None):

 def _contract_multipole(tdobj, ints, hermi=True, xy=None):
     if xy is None: xy = tdobj.xy
-    mo_coeff = tdobj._scf.mo_coeff
-    mo_occ = tdobj._scf.mo_occ
+    maska, maskb = tdobj.get_frozen_mask()
+    mo_coeff = (tdobj._scf.mo_coeff[0][:, maska], tdobj._scf.mo_coeff[1][:, maskb])
+    mo_occ = (tdobj._scf.mo_occ[0][maska], tdobj._scf.mo_occ[1][maskb])
     orbo_a = mo_coeff[0][:,mo_occ[0]==1]
     orbv_a = mo_coeff[0][:,mo_occ[0]==0]
     orbo_b = mo_coeff[1][:,mo_occ[1]==1]
@@ -610,10 +665,12 @@ def _contract_multipole(tdobj, ints, hermi=True, xy=None):
 class TDBase(rhf.TDBase):

     @lib.with_doc(get_ab.__doc__)
-    def get_ab(self, mf=None):
+    def get_ab(self, mf=None, frozen=None):
         if mf is None: mf = self._scf
-        return get_ab(mf)
+        if frozen is None: frozen = self.frozen
+        return get_ab(mf, frozen=frozen)

+    get_frozen_mask = get_frozen_mask
     analyze = analyze
     get_nto = get_nto
     _contract_multipole = _contract_multipole  # needed by transition dipoles
@@ -638,8 +695,9 @@ def get_init_guess(self, mf, nstates=None, wfnsym=None, return_symmetry=False):
         if wfnsym is None: wfnsym = self.wfnsym

         mol = mf.mol
-        mo_energy = mf.mo_energy
-        mo_occ = mf.mo_occ
+        maska, maskb = self.get_frozen_mask()
+        mo_energy = (mf.mo_energy[0][maska], mf.mo_energy[1][maskb])
+        mo_occ = (mf.mo_occ[0][maska], mf.mo_occ[1][maskb])
         occidxa = numpy.where(mo_occ[0]>0)[0]
         occidxb = numpy.where(mo_occ[1]>0)[0]
         viridxa = numpy.where(mo_occ[0]==0)[0]
@@ -650,7 +708,7 @@ def get_init_guess(self, mf, nstates=None, wfnsym=None, return_symmetry=False):
         nstates = min(nstates, nov)

         if (wfnsym is not None or return_symmetry) and mf.mol.symmetry:
-            x_sym_a, x_sym_b = _get_x_sym_table(mf)
+            x_sym_a, x_sym_b = _get_x_sym_table(self)
             if wfnsym is not None:
                 if isinstance(wfnsym, str):
                     wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
@@ -706,7 +764,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym_a, x_sym_b = _get_x_sym_table(self._scf)
+            x_sym_a, x_sym_b = _get_x_sym_table(self)
             x_sym = numpy.append(x_sym_a.ravel(), x_sym_b.ravel())
             x0sym = [rhf._guess_wfnsym_id(self, x_sym, x) for x in x0]

@@ -715,9 +773,10 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        nmo = self._scf.mo_occ[0].size
-        nocca = (self._scf.mo_occ[0]>0).sum()
-        noccb = (self._scf.mo_occ[1]>0).sum()
+        maska, maskb = self.get_frozen_mask()
+        nmo = self._scf.mo_occ[0][maska].size
+        nocca = (self._scf.mo_occ[0][maska]>0).sum()
+        noccb = (self._scf.mo_occ[1][maskb]>0).sum()
         nvira = nmo - nocca
         nvirb = nmo - noccb
         self.xy = [((xi[:nocca*nvira].reshape(nocca,nvira),  # X_alpha
@@ -751,9 +810,10 @@ def gen_tdhf_operation(mf, fock_ao=None, wfnsym=None, with_nlc=True):
 def _gen_tdhf_operation(td, fock_ao=None, wfnsym=None):
     mf = td._scf
     mol = mf.mol
-    mo_coeff = mf.mo_coeff
-    mo_energy = mf.mo_energy
-    mo_occ = mf.mo_occ
+    maska, maskb = td.get_frozen_mask()
+    mo_coeff = (mf.mo_coeff[0][:, maska], mf.mo_coeff[1][:, maskb])
+    mo_energy = (mf.mo_energy[0][maska], mf.mo_energy[1][maskb])
+    mo_occ = (mf.mo_occ[0][maska], mf.mo_occ[1][maskb])
     nao, nmo = mo_coeff[0].shape
     occidxa = numpy.where(mo_occ[0]>0)[0]
     occidxb = numpy.where(mo_occ[1]>0)[0]
@@ -772,7 +832,7 @@ def _gen_tdhf_operation(td, fock_ao=None, wfnsym=None):
         if isinstance(wfnsym, str):
             wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
         wfnsym = wfnsym % 10  # convert to D2h subgroup
-        x_sym_a, x_sym_b = _get_x_sym_table(mf)
+        x_sym_a, x_sym_b = _get_x_sym_table(td)
         sym_forbid = numpy.append(x_sym_a.ravel(), x_sym_b.ravel()) != wfnsym

     e_ia_a = mo_energy[0][viridxa] - mo_energy[0][occidxa,None]
@@ -883,7 +943,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym_a, x_sym_b = _get_x_sym_table(self._scf)
+            x_sym_a, x_sym_b = _get_x_sym_table(self)
             x_sym = y_sym = numpy.append(x_sym_a.ravel(), x_sym_b.ravel())
             x_sym = numpy.append(x_sym, y_sym)
             x0sym = [rhf._guess_wfnsym_id(self, x_sym, x) for x in x0]
@@ -893,8 +953,10 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        nmo = self._scf.mo_occ[0].size
-        nocca, noccb = self._scf.nelec
+        maska, maskb = self.get_frozen_mask()
+        nmo = self._scf.mo_occ[0][maska].size
+        nocca = (self._scf.mo_occ[0][maska]>0).sum()
+        noccb = (self._scf.mo_occ[1][maskb]>0).sum()
         nvira = nmo - nocca
         nvirb = nmo - noccb
         xy = []
diff --git a/pyscf/tdscf/uks.py b/pyscf/tdscf/uks.py
index c78f6cb1a6..4999b173a7 100644
--- a/pyscf/tdscf/uks.py
+++ b/pyscf/tdscf/uks.py
@@ -53,10 +53,11 @@ def gen_vind(self, mf=None):
         wfnsym = self.wfnsym

         mol = mf.mol
-        mo_coeff = mf.mo_coeff
-        assert mo_coeff[0].dtype == numpy.double
-        mo_energy = mf.mo_energy
-        mo_occ = mf.mo_occ
+        maska, maskb = self.get_frozen_mask()
+        mo_coeff = (mf.mo_coeff[0][:, maska], mf.mo_coeff[1][:, maskb])
+        assert (mo_coeff[0].dtype == numpy.double)
+        mo_energy = (mf.mo_energy[0][maska], mf.mo_energy[1][maskb])
+        mo_occ = (mf.mo_occ[0][maska], mf.mo_occ[1][maskb])
         nao, nmo = mo_coeff[0].shape
         occidxa = numpy.where(mo_occ[0]>0)[0]
         occidxb = numpy.where(mo_occ[1]>0)[0]
@@ -75,7 +76,7 @@ def gen_vind(self, mf=None):
             if isinstance(wfnsym, str):
                 wfnsym = symm.irrep_name2id(mol.groupname, wfnsym)
             wfnsym = wfnsym % 10  # convert to D2h subgroup
-            x_sym_a, x_sym_b = uhf._get_x_sym_table(mf)
+            x_sym_a, x_sym_b = uhf._get_x_sym_table(self)
             sym_forbid = numpy.append(x_sym_a.ravel(), x_sym_b.ravel()) != wfnsym

         e_ia_a = (mo_energy[0][viridxa,None] - mo_energy[0][occidxa]).T
@@ -144,7 +145,7 @@ def pickeig(w, v, nroots, envs):
             x0, x0sym = self.get_init_guess(
                 self._scf, self.nstates, return_symmetry=True)
         elif mol.symmetry:
-            x_sym_a, x_sym_b = uhf._get_x_sym_table(self._scf)
+            x_sym_a, x_sym_b = uhf._get_x_sym_table(self)
             x_sym = numpy.append(x_sym_a.ravel(), x_sym_b.ravel())
             x0sym = [rhf._guess_wfnsym_id(self, x_sym, x) for x in x0]

@@ -153,8 +154,9 @@ def pickeig(w, v, nroots, envs):
             nroots=nstates, x0sym=x0sym, pick=pickeig, max_cycle=self.max_cycle,
             max_memory=self.max_memory, verbose=log)

-        mo_energy = self._scf.mo_energy
-        mo_occ = self._scf.mo_occ
+        maska, maskb = self.get_frozen_mask()
+        mo_energy = (self._scf.mo_energy[0][maska], self._scf.mo_energy[1][maskb])
+        mo_occ = (self._scf.mo_occ[0][maska], self._scf.mo_occ[1][maskb])
         occidxa = numpy.where(mo_occ[0]>0)[0]
         occidxb = numpy.where(mo_occ[1]>0)[0]
         viridxa = numpy.where(mo_occ[0]==0)[0]
@@ -206,30 +208,30 @@ def nuc_grad_method(self):

 class dRPA(TDDFTNoHybrid):
-    def __init__(self, mf):
+    def __init__(self, mf, frozen=None):
         if not isinstance(mf, KohnShamDFT):
             raise RuntimeError("direct RPA can only be applied with DFT; for HF+dRPA, use .xc='hf'")
         mf = mf.to_uks()
         mf.xc = ''
-        TDDFTNoHybrid.__init__(self, mf)
+        TDDFTNoHybrid.__init__(self, mf, frozen)

 TDH = dRPA

 class dTDA(TDA):
-    def __init__(self, mf):
+    def __init__(self, mf, frozen=None):
         if not isinstance(mf, KohnShamDFT):
             raise RuntimeError("direct TDA can only be applied with DFT; for HF+dTDA, use .xc='hf'")
         mf = mf.to_uks()
         mf.xc = ''
-        TDA.__init__(self, mf)
+        TDA.__init__(self, mf, frozen)

-def tddft(mf):
+def tddft(mf, frozen=None):
     '''Driver to create TDDFT or CasidaTDDFT object'''
     if mf._numint.libxc.is_hybrid_xc(mf.xc):
-        return TDDFT(mf)
+        return TDDFT(mf, frozen)
     else:
-        return CasidaTDDFT(mf)
+        return CasidaTDDFT(mf, frozen)

 from pyscf import dft
 dft.uks.UKS.TDA           = dft.uks_symm.UKS.TDA           = lib.class_as_method(TDA)

diff --git a/pyscf/tdscf/test/test_tdgks.py b/pyscf/tdscf/test/test_tdgks.py
index aa568c4675..30ee8a92bc 100644
--- a/pyscf/tdscf/test/test_tdgks.py
+++ b/pyscf/tdscf/test/test_tdgks.py
@@ -205,6 +205,18 @@ def test_tddft_with_wfnsym(self):
         es = td.kernel()[0]
         self.assertAlmostEqual(lib.fp(es), 0.45050838461527387, 6)

+    def test_set_frozen(self):
+        td = mf_bp86.TDA()
+        td.frozen = 4
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask.sum(), 22)
+        td.set_frozen()
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask.sum(), 24)
+        td.frozen = [0,1]
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask.sum(), 24)
+

 if __name__ == "__main__":
     print("Full Tests for TD-GKS")
diff --git a/pyscf/tdscf/test/test_tdrhf.py b/pyscf/tdscf/test/test_tdrhf.py
index 80b30d2600..9d4c88b8c6 100644
--- a/pyscf/tdscf/test/test_tdrhf.py
+++ b/pyscf/tdscf/test/test_tdrhf.py
@@ -96,6 +96,21 @@ def test_symmetry_init_guess(self):
         wfnsym = tdscf.rhf._analyze_wfnsym(td, x_sym, td.xy[0][0])
         self.assertEqual(wfnsym, 'A1u')

+    def test_set_frozen(self):
+        td = mf.TDA()
+        td.frozen = 1
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask.sum(), 10)
+        td.set_frozen()
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask.sum(), 10)
+        td.frozen = [0,1]
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask.sum(), 9)
+        td.frozen = [1,9]
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask.sum(), 9)
+

 if __name__ == "__main__":
     print("Full Tests for rhf-TDA and rhf-TDHF")
diff --git a/pyscf/tdscf/test/test_tduhf.py b/pyscf/tdscf/test/test_tduhf.py
index 5973414180..ff8dc88e79 100644
--- a/pyscf/tdscf/test/test_tduhf.py
+++ b/pyscf/tdscf/test/test_tduhf.py
@@ -90,6 +90,29 @@ def test_symmetry_init_guess(self):
         self.assertAlmostEqual(wfnsyma, 'A1u')
         self.assertAlmostEqual(wfnsymb, 'A1u')

+    def test_set_frozen(self):
+        td = mf.TDA()
+        td.frozen = 1
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask[0].sum(), 10)
+        self.assertEqual(mask[1].sum(), 10)
+        td.set_frozen()
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask[0].sum(), 10)
+        self.assertEqual(mask[1].sum(), 10)
+        td.frozen = [0,1]
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask[0].sum(), 9)
+        self.assertEqual(mask[1].sum(), 9)
+        td.frozen = [1,9]
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask[0].sum(), 9)
+        self.assertEqual(mask[1].sum(), 9)
+        td.frozen = [[0],[1,2]]
+        mask = td.get_frozen_mask()
+        self.assertEqual(mask[0].sum(), 10)
+        self.assertEqual(mask[1].sum(), 9)
+
 if __name__ == "__main__":
     print("Full Tests for uhf-TDA and uhf-TDHF")
     unittest.main()


Filter possible 2q decomposers more carefully in unitary synthesis (Qiskit/qiskit#10008)

This PR fixes a few bugs in transpilation.\r\n\r\nCloses #9996\r\nCloses #9935\r\n\r\nThis PR supersedes #9994 , #9984, #9986, #9982 . These PRs will be closed\r\n\r\n* Skips running some decomposers when we know they will fail. In particular, before this PR, `XXDecomposer` was used unconditionally when a `Target` is supplied to `UnitarySynthesis`. Now it is skipped when there are no controlled basis gates to send to it, except in the case that the unitary to be synthesized is a product of 1q gates.\r\n* Makes `XXDecomposer`  throw more informative errors\r\n\r\n#### Behavior of previous bugs and tests\r\n\r\n* Adds a test that this bug  #9996 is fixed. Synthesis with a basis set whose only engtangling gate is iSwap no longer throws an error, but rather runs to completion. Resynthesizing with a CX-gate in the basis does not change the circuit. But the corresponding numeric unitary is correct.\r\n\r\n* `FakePrague` and other fake backends erroneously have no entangling gate ( #9983 #10004)  Prior to this PR this caused an error to be thrown when synthesizing some circuits #9935  With this PR an error is no longer thrown. All decomposers are filtered out in `DefaultUnitarySynthesis` so that none are run (Calling `transpile` with optimization level 3) A circuit is returned by `transpile`. But it is incorrect. An issue should be filed.\r\n\r\n* Synthesizing a non-entangling unitary with a basis containing no entangling gates still works. There was an existing test for this.\r\n\r\n* I am pretty sure that an error thrown by any decomposer will cause synthesis of the entire circuit to fail. There were existing tests for some cases in the test suite. These are still in place and still have the same behavior. \r\n\r\n#### Details\r\n\r\nPreviously all analysis of the unitary to be decomposed happened in the decomposers themselves. Now we do a bit of analysis on the unitary to be decomposed in order to choose which decomposers to run. This allows filtering some that would fail. To do this we call `TwoQubitWeylDecomposition` on the unitary. Since the 2q decomposers perform this same Weyl decomposition as a first step, we allow the decomposers to take this weyl decomposition as an argument. The argument is optional to avoid breaking code using the old API.

IndexError in `XXDecomposer` during `FakePrague` `transpile()` with optimization level 3

### Environment\n\n- **Qiskit Terra version**: 0.24.0 main (@ 5672c70649b4f83a6c7146c68a6a543d4a3908fd)\r\n- **Python version**:  3.10\r\n- **Operating system**:  Linux\r\n\n\n### What is happening?\n\nDuring `transpile()` with optimization level 3 on some circuits targeting `FakePrague` the 2q unitary synthesis code is raising an index error with the main branch. This code works fine on the most recent release.\n\n### How can we reproduce the issue?\n\n```python\r\nfrom qiskit.circuit import QuantumCircuit\r\nfrom qiskit.compiler import transpile\r\nfrom qiskit.providers.fake_provider import FakePrague\r\n\r\nbackend = FakePrague()\r\nnum_qubits = 10\r\n# Circuit creation\r\nqc = QuantumCircuit(num_qubits)\r\nqc.h(0)\r\nfor i in range(num_qubits - 1):\r\n    if i % 2 == 0:\r\n        qc.cy(0, i + 1)\r\n    else:\r\n        qc.cx(0, i + 1)\r\nqc.measure_all()\r\ntqc = transpile(\r\n    qc, backend, seed_transpiler=2023, optimization_level=3\r\n)\r\n```\n\n### What should happen?\n\nThe `transpile()` shouldn't error and it should successfully run 2q synthesis and return a compiled circuit.\n\n### Any suggestions?\n\nThis is a bug that has been introduced since the 0.23.0 release (as it still works on the released version). Looking at the git log this is likely a bug that was introduced in #9175 (https://github.com/Qiskit/qiskit-terra/commit/a8f83c4469f55a62f958f3eed2df10a598811eae)

diff --git a/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py b/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py\nindex 3d5714b5c2ab..2527816530c8 100644\n--- a/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py\n+++ b/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py\n@@ -151,6 +151,13 @@ def _best_decomposition(canonical_coordinate, available_strengths):\n         canonical_coordinate = np.array(canonical_coordinate)\n \n         while True:\n+            if len(priority_queue) == 0:\n+                if len(available_strengths) == 0:\n+                    raise QiskitError(\n+                        "Attempting to synthesize entangling gate with no controlled gates in basis set."\n+                    )\n+                raise QiskitError("Unable to synthesize a 2q unitary with the supplied basis set.")\n+\n             sequence_cost, sequence = heapq.heappop(priority_queue)\n \n             strength_polytope = XXPolytope.from_strengths(*[x / 2 for x in sequence])\ndiff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py\nindex 9b42567b8697..efd92085887b 100644\n--- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py\n+++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py\n@@ -701,24 +701,25 @@ def is_controlled(gate):\n         # if we are using the approximation_degree knob, use it to scale already-given fidelities\n         if approximation_degree is not None:\n             basis_2q_fidelity = {k: v * approximation_degree for k, v in basis_2q_fidelity.items()}\n-        for basis_1q in available_1q_basis:\n-            if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX":\n-                pi2_decomposer = TwoQubitBasisDecomposer(\n-                    pi2_basis,\n-                    euler_basis=basis_1q,\n+        if basis_2q_fidelity:\n+            for basis_1q in available_1q_basis:\n+                if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX":\n+                    pi2_decomposer = TwoQubitBasisDecomposer(\n+                        pi2_basis,\n+                        euler_basis=basis_1q,\n+                        basis_fidelity=basis_2q_fidelity,\n+                        pulse_optimize=True,\n+                    )\n+                    embodiments.update({pi / 2: XXEmbodiments[type(pi2_decomposer.gate)]})\n+                else:\n+                    pi2_decomposer = None\n+                decomposer = XXDecomposer(\n                     basis_fidelity=basis_2q_fidelity,\n-                    pulse_optimize=True,\n+                    euler_basis=basis_1q,\n+                    embodiments=embodiments,\n+                    backup_optimizer=pi2_decomposer,\n                 )\n-                embodiments.update({pi / 2: XXEmbodiments[type(pi2_decomposer.gate)]})\n-            else:\n-                pi2_decomposer = None\n-            decomposer = XXDecomposer(\n-                basis_fidelity=basis_2q_fidelity,\n-                euler_basis=basis_1q,\n-                embodiments=embodiments,\n-                backup_optimizer=pi2_decomposer,\n-            )\n-            decomposers.append(decomposer)\n+                decomposers.append(decomposer)\n \n         self._decomposer_cache[qubits_tuple] = decomposers\n         return decomposers\n

diff --git a/test/python/quantum_info/xx_decompose/test_decomposer.py b/test/python/quantum_info/xx_decompose/test_decomposer.py\nindex 5c21ac13f384..c4309b327d5f 100644\n--- a/test/python/quantum_info/xx_decompose/test_decomposer.py\n+++ b/test/python/quantum_info/xx_decompose/test_decomposer.py\n@@ -22,6 +22,7 @@\n from scipy.stats import unitary_group\n \n import qiskit\n+from qiskit.circuit.quantumcircuit import QuantumCircuit\n from qiskit.quantum_info.operators import Operator\n from qiskit.quantum_info.synthesis.xx_decompose.decomposer import (\n     XXDecomposer,\n@@ -132,3 +133,25 @@ def test_compilation_improvement(self):\n         # the following are taken from Fig 14 of the XX synthesis paper\n         self.assertAlmostEqual(mean(clever_costs), 1.445e-2, delta=5e-3)\n         self.assertAlmostEqual(mean(naive_costs), 2.058e-2, delta=5e-3)\n+\n+    def test_error_on_empty_basis_fidelity(self):\n+        """Test synthesizing entangling gate with no entangling basis fails."""\n+        decomposer = XXDecomposer(basis_fidelity={})\n+        qc = QuantumCircuit(2)\n+        qc.cx(0, 1)\n+        mat = Operator(qc).to_matrix()\n+        with self.assertRaisesRegex(\n+            qiskit.exceptions.QiskitError,\n+            "Attempting to synthesize entangling gate with no controlled gates in basis set.",\n+        ):\n+            decomposer(mat)\n+\n+    def test_no_error_on_empty_basis_fidelity_trivial_target(self):\n+        """Test synthesizing non-entangling gate with no entangling basis succeeds."""\n+        decomposer = XXDecomposer(basis_fidelity={})\n+        qc = QuantumCircuit(2)\n+        qc.x(0)\n+        qc.y(1)\n+        mat = Operator(qc).to_matrix()\n+        dqc = decomposer(mat)\n+        self.assertTrue(np.allclose(mat, Operator(dqc).to_matrix()))\ndiff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py\nindex 9731f84214ee..39a2a5880172 100644\n--- a/test/python/transpiler/test_unitary_synthesis.py\n+++ b/test/python/transpiler/test_unitary_synthesis.py\n@@ -47,7 +47,12 @@\n     TrivialLayout,\n )\n from qiskit.circuit.library import (\n+    IGate,\n     CXGate,\n+    RZGate,\n+    SXGate,\n+    XGate,\n+    iSwapGate,\n     ECRGate,\n     UGate,\n     ZGate,\n@@ -55,6 +60,7 @@\n     RZZGate,\n     RXXGate,\n )\n+from qiskit.circuit import Measure\n from qiskit.circuit.controlflow import IfElseOp\n from qiskit.circuit import Parameter, Gate\n \n@@ -855,7 +861,7 @@ def __init__(self):\n         unitary_synth_pass = UnitarySynthesis(target=target)\n         result_dag = unitary_synth_pass.run(dag)\n         result_qc = dag_to_circuit(result_dag)\n-        self.assertEqual(result_qc, QuantumCircuit(2))\n+        self.assertEqual(result_qc, qc)\n \n     def test_default_does_not_fail_on_no_syntheses(self):\n         qc = QuantumCircuit(1)\n@@ -863,6 +869,51 @@ def test_default_does_not_fail_on_no_syntheses(self):\n         pass_ = UnitarySynthesis(["unknown", "gates"])\n         self.assertEqual(qc, pass_(qc))\n \n+    def test_iswap_no_cx_synthesis_succeeds(self):\n+        """Test basis set with iswap but no cx can synthesize a circuit"""\n+        target = Target()\n+        theta = Parameter("theta")\n+\n+        i_props = {\n+            (0,): InstructionProperties(duration=35.5e-9, error=0.000413),\n+            (1,): InstructionProperties(duration=35.5e-9, error=0.000502),\n+        }\n+        target.add_instruction(IGate(), i_props)\n+        rz_props = {\n+            (0,): InstructionProperties(duration=0, error=0),\n+            (1,): InstructionProperties(duration=0, error=0),\n+        }\n+        target.add_instruction(RZGate(theta), rz_props)\n+        sx_props = {\n+            (0,): InstructionProperties(duration=35.5e-9, error=0.000413),\n+            (1,): InstructionProperties(duration=35.5e-9, error=0.000502),\n+        }\n+        target.add_instruction(SXGate(), sx_props)\n+        x_props = {\n+            (0,): InstructionProperties(duration=35.5e-9, error=0.000413),\n+            (1,): InstructionProperties(duration=35.5e-9, error=0.000502),\n+        }\n+        target.add_instruction(XGate(), x_props)\n+        iswap_props = {\n+            (0, 1): InstructionProperties(duration=519.11e-9, error=0.01201),\n+            (1, 0): InstructionProperties(duration=554.66e-9, error=0.01201),\n+        }\n+        target.add_instruction(iSwapGate(), iswap_props)\n+        measure_props = {\n+            (0,): InstructionProperties(duration=5.813e-6, error=0.0751),\n+            (1,): InstructionProperties(duration=5.813e-6, error=0.0225),\n+        }\n+        target.add_instruction(Measure(), measure_props)\n+\n+        qc = QuantumCircuit(2)\n+        cxmat = Operator(CXGate()).to_matrix()\n+        qc.unitary(cxmat, [0, 1])\n+        unitary_synth_pass = UnitarySynthesis(target=target)\n+        dag = circuit_to_dag(qc)\n+        result_dag = unitary_synth_pass.run(dag)\n+        result_qc = dag_to_circuit(result_dag)\n+        self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat))\n+\n \n if __name__ == "__main__":\n     unittest.main()\n


fix 1q matrix bug in Quantum Shannon Decomposer (Qiskit/qiskit#10126)

<!--\r\n⚠ If you do not respect this template, your pull request will be closed.\r\n⚠ Your pull request title should be short detailed and understandable for all.\r\n⚠ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [x] I have added the tests to cover my changes.\r\n- [x] I have updated the documentation accordingly.\r\n- [x] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nFixes #10036\r\n\r\n\r\n### Details and comments\r\n\r\n\r\n

Quantum shannon decomposition failing for some inputs

### Environment\r\n\r\n- **Qiskit Terra version**: 0.42.1\r\n- **Python version**: 3.10.8\r\n\r\n\r\n\r\n### What is happening?\r\n\r\nqs_decomposition in [qsd.py](https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/quantum_info/synthesis/qsd.py) throws an error for some examples, seemingly due to a bug in the code\r\n\r\n### How can we reproduce the issue?\r\n\r\n```\r\nfrom qiskit.quantum_info.synthesis.qsd import qs_decomposition\r\nqs_decomposition(np.array([[0,1],[1,0]]))\r\n```\r\nOutput:\r\n```\r\nTraceback (most recent call last):\r\n  Cell In[14], line 3\r\n    qs_decomposition(np.array([[0,1],[1,0]]))\r\n  File /opt/conda/lib/python3.10/site-packages/qiskit/quantum_info/synthesis/qsd.py:122 in qs_decomposition\r\n    return _apply_a2(circ)\r\n  File /opt/conda/lib/python3.10/site-packages/qiskit/quantum_info/synthesis/qsd.py:253 in _apply_a2\r\n    qc3 = two_qubit_decompose.two_qubit_cnot_decompose(mat2)\r\nUnboundLocalError: local variable 'mat2' referenced before assignment\r\n \r\nUse %tb to get the full traceback.\r\n```\r\nAlternatively,\r\n```\r\nqs_decomposition(qiskit.quantum_info.random_unitary(4).to_matrix())\r\n```\r\nGiving the same error\r\n\r\n### What should happen?\r\n\r\nWe should still be able to produce a decomposed circuit from these examples\r\n\r\n### Any suggestions?\r\n\r\nThis seems to occur when line 233 of qsd.py in the function ‘_apply_a2()’ does not transpile to include any of the ‘qsd2q’ gate type for an instance.\r\n\r\nTo fix this add something such as the following before the loop on line 242:\r\n```\r\nif not ind2q:\r\n    return ccirc\r\n```\r\nAdditionally should add a test for this kind of case to the [test](https://github.com/Qiskit/qiskit-terra/blob/main/test/python/quantum_info/test_synthesis.py)

diff --git a/qiskit/quantum_info/synthesis/qsd.py b/qiskit/quantum_info/synthesis/qsd.py\nindex 87aae51cabad..82694e45ab22 100644\n--- a/qiskit/quantum_info/synthesis/qsd.py\n+++ b/qiskit/quantum_info/synthesis/qsd.py\n@@ -81,7 +81,7 @@ def qs_decomposition(\n         circ = decomposer_1q(mat)\n     elif dim == 4:\n         if decomposer_2q is None:\n-            if opt_a2:\n+            if opt_a2 and _depth > 0:\n                 from qiskit.extensions.unitary import UnitaryGate  # pylint: disable=cyclic-import\n \n                 def decomp_2q(mat):\n@@ -118,7 +118,7 @@ def decomp_2q(mat):\n         right_circ = _demultiplex(u1, u2, opt_a1=opt_a1, opt_a2=opt_a2, _depth=_depth)\n         circ.append(right_circ.to_instruction(), qr)\n \n-    if opt_a2 and _depth == 0:\n+    if opt_a2 and _depth == 0 and dim > 4:\n         return _apply_a2(circ)\n     return circ\n \n@@ -236,6 +236,8 @@ def _apply_a2(circ):\n     for i, instruction in enumerate(ccirc.data):\n         if instruction.operation.name == "qsd2q":\n             ind2q.append(i)\n+    if not ind2q:\n+        return ccirc\n     # rolling over diagonals\n     ind2 = None  # lint\n     for ind1, ind2 in zip(ind2q[0:-1:], ind2q[1::]):\ndiff --git a/releasenotes/notes/fix-1q-matrix-bug-in-quantum-shannon-decomposer-c99ce6509f03715b.yaml b/releasenotes/notes/fix-1q-matrix-bug-in-quantum-shannon-decomposer-c99ce6509f03715b.yaml\nnew file mode 100644\nindex 000000000000..d68d20c89e24\n--- /dev/null\n+++ b/releasenotes/notes/fix-1q-matrix-bug-in-quantum-shannon-decomposer-c99ce6509f03715b.yaml\n@@ -0,0 +1,7 @@\n+---\n+fixes:\n+  - |\n+    When :func:`~qiskit.quantum_info.synthesis.qs_decomposition`, which does quantum Shannon\n+    decomposition, was called on trivial numeric unitaries that do not benefit from this\n+    decomposition, an unexpected error was raised. With this fix, such unitaries are detected and\n+    the equivalent circuit is returned without performing Shannon decomposition.\n

diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py\nindex 58074995c7a1..19ec8b8139e9 100644\n--- a/test/python/quantum_info/test_synthesis.py\n+++ b/test/python/quantum_info/test_synthesis.py\n@@ -1530,7 +1530,7 @@ def test_hermitian(self, nqubits):\n             expected_cx = self._qsd_l2_cx_count(nqubits) - self._qsd_l2_a1_mod(nqubits)\n             self.assertLessEqual(ccirc.count_ops().get("cx"), expected_cx)\n \n-    @data(*list(range(3, 6)))\n+    @data(*list(range(1, 6)))\n     def test_opt_a1a2(self, nqubits):\n         """Test decomposition with both optimization a1 and a2 from shende2006"""\n         dim = 2**nqubits\n@@ -1538,9 +1538,15 @@ def test_opt_a1a2(self, nqubits):\n         circ = self.qsd(umat, opt_a1=True, opt_a2=True)\n         ccirc = transpile(circ, basis_gates=["u", "cx"], optimization_level=0)\n         self.assertTrue(Operator(umat) == Operator(ccirc))\n-        self.assertEqual(\n-            ccirc.count_ops().get("cx"), (23 / 48) * 4**nqubits - (3 / 2) * 2**nqubits + 4 / 3\n-        )\n+        if nqubits > 2:\n+            self.assertEqual(\n+                ccirc.count_ops().get("cx"),\n+                (23 / 48) * 4**nqubits - (3 / 2) * 2**nqubits + 4 / 3,\n+            )\n+        elif nqubits == 1:\n+            self.assertEqual(ccirc.count_ops().get("cx", 0), 0)\n+        elif nqubits == 2:\n+            self.assertLessEqual(ccirc.count_ops().get("cx", 0), 3)\n \n \n class TestTwoQubitDecomposeUpToDiagonal(QiskitTestCase):\n


Fix output of `DensityMatrix.partial_transpose` to match input dimensions (Qiskit/qiskit#10163)

<!--\r\n⚠ If you do not respect this template, your pull request will be closed.\r\n⚠ Your pull request title should be short detailed and understandable for all.\r\n⚠ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [ ] I have added the tests to cover my changes.\r\n- [ ] I have updated the documentation accordingly.\r\n- [ ] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nAdd dimensions to the `DensityMatrix` returned by `partial_transpose` so its dimensions match those of the input density matrix.\r\n\r\n\r\n### Details and comments\r\nFix #10155 \r\n\r\n

`DensityMatrix.partial_transpose` does not preserve input dimensions

### Environment\r\n\r\n- **Qiskit Terra version**: to be released in 0.24.0\r\n- **Python version**: 3.10\r\n- **Operating system**: MacOS\r\n\r\n\r\n### What is happening?\r\n\r\nThe `partial_transpose` should return a density matrix of the same dimensionality as the input `DensityMatrix` object. When the dimensions are for systems different than qubits, the dimensions are currently not preserved. \r\n\r\n### How can we reproduce the issue?\r\n\r\nFor in the following example, the dimensions of the output density matrix ρ2 are (9,) but they should be (3,3).\r\n\r\n```python\r\n>>> import numpy as np\r\n>>> from qiskit.quantum_info import Statevector, DensityMatrix\r\n>>> ψ = Statevector(np.array([1,0,0,0,1,0,0,0,1])*1/np.sqrt(3),dims=(3,3))\r\n>>> ρ1 = DensityMatrix(ψ)\r\n>>> ρ1.dims()\r\n(3, 3)\r\n>>> ρ2 = ρ1.partial_transpose([0])\r\n>>> ρ2.dims()\r\n(9,)\r\n```\r\n\r\n### What should happen?\r\n\r\nIt should be an easy fix. The function should return:\r\n```python\r\nreturn DensityMatrix(rho, dims=self.dims())\r\n```\r\n\r\ninstead of just:\r\n```python\r\nreturn DensityMatrix(rho)\r\n```\r\n\r\n### Any suggestions?\r\n\r\n_No response_

diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py\nindex 124a382f01d1..3103ef23036a 100644\n--- a/qiskit/quantum_info/states/densitymatrix.py\n+++ b/qiskit/quantum_info/states/densitymatrix.py\n@@ -832,4 +832,4 @@ def partial_transpose(self, qargs):\n             lst[i], lst[i + n] = lst[i + n], lst[i]\n         rho = np.transpose(arr, lst)\n         rho = np.reshape(rho, self._op_shape.shape)\n-        return DensityMatrix(rho)\n+        return DensityMatrix(rho, dims=self.dims())\ndiff --git a/releasenotes/notes/fix-partial-transpose-output-dims-3082fcf4147055dc.yaml b/releasenotes/notes/fix-partial-transpose-output-dims-3082fcf4147055dc.yaml\nnew file mode 100644\nindex 000000000000..edba41333ce8\n--- /dev/null\n+++ b/releasenotes/notes/fix-partial-transpose-output-dims-3082fcf4147055dc.yaml\n@@ -0,0 +1,5 @@\n+---\n+fixes:\n+  - |\n+    Fixed the dimensions of the output density matrix from :meth:`.DensityMatrix.partial_transpose`\n+    so they match the dimensions of the corresponding input density matrix.\n

diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py\nindex 8886e35cc288..16fbbbc1b9a4 100644\n--- a/test/python/quantum_info/states/test_densitymatrix.py\n+++ b/test/python/quantum_info/states/test_densitymatrix.py\n@@ -1219,6 +1219,15 @@ def test_density_matrix_partial_transpose(self):\n             self.assertEqual(rho.partial_transpose([0]), DensityMatrix(rho1))\n             self.assertEqual(rho.partial_transpose([1]), DensityMatrix(rho1))\n \n+        with self.subTest(msg="dims(3,3)"):\n+            mat = np.zeros((9, 9))\n+            mat1 = np.zeros((9, 9))\n+            mat[8, 0] = 1\n+            mat1[0, 8] = 1\n+            rho = DensityMatrix(mat, dims=(3, 3))\n+            rho1 = DensityMatrix(mat1, dims=(3, 3))\n+            self.assertEqual(rho.partial_transpose([0, 1]), rho1)\n+\n     def test_clip_probabilities(self):\n         """Test probabilities are clipped to [0, 1]."""\n         dm = DensityMatrix([[1.1, 0], [0, 0]])\n


Removed 2pi wrapping for global phase parameter (Qiskit/qiskit#10631)

It fixes #10569\r\n<!--\r\n⚠ If you do not respect this template, your pull request will be closed.\r\n⚠ Your pull request title should be short detailed and understandable for all.\r\n⚠ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [ ] I have added the tests to cover my changes.\r\n- [ ] I have updated the documentation accordingly.\r\n- [ ] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nRemoved `_mod_2pi` function.\r\n\r\n\r\n### Details and comments\r\nIt is similiar to Qiskit/qiskit-ibm-provider#689\r\n\r\n\r\n

Cannot apply dynamical decoupling on a circuit with parameterized global phase

### Environment\n\n- **Qiskit Terra version**: 0.25 \r\n- **Python version**: 3.11\r\n- **Operating system**: Windows\r\n\n\n### What is happening?\n\nDynamical decoupling cannot be applied on a circuit with a parameterized global phase, due to a failing `_mod_2pi` wrapping \n\n### How can we reproduce the issue?\n\n```python\r\nfrom qiskit.transpiler.passes import ALAPScheduleAnalysis, PadDynamicalDecoupling\r\nfrom qiskit.transpiler import PassManager, InstructionDurations\r\nfrom qiskit.circuit.library import XGate\r\nfrom qiskit.circuit import Parameter\r\ndurations = InstructionDurations.from_backend(backend)\r\n\r\nqc = QuantumCircuit(1,1)\r\nqc.sx(0)\r\nqc.delay(1600, 0)\r\nqc.sx(0)\r\nqc.global_phase=Parameter('a')\r\n\r\npm = PassManager([ALAPScheduleAnalysis(durations), PadDynamicalDecoupling(durations, [XGate(), XGate()])])\r\npm.run(qc)\r\n```\r\n\r\nerrors in\r\nhttps://github.com/Qiskit/qiskit-terra/blob/c29e938c2ba1c359a0a3268679c68f3237b2c1b7/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py#L260\r\nwith:\r\n`TypeError: unsupported operand type(s) for %: 'ParameterExpression' and 'float'`\n\n### What should happen?\n\nRun the DD transpiler pass successfully\n\n### Any suggestions?\n\nDon't wrap global phase if parameterized, see same issue in https://github.com/Qiskit/qiskit-ibm-provider/pull/689

diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py\nindex 87bbdbff919c..cf8477278372 100644\n--- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py\n+++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py\n@@ -274,7 +274,7 @@ def run(self, dag):\n                 if gate is not None:\n                     new_dag.apply_operation_back(gate, [dag_qubit], check=False)\n \n-            new_dag.global_phase = _mod_2pi(new_dag.global_phase + sequence_gphase)\n+            new_dag.global_phase = new_dag.global_phase + sequence_gphase\n \n         return new_dag\n \n@@ -283,11 +283,3 @@ def __gate_supported(self, gate: Gate, qarg: int) -> bool:\n         if self._target is None or self._target.instruction_supported(gate.name, qargs=(qarg,)):\n             return True\n         return False\n-\n-\n-def _mod_2pi(angle: float, atol: float = 0):\n-    """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π"""\n-    wrapped = (angle + np.pi) % (2 * np.pi) - np.pi\n-    if abs(wrapped - np.pi) < atol:\n-        wrapped = -np.pi\n-    return wrapped\ndiff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\nindex 2d4114f3cfc7..9137133567ec 100644\n--- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\n+++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\n@@ -394,8 +394,7 @@ def _constrained_length(values):\n                 gate_length = self._dd_sequence_lengths[qubit][dd_ind]\n                 self._apply_scheduled_op(dag, idle_after, gate, qubit)\n                 idle_after += gate_length\n-\n-        dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase)\n+        dag.global_phase = dag.global_phase + sequence_gphase\n \n     @staticmethod\n     def _resolve_params(gate: Gate) -> tuple:\n@@ -407,11 +406,3 @@ def _resolve_params(gate: Gate) -> tuple:\n             else:\n                 params.append(p)\n         return tuple(params)\n-\n-    @staticmethod\n-    def _mod_2pi(angle: float, atol: float = 0):\n-        """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π"""\n-        wrapped = (angle + np.pi) % (2 * np.pi) - np.pi\n-        if abs(wrapped - np.pi) < atol:\n-            wrapped = -np.pi\n-        return wrapped\ndiff --git a/releasenotes/notes/fix-param-global-phase-31547267f6124552.yaml b/releasenotes/notes/fix-param-global-phase-31547267f6124552.yaml\nnew file mode 100644\nindex 000000000000..13048f626dc4\n--- /dev/null\n+++ b/releasenotes/notes/fix-param-global-phase-31547267f6124552.yaml\n@@ -0,0 +1,7 @@\n+---\n+fixes:\n+  - |\n+    Fixed compatibility of :class:`.DynamicalDecoupling` and\n+    :class:`.PadDynamicalDecoupling` with circuits that have a parameterized\n+    global phase.\n+    Fixed `#10569 <https://github.com/Qiskit/qiskit-terra/issues/10569>`__.\n

diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py\nindex b74091dd76f8..e53fb9b0226e 100644\n--- a/test/python/transpiler/test_dynamical_decoupling.py\n+++ b/test/python/transpiler/test_dynamical_decoupling.py\n@@ -1031,6 +1031,25 @@ def test_respect_target_instruction_constraints(self):\n         expected.delay(200, [0])\n         self.assertEqual(expected, scheduled)\n \n+    def test_paramaterized_global_phase(self):\n+        """Test paramaterized global phase in DD circuit.\n+        See:https://github.com/Qiskit/qiskit-terra/issues/10569\n+        """\n+        dd_sequence = [XGate(), YGate()] * 2\n+        qc = QuantumCircuit(1, 1)\n+        qc.h(0)\n+        qc.delay(1700, 0)\n+        qc.y(0)\n+        qc.global_phase = Parameter("a")\n+        pm = PassManager(\n+            [\n+                ALAPScheduleAnalysis(self.durations),\n+                PadDynamicalDecoupling(self.durations, dd_sequence),\n+            ]\n+        )\n+\n+        self.assertEqual(qc.global_phase + np.pi, pm.run(qc).global_phase)\n+\n \n if __name__ == "__main__":\n     unittest.main()\n


Allow dynamical decoupling pass to work on circuits with pulse gates  (Qiskit/qiskit#10834)

### Summary\r\n\r\nAllow dynamical decoupling pass to work on circuits with pulse gates \r\n\r\n### Details and comments\r\n\r\nThe pass was accessing the circuit calibrations incorrectly, resulting in\r\nan error when one of the gates in the dynamical decoupling sequence had\r\na pulse gate calibration.\r\n\r\nCloses #10833

Dynamical decoupling fails when a gate in the decoupling sequence has a calibration attached to the circuit

### Environment\n\n- **Qiskit Terra version**: 0.25.1\r\n- **Python version**: 3.10\r\n- **Operating system**: Fedora Linux\r\n\n\n### What is happening?\n\n`PadDynamicalDecoupling` raises `TypeError: unhashable type: 'list'` when run on a circuit with a calibration for one of the gates in the decoupling sequence.\n\n### How can we reproduce the issue?\n\n```python\r\nfrom qiskit_ibm_provider import IBMProvider\r\n\r\n\r\nprovider = IBMProvider()\r\nbackend = provider.get_backend("ibm_nazca")\r\n\r\n\r\nimport numpy as np\r\n\r\nfrom qiskit import pulse, QuantumCircuit\r\nfrom qiskit.transpiler import PassManager\r\nfrom qiskit.transpiler.passes import ALAPScheduleAnalysis, PadDynamicalDecoupling\r\nfrom qiskit.circuit.library import XGate, YGate\r\nfrom qiskit.pulse import DriveChannel\r\n\r\n\r\ninst_sched_map = backend.instruction_schedule_map\r\n\r\n\r\ncirc = QuantumCircuit(1, 1)\r\ncirc.x(0)\r\ncirc.delay(1600, 0)\r\ncirc.x(0)\r\n\r\nfor qubit in range(backend.configuration().n_qubits):\r\n    with pulse.build(f"y gate for qubit {qubit}") as sched:\r\n        with pulse.phase_offset(np.pi / 2, DriveChannel(qubit)):\r\n            x_sched = inst_sched_map.get("x", qubits=[qubit])\r\n            pulse.call(x_sched)\r\n        circ.add_calibration("y", [qubit], sched)\r\n\r\ndd_sequence = [XGate(), YGate()]*4\r\ndurations = backend.target.durations()\r\n\r\npm = PassManager(\r\n        [\r\n            ALAPScheduleAnalysis(durations),\r\n            PadDynamicalDecoupling(\r\n                durations,\r\n                dd_sequence,\r\n                pulse_alignment=backend.configuration().timing_constraints[\r\n                    "pulse_alignment"\r\n                ],\r\n            ),\r\n        ]\r\n    )\r\n\r\npm.run(circ)\r\n```\n\n### What should happen?\n\nThe duration of the gate calibration should be used to schedule the decoupling sequence.\n\n### Any suggestions?\n\nA PR with a fix will be opened shortly.

diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\nindex bbb1b39bbd72..fb33361a4959 100644\n--- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\n+++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py\n@@ -16,7 +16,7 @@\n import logging\n import numpy as np\n \n-from qiskit.circuit import Qubit, Gate\n+from qiskit.circuit import Gate, ParameterExpression, Qubit\n from qiskit.circuit.delay import Delay\n from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate\n from qiskit.circuit.reset import Reset\n@@ -227,7 +227,8 @@ def _pre_runhook(self, dag: DAGCircuit):\n             for gate in self._dd_sequence:\n                 try:\n                     # Check calibration.\n-                    gate_length = dag.calibrations[gate.name][(physical_index, gate.params)]\n+                    params = self._resolve_params(gate)\n+                    gate_length = dag.calibrations[gate.name][((physical_index,), params)].duration\n                     if gate_length % self._alignment != 0:\n                         # This is necessary to implement lightweight scheduling logic for this pass.\n                         # Usually the pulse alignment constraint and pulse data chunk size take\n@@ -394,6 +395,17 @@ def _constrained_length(values):\n \n         dag.global_phase = self._mod_2pi(dag.global_phase + sequence_gphase)\n \n+    @staticmethod\n+    def _resolve_params(gate: Gate) -> tuple:\n+        """Return gate params with any bound parameters replaced with floats"""\n+        params = []\n+        for p in gate.params:\n+            if isinstance(p, ParameterExpression) and not p.parameters:\n+                params.append(float(p))\n+            else:\n+                params.append(p)\n+        return tuple(params)\n+\n     @staticmethod\n     def _mod_2pi(angle: float, atol: float = 0):\n         """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π"""\ndiff --git a/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml b/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml\nnew file mode 100644\nindex 000000000000..cdf23b9ef9a3\n--- /dev/null\n+++ b/releasenotes/notes/dd-pg-10833-ddddee68ffd913c4.yaml\n@@ -0,0 +1,7 @@\n+---\n+fixes:\n+  - |\n+    Fixed a bug in the :class:`.PadDynamicalDecoupling` transpiler pass which\n+    would cause the pass to fail if a circuit contained a pulse gate\n+    calibration for one of the gates in the decoupling sequence. Fixed `#10833\n+    <https://github.com/Qiskit/qiskit/issues/10833>`_.\n

diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py\nindex bd8c44ab0973..6ae6a7b3c311 100644\n--- a/test/python/transpiler/test_dynamical_decoupling.py\n+++ b/test/python/transpiler/test_dynamical_decoupling.py\n@@ -17,7 +17,8 @@\n from numpy import pi\n from ddt import ddt, data\n \n-from qiskit.circuit import QuantumCircuit, Delay, Measure, Reset, Parameter\n+from qiskit import pulse\n+from qiskit.circuit import Gate, QuantumCircuit, Delay, Measure, Reset, Parameter\n from qiskit.circuit.library import XGate, YGate, RXGate, UGate, CXGate, HGate\n from qiskit.quantum_info import Operator\n from qiskit.transpiler.instruction_durations import InstructionDurations\n@@ -29,7 +30,6 @@\n from qiskit.transpiler.passmanager import PassManager\n from qiskit.transpiler.exceptions import TranspilerError\n from qiskit.transpiler.target import Target, InstructionProperties\n-from qiskit import pulse\n \n from qiskit.test import QiskitTestCase\n \n@@ -339,6 +339,154 @@ def test_insert_dd_ghz_everywhere(self):\n \n         self.assertEqual(ghz4_dd, expected)\n \n+    def test_insert_dd_with_pulse_gate_calibrations(self):\n+        """Test DD gates are inserted without error when circuit calibrations are used\n+\n+                      ┌───┐            ┌───────────────┐      ┌───┐       »\n+           q_0: ──────┤ H ├─────────■──┤ Delay(75[dt]) ├──────┤ X ├───────»\n+                ┌─────┴───┴─────┐ ┌─┴─┐└───────────────┘┌─────┴───┴──────┐»\n+           q_1: ┤ Delay(50[dt]) ├─┤ X ├────────■────────┤ Delay(300[dt]) ├»\n+                ├───────────────┴┐└───┘      ┌─┴─┐      └────────────────┘»\n+           q_2: ┤ Delay(750[dt]) ├───────────┤ X ├──────────────■─────────»\n+                ├────────────────┤           └───┘            ┌─┴─┐       »\n+           q_3: ┤ Delay(950[dt]) ├────────────────────────────┤ X ├───────»\n+                └────────────────┘                            └───┘       »\n+        meas: 4/══════════════════════════════════════════════════════════»\n+                                                                           »\n+        «        ┌────────────────┐┌───┐┌───────────────┐ ░ ┌─┐\n+        «   q_0: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(75[dt]) ├─░─┤M├─────────\n+        «        └────────────────┘└───┘└───────────────┘ ░ └╥┘┌─┐\n+        «   q_1: ─────────────────────────────────────────░──╫─┤M├──────\n+        «                                                 ░  ║ └╥┘┌─┐\n+        «   q_2: ─────────────────────────────────────────░──╫──╫─┤M├───\n+        «                                                 ░  ║  ║ └╥┘┌─┐\n+        «   q_3: ─────────────────────────────────────────░──╫──╫──╫─┤M├\n+        «                                                 ░  ║  ║  ║ └╥┘\n+        «meas: 4/════════════════════════════════════════════╩══╩══╩══╩═\n+        «                                                     0  1  2  3\n+        """\n+        dd_sequence = [XGate(), XGate()]\n+        pm = PassManager(\n+            [\n+                ALAPScheduleAnalysis(self.durations),\n+                PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]),\n+            ]\n+        )\n+\n+        # Change duration to 100 from the 50 in self.durations to make sure\n+        # gate duration is used correctly.\n+        with pulse.builder.build() as x_sched:\n+            pulse.builder.delay(100, pulse.DriveChannel(0))\n+\n+        circ_in = self.ghz4.measure_all(inplace=False)\n+        circ_in.add_calibration(XGate(), (0,), x_sched)\n+\n+        ghz4_dd = pm.run(circ_in)\n+\n+        expected = self.ghz4.copy()\n+        expected = expected.compose(Delay(50), [1], front=True)\n+        expected = expected.compose(Delay(750), [2], front=True)\n+        expected = expected.compose(Delay(950), [3], front=True)\n+\n+        # Delays different from those of the default case using self.durations\n+        expected = expected.compose(Delay(75), [0])\n+        expected = expected.compose(XGate(), [0])\n+        expected = expected.compose(Delay(150), [0])\n+        expected = expected.compose(XGate(), [0])\n+        expected = expected.compose(Delay(75), [0])\n+\n+        expected = expected.compose(Delay(300), [1])\n+\n+        expected.measure_all()\n+        expected.add_calibration(XGate(), (0,), x_sched)\n+\n+        self.assertEqual(ghz4_dd, expected)\n+\n+    def test_insert_dd_with_pulse_gate_calibrations_with_parmas(self):\n+        """Test DD gates are inserted without error when parameterized circuit calibrations are used\n+\n+                      ┌───┐            ┌───────────────┐      ┌───┐       »\n+           q_0: ──────┤ H ├─────────■──┤ Delay(75[dt]) ├──────┤ X ├───────»\n+                ┌─────┴───┴─────┐ ┌─┴─┐└───────────────┘┌─────┴───┴──────┐»\n+           q_1: ┤ Delay(50[dt]) ├─┤ X ├────────■────────┤ Delay(300[dt]) ├»\n+                ├───────────────┴┐└───┘      ┌─┴─┐      └────────────────┘»\n+           q_2: ┤ Delay(750[dt]) ├───────────┤ X ├──────────────■─────────»\n+                ├────────────────┤           └───┘            ┌─┴─┐       »\n+           q_3: ┤ Delay(950[dt]) ├────────────────────────────┤ X ├───────»\n+                └────────────────┘                            └───┘       »\n+        meas: 4/══════════════════════════════════════════════════════════»\n+                                                                           »\n+        «        ┌────────────────┐┌───┐┌───────────────┐ ░ ┌─┐\n+        «   q_0: ┤ Delay(150[dt]) ├┤ X ├┤ Delay(75[dt]) ├─░─┤M├─────────\n+        «        └────────────────┘└───┘└───────────────┘ ░ └╥┘┌─┐\n+        «   q_1: ─────────────────────────────────────────░──╫─┤M├──────\n+        «                                                 ░  ║ └╥┘┌─┐\n+        «   q_2: ─────────────────────────────────────────░──╫──╫─┤M├───\n+        «                                                 ░  ║  ║ └╥┘┌─┐\n+        «   q_3: ─────────────────────────────────────────░──╫──╫──╫─┤M├\n+        «                                                 ░  ║  ║  ║ └╥┘\n+        «meas: 4/════════════════════════════════════════════╩══╩══╩══╩═\n+        «                                                     0  1  2  3\n+        """\n+        # Change duration to 100 from the 50 in self.durations to make sure\n+        # gate duration is used correctly.\n+        amp = Parameter("amp")\n+        with pulse.builder.build() as sched:\n+            pulse.builder.play(\n+                pulse.Gaussian(100, amp=amp, sigma=10.0),\n+                pulse.DriveChannel(0),\n+            )\n+\n+        class Echo(Gate):\n+            """Dummy Gate subclass for testing\n+\n+            In this test, we use a non-standard gate so we can add parameters\n+            to it, in order to test the handling of parameters by\n+            PadDynamicalDecoupling. PadDynamicalDecoupling checks that the DD\n+            sequence is equivalent to the identity, so we can not use Gate\n+            directly. Here we subclass Gate and add the identity as its matrix\n+            representation to satisfy PadDynamicalDecoupling's check.\n+            """\n+\n+            def __array__(self, dtype=None):\n+                return np.eye(2, dtype=dtype)\n+\n+        # A gate with one unbound and one bound parameter to leave in the final\n+        # circuit.\n+        echo = Echo("echo", 1, [amp, 10.0])\n+\n+        circ_in = self.ghz4.measure_all(inplace=False)\n+        circ_in.add_calibration(echo, (0,), sched)\n+\n+        dd_sequence = [echo, echo]\n+        pm = PassManager(\n+            [\n+                ALAPScheduleAnalysis(self.durations),\n+                PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]),\n+            ]\n+        )\n+\n+        ghz4_dd = pm.run(circ_in)\n+\n+        expected = self.ghz4.copy()\n+        expected = expected.compose(Delay(50), [1], front=True)\n+        expected = expected.compose(Delay(750), [2], front=True)\n+        expected = expected.compose(Delay(950), [3], front=True)\n+\n+        # Delays different from those of the default case using self.durations\n+        expected = expected.compose(Delay(75), [0])\n+        expected = expected.compose(echo, [0])\n+        expected = expected.compose(Delay(150), [0])\n+        expected = expected.compose(echo, [0])\n+        expected = expected.compose(Delay(75), [0])\n+\n+        expected = expected.compose(Delay(300), [1])\n+\n+        expected.measure_all()\n+        expected.add_calibration(echo, (0,), sched)\n+\n+        self.assertEqual(ghz4_dd, expected)\n+\n     def test_insert_dd_ghz_xy4(self):\n         """Test XY4 sequence of DD gates.\n \n