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

org: pyscf
repo: pyscf
pr_number: 2886
issue_title: Add frozen orbital support for tdscf
issue_body: 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.
resolved_issues: number: 2853
title: [FEATURE REQUEST] Interest in core-valence separation for TDSCF?
body: **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: ...
```

Reference solution:
fix_patch: 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)