|
27 | 27 | ``gas_restr`` and ``gas_restr_type`` are normalized by the same GAS helper |
28 | 28 | routines. This module supports ordinary state averaging, including |
29 | 29 | zero-weight roots, GAS-safe canonicalization of inactive/external orbitals, |
| 30 | +optional within-GAS pseudo-natural orbitals with synchronized CI rotation, |
30 | 31 | density fitting through PySCF's ``mcscf.df`` machinery, energy-only scanners |
31 | 32 | and the GASCI-native spin-penalty Hamiltonian. It does not implement |
32 | 33 | state-average-mix, state-specific excited-state wrappers, |
33 | | -active-space natural-orbital rotations, analytic gradients/NACs or the |
| 34 | +unrestricted active-space natural-orbital rotations, analytic gradients/NACs or the |
34 | 35 | legacy two-step CASSCF driver. |
35 | 36 | """ |
36 | 37 |
|
@@ -756,6 +757,10 @@ class GASSCF(newton_casscf.CASSCF): |
756 | 757 | returns occupations as one array per subspace. These methods leave |
757 | 758 | the computational orbitals and CI vectors unchanged. |
758 | 759 |
|
| 760 | + To rotate computational orbitals and CI together, explicitly call |
| 761 | + ``mc.canonicalize_(gas_pseudo_natorb=True)``. The variant without the |
| 762 | + trailing underscore returns the transformed results without writeback. |
| 763 | +
|
759 | 764 | Analysis orbitals can be exported with PySCF's Molden writer:: |
760 | 765 |
|
761 | 766 | from pyscf.tools import molden |
@@ -1255,30 +1260,112 @@ def casci(self, mo_coeff=None, ci0=None, eris=None, |
1255 | 1260 |
|
1256 | 1261 | def canonicalize(self, mo_coeff=None, ci=None, eris=None, sort=False, |
1257 | 1262 | gas_natorb=False, gasdm1=None, verbose=None, |
1258 | | - cas_natorb=None, **kwargs): |
1259 | | - """Canonicalize core/external orbitals without rotating GAS subspaces. |
1260 | | -
|
1261 | | - Native CASSCF calls this method with its ``natorb`` flag as the fifth |
1262 | | - positional argument. In GASSCF, any automatic active-space natural |
1263 | | - orbital rotation would generally mix GAS subspaces and invalidate the |
1264 | | - restricted determinant space, so active-space natural orbitals are |
1265 | | - explicitly guarded. The active GAS block is otherwise kept fixed; |
1266 | | - only inactive and external orbitals are canonicalized by the inherited |
1267 | | - PySCF machinery. |
| 1263 | + cas_natorb=None, *, gas_pseudo_natorb=False, **kwargs): |
| 1264 | + """Return ``(mo_coeff, ci, mo_energy)`` without writing to this object. |
| 1265 | +
|
| 1266 | + By default only core/external orbitals are canonicalized. Opt in with |
| 1267 | + ``gas_pseudo_natorb=True`` to diagonalize the density separately within |
| 1268 | + each GAS subspace and transform every CI root into the new basis, |
| 1269 | + including zero-weight roots. SA uses the weighted density unless |
| 1270 | + ``gasdm1`` is supplied in the input active-orbital basis. |
| 1271 | +
|
| 1272 | + Active occupations are ordered from largest to smallest within each |
| 1273 | + unfrozen symmetry block of a GAS subspace. Frozen orbitals and orbital |
| 1274 | + symmetry/``extrasym`` labels are respected. ``sort`` retains its native |
| 1275 | + meaning for core/external orbital energies. ``mo_energy`` contains |
| 1276 | + Fock diagonal elements, not pseudo-natural occupations. |
| 1277 | +
|
| 1278 | + These are pseudo-natural orbitals: density between GAS subspaces need |
| 1279 | + not vanish. Rotations across GAS subspaces remain unsupported, as do |
| 1280 | + ``gas_natorb=True`` and ``cas_natorb=True``. Use ``canonicalize_`` to |
| 1281 | + write the returned orbitals, CI and orbital energies to this object. |
1268 | 1282 | """ |
1269 | 1283 |
|
1270 | 1284 | if gas_natorb or cas_natorb: |
1271 | 1285 | _unsupported("GAS natural-orbital rotation") |
1272 | | - return gasci.GASCI.canonicalize( |
1273 | | - self, mo_coeff, ci, eris, sort=sort, gas_natorb=False, |
| 1286 | + if not gas_pseudo_natorb: |
| 1287 | + return gasci.GASCI.canonicalize( |
| 1288 | + self, mo_coeff, ci, eris, sort=sort, gas_natorb=False, |
| 1289 | + gasdm1=gasdm1, verbose=verbose, **kwargs) |
| 1290 | + |
| 1291 | + mo_coeff = self.mo_coeff if mo_coeff is None else mo_coeff |
| 1292 | + ci = self.ci if ci is None else ci |
| 1293 | + if mo_coeff is None or ci is None: |
| 1294 | + raise ValueError("pseudo-natural canonicalization requires orbitals and CI") |
| 1295 | + if gasdm1 is None: |
| 1296 | + gasdm1 = self.make_gasdm1( |
| 1297 | + ci=ci, state=None if self._has_state_weights() else 0) |
| 1298 | + gasdm1 = numpy.asarray(gasdm1) |
| 1299 | + if (gasdm1.shape != (self.ncas, self.ncas) |
| 1300 | + or numpy.iscomplexobj(gasdm1) |
| 1301 | + or not numpy.all(numpy.isfinite(gasdm1))): |
| 1302 | + raise ValueError("gasdm1 must be a finite real (ncas, ncas) matrix") |
| 1303 | + |
| 1304 | + nmo = mo_coeff.shape[1] |
| 1305 | + frozen = numpy.zeros(nmo, dtype=bool) |
| 1306 | + if isinstance(self.frozen, (int, numpy.integer)): |
| 1307 | + frozen[:self.frozen] = True |
| 1308 | + elif self.frozen is not None: |
| 1309 | + frozen[self.frozen] = True |
| 1310 | + orbsym = getattr(mo_coeff, 'orbsym', numpy.zeros(nmo, dtype=int)) |
| 1311 | + extrasym = getattr(self, 'extrasym', None) |
| 1312 | + if extrasym is None: |
| 1313 | + extrasym = numpy.zeros(nmo, dtype=int) |
| 1314 | + rotation = numpy.eye(self.ncas) |
| 1315 | + offset = 0 |
| 1316 | + for size in self.gas_orbs: |
| 1317 | + groups = {} |
| 1318 | + for p in range(offset, offset + size): |
| 1319 | + i = self.ncore + p |
| 1320 | + if not frozen[i]: |
| 1321 | + groups.setdefault((orbsym[i], extrasym[i]), []).append(p) |
| 1322 | + for indices in groups.values(): |
| 1323 | + block = numpy.ix_(indices, indices) |
| 1324 | + _, vectors = self._natural_eigensystem(gasdm1[block], sort=True) |
| 1325 | + rotation[block] = vectors |
| 1326 | + offset += size |
| 1327 | + |
| 1328 | + # Transform CI explicitly, without exposing a general rotation hook to |
| 1329 | + # Newton. In particular, never solve GASCI again in the rotated basis. |
| 1330 | + def transform(vector): |
| 1331 | + return self.fcisolver.transform_ci_within_gas( |
| 1332 | + vector, self.ncas, self.nelecas, rotation) |
| 1333 | + |
| 1334 | + if isinstance(ci, (list, tuple)): |
| 1335 | + ci_new = type(ci)(transform(vector) for vector in ci) |
| 1336 | + elif numpy.ndim(ci) == 2 and self.fcisolver.nroots > 1: |
| 1337 | + ci_new = numpy.asarray([transform(vector) for vector in ci]) |
| 1338 | + else: |
| 1339 | + ci_new = transform(ci) |
| 1340 | + # The native routine may reorder orbital labels when sort=True; |
| 1341 | + # own the metadata as well as the MO array to keep this call nonmutating. |
| 1342 | + mo_input = mo_coeff.copy() |
| 1343 | + if getattr(mo_coeff, 'orbsym', None) is not None: |
| 1344 | + mo_input = lib.tag_array(mo_input, orbsym=numpy.array(orbsym, copy=True)) |
| 1345 | + # Both calls use the original basis, so a supplied ERIS remains valid. |
| 1346 | + fock = self.get_fock(mo_coeff, ci, eris, gasdm1, verbose) |
| 1347 | + mo_new, _, mo_energy = gasci.GASCI.canonicalize( |
| 1348 | + self, mo_input, ci, eris, sort=sort, gas_natorb=False, |
1274 | 1349 | gasdm1=gasdm1, verbose=verbose, **kwargs) |
| 1350 | + active = slice(self.ncore, self.ncore + self.ncas) |
| 1351 | + mo_new[:, active] = mo_coeff[:, active] @ rotation |
| 1352 | + mo_energy[active] = numpy.einsum( |
| 1353 | + 'pi,pi->i', mo_new[:, active], fock @ mo_new[:, active]) |
| 1354 | + return mo_new, ci_new, mo_energy |
1275 | 1355 |
|
1276 | 1356 | def canonicalize_(self, mo_coeff=None, ci=None, eris=None, sort=False, |
1277 | 1357 | gas_natorb=False, gasdm1=None, verbose=None, |
1278 | | - cas_natorb=None, **kwargs): |
| 1358 | + cas_natorb=None, *, gas_pseudo_natorb=False, **kwargs): |
| 1359 | + """Write the canonicalized MO, CI and orbital energies to this object. |
| 1360 | +
|
| 1361 | + Returns the same three values as :meth:`canonicalize`; the method |
| 1362 | + without the trailing underscore leaves these result attributes intact. |
| 1363 | + """ |
| 1364 | + |
1279 | 1365 | mo_coeff, ci, mo_energy = self.canonicalize( |
1280 | 1366 | mo_coeff, ci, eris, sort=sort, gas_natorb=gas_natorb, |
1281 | | - gasdm1=gasdm1, verbose=verbose, cas_natorb=cas_natorb, **kwargs) |
| 1367 | + gasdm1=gasdm1, verbose=verbose, cas_natorb=cas_natorb, |
| 1368 | + gas_pseudo_natorb=gas_pseudo_natorb, **kwargs) |
1282 | 1369 | self.mo_coeff = mo_coeff |
1283 | 1370 | self.ci = ci |
1284 | 1371 | self.mo_energy = mo_energy |
|
0 commit comments