• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

adc-connect / adcc / 16384966787

03 Jul 2025 11:29AM UTC coverage: 73.883% (+0.3%) from 73.602%
16384966787

push

github

web-flow
Restructure ExcitedStates etc for IP/EA Integration (#195)

* allow timer customization in cached_member_function

* add option to exclude the args in the timer task

* timer description if nothing was recorded

* basic restructuring of ElectronicTransition, ExcitedStates and State2States

* basic restructuring of Excitation

* add _module member variable to dispatch to the corresponding working equations

* basic restructuring for excited state properties

* enable excitation for s2s

* port dataframe export

* add state_dm

* port the remaining properties - tests passing

* * cache the MO integrals instead of the AO integrals
* remove unnecessary property decorated returns of callbacks
* implement available integrals directly in the backends

* raise Exception directly in the backends for PE and PCM

* add to_qcvars back in

* split plot_spectrum in common function on base class and adc type dependent function on child classes

* invert order of unit conversion and broadening for plot_spectrum

* add input options for lower and upper bounds of the broadened spectrum

* remove broadening min/max and allow variable width_units

* refactor the ExcitedStates.describe method

* determine the column width automatically

* move describe_amplitudes to ElectronicStates and adapt for IP/EA

* store operators in tuple on IsrMatrix

* make flake happy and remove the cache for now since it is not used anyway

* reintroduce the cache for available backends and cache them lazily

* explicitly install setupstools

* enable libtensorlight download for macos arm64

* naively update CI to macos-latest

* install llvm and use arm64 compilers

* explicitly only build for the current platform

* only set architecture for macos

* generic block_orders for secular and isr matrix

* move init complete back to secular and isr matrix

* raise more explicit exception for not implemented methods

* patch coverage not required for status check

* add token for github api request... (continued)

1176 of 1884 branches covered (62.42%)

Branch coverage included in aggregate %.

700 of 965 new or added lines in 21 files covered. (72.54%)

6 existing lines in 6 files now uncovered.

7376 of 9691 relevant lines covered (76.11%)

175662.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.5
/adcc/AdcMatrix.py
1
#!/usr/bin/env python3
2
## vi: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
3
## ---------------------------------------------------------------------
4
##
5
## Copyright (C) 2018 by the adcc authors
6
##
7
## This file is part of adcc.
8
##
9
## adcc is free software: you can redistribute it and/or modify
10
## it under the terms of the GNU General Public License as published
11
## by the Free Software Foundation, either version 3 of the License, or
12
## (at your option) any later version.
13
##
14
## adcc is distributed in the hope that it will be useful,
15
## but WITHOUT ANY WARRANTY; without even the implied warranty of
16
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
## GNU General Public License for more details.
18
##
19
## You should have received a copy of the GNU General Public License
20
## along with adcc. If not, see <http://www.gnu.org/licenses/>.
21
##
22
## ---------------------------------------------------------------------
23
import itertools
2✔
24
import numpy as np
2✔
25

26
import libadcc
2✔
27

28
from .LazyMp import LazyMp
2✔
29
from .adc_pp import matrix as ppmatrix
2✔
30
from .timings import Timer, timed_member_call
2✔
31
from .AdcMethod import AdcMethod
2✔
32
from .functions import ones_like
2✔
33
from .Intermediates import Intermediates
2✔
34
from .AmplitudeVector import AmplitudeVector
2✔
35

36

37
class AdcExtraTerm:
2✔
38
    def __init__(self, matrix, blocks):
2✔
39
        """Initialise an AdcExtraTerm.
40
        This class can be used to add customs terms
41
        to an existing :py:class:`AdcMatrix`
42

43
        Parameters
44
        ----------
45
        matrix : AdcMatrix
46
            The matrix for which the extra term
47
            should be created.
48
        blocks : dict
49
            A dictionary where the key labels the matrix block
50
            and the item denotes a callable to construct
51
            an :py:class:`AdcBlock`
52
        """
53
        self.ground_state = matrix.ground_state
2✔
54
        self.reference_state = matrix.reference_state
2✔
55
        self.intermediates = matrix.intermediates
2✔
56
        self.blocks = {}
2✔
57
        if not isinstance(blocks, dict):
2✔
58
            raise TypeError("blocks needs to be a dict.")
2✔
59
        for space in blocks:
2✔
60
            block_fun = blocks[space]
2✔
61
            if not callable(block_fun):
2✔
62
                raise TypeError("Items in additional_blocks must be callable.")
2✔
63
            block = block_fun(
2✔
64
                self.reference_state, self.ground_state, self.intermediates
65
            )
66
            self.blocks[space] = block
2✔
67

68

69
class AdcMatrixlike:
2✔
70
    """
71
    Base class marker for all objects like ADC matrices.
72
    """
73

74
    _special_block_orders = {
2✔
75
        "adc2x": {"ph_ph": 2, "ph_pphh": 1, "pphh_ph": 1, "pphh_pphh": 1},
76
    }
77

78
    @classmethod
2✔
79
    def _default_block_orders(cls, method: AdcMethod) -> dict[str, int]:
2✔
80
        """
81
        Determines the default block orders for the given adc method.
82
        """
83
        # check if we have a special method like adc2x
84
        # I guess base_method should also contain the adc_type prefix so
85
        # we don't need to separate different adc_types
86
        block_orders = cls._special_block_orders.get(method.base_method.name, None)
2✔
87
        if block_orders is not None:
2✔
88
            return block_orders.copy()
2✔
89
        # otherwise assume that we have a "normal" PP/IP/...-ADC(n) method
90
        # - determine which spaces are available in the ADC(n) matrix
91
        #   starting from the given minimal space
92
        min_space = {
2✔
93
            "pp": "ph"
94
        }.get(method.adc_type, None)
95
        if min_space is None:
2!
NEW
96
            raise ValueError(f"Unknown adc type {method.adc_type} for method "
×
97
                             f"{method.name}. Can not determine default block "
98
                             "orders.")
99
        spaces = [
2✔
100
            "p" * i + min_space + "h" * i for i in range(0, (method.level // 2) + 1)
101
        ]
102
        # exploit the fact that the spaces are sorted from small to high:
103
        # If we walk the adc matrix in any direction we always have to subtract 1!
104
        # Therefore, we can determine the order according to the position of the
105
        # spaces: maxorder - 0 for singles, maxorder - 2 for doubles and
106
        # maxorder - 3 for the doubles/triples coupling
107
        ret = {}
2✔
108
        for ((i1, bra), (i2, ket)) in \
2✔
109
                itertools.product(enumerate(spaces), repeat=2):
110
            order = method.level - i1 - i2
2✔
111
            assert order >= 0
2✔
112
            ret[f"{bra}_{ket}"] = order
2✔
113
        return ret
2✔
114

115
    @classmethod
2✔
116
    def _validate_block_orders(cls, block_orders: dict[str, int],
2✔
117
                               method: AdcMethod,
118
                               allow_missing_diagonal_blocks: bool = False) -> None:
119
        """
120
        Validates that the given block_orders form a valid adc matrix for the given
121
        adc method.
122

123
        Parameters
124
        ----------
125
        block_orders: dict[str, int]
126
            The block orders to validate. Block orders should be of the form
127
            {'ph_ph': 2, 'ph_pphh': 1, ...}
128
        method: AdcMethod
129
            The adc method/adc type (PP-ADC, ...) for which to validate
130
            the block_orders.
131
        allow_missing_diagonal_blocks: bool, optional
132
            If set, couplings between missing diagonal blocks are allowed, e.g.,
133
            {'ph_ph': 1, 'ph_pphh': 0, 'pphh_ph': 0}
134
            will be valid although there is a coupling between the 'ph_ph' block
135
            and the missing 'pphh_pphh' block. (default: False)
136
        """
137
        for block, order in block_orders.items():
2✔
138
            if order is None:
2✔
139
                continue
2✔
140
            # ensure that the block is valid for the given adc type
141
            bra, ket = block.split("_")
2✔
142
            if not cls._is_valid_space(bra, method) or \
2✔
143
                    not cls._is_valid_space(ket, method):
144
                raise ValueError(f"Invalid block {block} for a "
2✔
145
                                 f"{method.adc_type} ADC matrix.")
146
            if bra == ket:  # done for diagonal blocks
2✔
147
                continue
2✔
148
            # ensure that the matrix is symmetric
149
            inv_block = f"{ket}_{bra}"
2✔
150
            inv_order = block_orders.get(inv_block, None)
2✔
151
            if inv_order is None or inv_order != order:
2✔
152
                raise ValueError(f"{block} and {inv_block} should always have "
2✔
153
                                 "the same order.")
154
            if allow_missing_diagonal_blocks:
2✔
155
                continue
2✔
156
            # ensure that we have no coupling between missing diagonal blocks
157
            bra_diag = f"{bra}_{bra}"
2✔
158
            ket_diag = f"{ket}_{ket}"
2✔
159
            bra_diag_order = block_orders.get(bra_diag, None)
2✔
160
            ket_diag_order = block_orders.get(ket_diag, None)
2✔
161
            if bra_diag_order is None or ket_diag_order is None:
2✔
162
                raise ValueError(f"Can only have a couling block {block} "
2✔
163
                                 f"if both diagonal blocks {bra_diag} and "
164
                                 f"{ket_diag} are in the matrix too.")
165

166
    @classmethod
2✔
167
    def _is_valid_space(cls, space: str, method: AdcMethod) -> bool:
2✔
168
        """
169
        Checks whether the given space ('ph' for instance) is valid for the given
170
        adc method. Thereby we only verify that the space matches the adc_type of
171
        adc method!
172
        """
173
        n_particle, n_hole = space.count("p"), space.count("h")
2✔
174
        # ensure that the space is of the form pp...hh...
175
        if ("p" * n_particle + "h" * n_hole) != space:
2✔
176
            return False
2✔
177
        # depending on the adc type n_particle and n_hole have to
178
        # be equal or differ e.g. by +-1 (IP/EA)
179
        if method.adc_type == "pp":
2!
180
            return n_particle == n_hole
2✔
NEW
181
        raise ValueError(f"Unknown adc type {method.adc_type} for method "
×
182
                         f"{method.name}. Can not validate space.")
183

184

185
class AdcMatrix(AdcMatrixlike):
2✔
186

187
    def __init__(self, method, hf_or_mp, block_orders=None, intermediates=None,
2✔
188
                 diagonal_precomputed=None):
189
        """
190
        Initialise an ADC matrix.
191

192
        Parameters
193
        ----------
194
        method : str or AdcMethod
195
            Method to use.
196
        hf_or_mp : adcc.ReferenceState or adcc.LazyMp
197
            HF reference or MP ground state
198
        block_orders : optional
199
            The order of perturbation theory to employ for each matrix block.
200
            If not set, defaults according to the selected ADC method are chosen.
201
        intermediates : adcc.Intermediates or NoneType
202
            Allows to pass intermediates to re-use to this class.
203
        diagonal_precomputed: adcc.AmplitudeVector
204
            Allows to pass a pre-computed diagonal, for internal use only.
205
        """
206
        if isinstance(hf_or_mp, (libadcc.ReferenceState,
2✔
207
                                 libadcc.HartreeFockSolution_i)):
208
            hf_or_mp = LazyMp(hf_or_mp)
2✔
209
        if not isinstance(hf_or_mp, LazyMp):
2!
210
            raise TypeError("hf_or_mp is not a valid object. It needs to be "
×
211
                            "either a LazyMp, a ReferenceState or a "
212
                            "HartreeFockSolution_i.")
213

214
        if not isinstance(method, AdcMethod):
2✔
215
            method = AdcMethod(method)
2✔
216

217
        if diagonal_precomputed:
2✔
218
            if not isinstance(diagonal_precomputed, AmplitudeVector):
2✔
219
                raise TypeError("diagonal_precomputed needs to be"
2✔
220
                                " an AmplitudeVector.")
221
            if diagonal_precomputed.needs_evaluation:
2✔
222
                raise ValueError("diagonal_precomputed must already"
2✔
223
                                 " be evaluated.")
224

225
        self.timer = Timer()
2✔
226
        self.method = method
2✔
227
        self.ground_state = hf_or_mp
2✔
228
        self.reference_state = hf_or_mp.reference_state
2✔
229
        self.mospaces = hf_or_mp.reference_state.mospaces
2✔
230
        self.is_core_valence_separated = method.is_core_valence_separated
2✔
231
        self.ndim = 2
2✔
232
        self.extra_terms = []
2✔
233

234
        self.intermediates = intermediates
2✔
235
        if self.intermediates is None:
2✔
236
            self.intermediates = Intermediates(self.ground_state)
2✔
237

238
        self.block_orders = self._default_block_orders(self.method)
2✔
239
        if block_orders is None:
2✔
240
            if method.level > 3:
2!
NEW
241
                raise NotImplementedError("The ADC secular matrix is not "
×
242
                                          f"implemented for method {method.name}.")
243
        else:
244
            self.block_orders.update(block_orders)
2✔
245
        self._validate_block_orders(
2✔
246
            block_orders=self.block_orders, method=self.method,
247
            allow_missing_diagonal_blocks=False
248
        )
249

250
        # Build the blocks and diagonals
251
        with self.timer.record("build"):
2✔
252
            variant = None
2✔
253
            if self.is_core_valence_separated:
2✔
254
                variant = "cvs"
2✔
255
            blocks = {
2✔
256
                block: ppmatrix.block(self.ground_state, block.split("_"),
257
                                      order=order, intermediates=self.intermediates,
258
                                      variant=variant)
259
                for block, order in self.block_orders.items() if order is not None
260
            }
261
            self.blocks = {bl: blocks[bl].apply for bl in blocks}
2✔
262
            if diagonal_precomputed:
2✔
263
                self._diagonal: AmplitudeVector = diagonal_precomputed
2✔
264
            else:
265
                self._diagonal: AmplitudeVector = sum(
2✔
266
                    bl.diagonal for bl in blocks.values() if bl.diagonal
267
                )
268
                self._diagonal.evaluate()
2✔
269
            self._init_space_data(self._diagonal)
2✔
270

271
    def __iadd__(self, other):
2✔
272
        """In-place addition of an :py:class:`AdcExtraTerm`
273

274
        Parameters
275
        ----------
276
        other : AdcExtraTerm
277
            the extra term to be added
278
        """
279
        if not isinstance(other, AdcExtraTerm):
2✔
280
            return NotImplemented
2✔
281
        if not all(k in self.blocks for k in other.blocks):
2✔
282
            raise ValueError("Can only add to blocks of"
2✔
283
                             " AdcMatrix that already exist.")
284
        for sp in other.blocks:
2✔
285
            orig_app = self.blocks[sp]
2✔
286
            other_app = other.blocks[sp].apply
2✔
287

288
            def patched_apply(ampl, original=orig_app, other=other_app):
2✔
289
                return sum(app(ampl) for app in (original, other))
2✔
290
            self.blocks[sp] = patched_apply
2✔
291
        other_diagonal = sum(bl.diagonal for bl in other.blocks.values()
2✔
292
                             if bl.diagonal)
293
        self._diagonal = self._diagonal + other_diagonal
2✔
294
        self._diagonal.evaluate()
2✔
295
        self.extra_terms.append(other)
2✔
296
        return self
2✔
297

298
    def __add__(self, other):
2✔
299
        """Addition of an :py:class:`AdcExtraTerm`, creating
300
        a copy of self and adding the term to the new matrix
301

302
        Parameters
303
        ----------
304
        other : AdcExtraTerm
305
            the extra term to be added
306

307
        Returns
308
        -------
309
        AdcMatrix
310
            a copy of the AdcMatrix with the extra term added
311
        """
312
        if not isinstance(other, AdcExtraTerm):
2✔
313
            return NotImplemented
2✔
314
        ret = AdcMatrix(self.method, self.ground_state,
2✔
315
                        block_orders=self.block_orders,
316
                        intermediates=self.intermediates,
317
                        diagonal_precomputed=self.diagonal())
318
        ret += other
2✔
319
        return ret
2✔
320

321
    def __radd__(self, other):
2✔
322
        return self.__add__(other)
2✔
323

324
    def _init_space_data(self, diagonal):
2✔
325
        """Update the cached data regarding the spaces of the ADC matrix"""
326
        self.axis_spaces = {}
2✔
327
        self.axis_lengths = {}
2✔
328
        for block in diagonal.blocks:
2✔
329
            self.axis_spaces[block] = getattr(diagonal, block).subspaces
2✔
330
            self.axis_lengths[block] = np.prod([
2✔
331
                self.mospaces.n_orbs(sp) for sp in self.axis_spaces[block]
332
            ])
333
        self.shape = (sum(self.axis_lengths.values()),
2✔
334
                      sum(self.axis_lengths.values()))
335

336
    def __repr__(self):
2✔
337
        ret = f"AdcMatrix({self.method.name}, "
2✔
338
        for b, o in self.block_orders.items():
2✔
339
            ret += f"{b}={o}, "
2✔
340
        return ret + ")"
2✔
341

342
    def __len__(self):
2✔
343
        return self.shape[0]
2✔
344

345
    @property
2✔
346
    def axis_blocks(self):
2✔
347
        """
348
        Return the blocks used along one of the axes of the ADC matrix
349
        (e.g. ['ph', 'pphh']).
350
        """
351
        # sort the keys by length to ensure that we always get
352
        # [singles, doubles, ...]
353
        return sorted(self.axis_spaces, key=len)
2✔
354

355
    def diagonal(self):
2✔
356
        """Return the diagonal of the ADC matrix"""
357
        return self._diagonal
2✔
358

359
    def block_apply(self, block, tensor):
2✔
360
        """
361
        Compute the application of a block of the ADC matrix
362
        with another AmplitudeVector or Tensor. Non-matching blocks
363
        in the AmplitudeVector will be ignored.
364
        """
365
        if not isinstance(tensor, libadcc.Tensor):
2!
366
            raise TypeError("tensor should be an adcc.Tensor")
×
367

368
        with self.timer.record(f"apply/{block}"):
2✔
369
            outblock, inblock = block.split("_")
2✔
370
            ampl = AmplitudeVector(**{inblock: tensor})
2✔
371
            ret = self.blocks[block](ampl)
2✔
372
            return getattr(ret, outblock)
2✔
373

374
    @timed_member_call()
2✔
375
    def matvec(self, v):
2✔
376
        """
377
        Compute the matrix-vector product of the ADC matrix
378
        with an excitation amplitude and return the result.
379
        """
380
        return sum(block(v) for block in self.blocks.values())
2✔
381

382
    def rmatvec(self, v):
2✔
383
        # ADC matrix is symmetric
384
        return self.matvec(v)
2✔
385

386
    def __matmul__(self, other):
2✔
387
        if isinstance(other, AmplitudeVector):
2✔
388
            return self.matvec(other)
2✔
389
        if isinstance(other, list):
2!
390
            if all(isinstance(elem, AmplitudeVector) for elem in other):
2!
391
                return [self.matvec(ov) for ov in other]
2✔
392
        return NotImplemented
×
393

394
    def block_view(self, block):
2✔
395
        """
396
        Return a view into the AdcMatrix that represents a single
397
        block of the matrix. Currently only diagonal blocks are supported.
398
        """
399
        b1, b2 = block.split("_")
2✔
400
        if b1 != b2:
2!
401
            raise NotImplementedError("Off-diagonal block views not yet "
×
402
                                      "implemented.")
403
            # TODO For off-diagonal blocks we probably need a different
404
            #      data structure as the AdcMatrix class as these block
405
            #      are inherently different than an AdcMatrix (no Hermiticity
406
            #      for example) and basically they only need to support some
407
            #      form of matrix-vector product and some statistics like
408
            #      spaces and sizes etc.
409
        block_orders = {bl: None for bl in self.block_orders.keys()}
2✔
410
        block_orders[block] = self.block_orders[block]
2✔
411
        return AdcMatrix(self.method, self.ground_state,
2✔
412
                         block_orders=block_orders,
413
                         intermediates=self.intermediates)
414

415
    def construct_symmetrisation_for_blocks(self):
2✔
416
        """
417
        Construct the symmetrisation functions, which need to be
418
        applied to relevant blocks of an AmplitudeVector in order
419
        to symmetrise it to the right symmetry in order to be used
420
        with the various matrix-vector-products of this function.
421

422
        Most importantly the returned functions antisymmetrise
423
        the occupied and virtual parts of the doubles parts
424
        if this is sensible for the method behind this adcmatrix.
425

426
        Returns a dictionary block identifier -> function
427
        """
428
        ret = {}
2✔
429
        if self.is_core_valence_separated:
2✔
430
            # CVS doubles part is antisymmetric wrt. (i,K,a,b) <-> (i,K,b,a)
431
            ret["pphh"] = lambda v: v.antisymmetrise([(2, 3)])
2✔
432
        else:
433
            def symmetrise_generic_adc_doubles(invec):
2✔
434
                # doubles part is antisymmetric wrt. (i,j,a,b) <-> (i,j,b,a)
435
                scratch = invec.antisymmetrise([(2, 3)])
2✔
436
                # doubles part is symmetric wrt. (i,j,a,b) <-> (j,i,b,a)
437
                return scratch.symmetrise([(0, 1), (2, 3)])
2✔
438
            ret["pphh"] = symmetrise_generic_adc_doubles
2✔
439
        return ret
2✔
440

441
    def dense_basis(self, axis_blocks=None, ordering="adcc"):
2✔
442
        """
443
        Return the list of indices and their values
444
        of the dense basis representation
445

446
        ordering: adcc, spin, spatial
447
        """
448
        ret = []
2✔
449
        if axis_blocks is None:
2!
450
            axis_blocks = self.axis_blocks
×
451
        if not isinstance(axis_blocks, list):
2!
452
            axis_blocks = [axis_blocks]
2✔
453

454
        # Define function to impose the order in the basis
455
        if ordering == "adcc":
2!
456
            def reduce_index(n_orbsa, idx):
2✔
457
                return idx, idx
2✔
458
        elif ordering == "spin":
×
459
            def reduce_index(n_orbsa, idx):
×
460
                is_beta = [idx[i] >= n_orbsa[i] for i in range(len(idx))]
×
461
                spatial = [idx[i] - n_orbsa[i] if is_beta[i] else idx[i]
×
462
                           for i in range(len(idx))]
463
                # Sort first by spin, then by spatial
464
                return (is_beta, spatial)
×
465
        elif ordering == "spatial":
×
466
            def reduce_index(n_orbsa, idx):
×
467
                is_beta = [idx[i] >= n_orbsa[i] for i in range(len(idx))]
×
468
                spatial = [idx[i] - n_orbsa[i] if is_beta[i] else idx[i]
×
469
                           for i in range(len(idx))]
470
                # Sort first by spatial, then by spin
471
                return (spatial, is_beta)
×
472

473
        if "ph" in axis_blocks:
2✔
474
            ret_s = []
2✔
475
            sp_s = self.axis_spaces["ph"]
2✔
476
            n_orbs_s = [self.mospaces.n_orbs(sp) for sp in sp_s]
2✔
477
            n_orbsa_s = [self.mospaces.n_orbs_alpha(sp) for sp in sp_s]
2✔
478
            for i in range(n_orbs_s[0]):
2✔
479
                for a in range(n_orbs_s[1]):
2✔
480
                    ret_s.append([((i, a), 1)])
2✔
481

482
            def sortfctn(x):
2✔
483
                return min(reduce_index(n_orbsa_s, idx) for idx, factor in x)
2✔
484
            ret_s.sort(key=sortfctn)
2✔
485
            ret_s.sort(key=sortfctn)
2✔
486
            ret.extend(ret_s)
2✔
487

488
        if "pphh" in axis_blocks:
2✔
489
            ret_d = []
2✔
490
            sp_d = self.axis_spaces["pphh"]
2✔
491
            n_orbsa_d = [self.mospaces.n_orbs_alpha(sp) for sp in sp_d]
2✔
492

493
            if sp_d[0] == sp_d[1] and sp_d[2] == sp_d[3]:
2✔
494
                nso = self.mospaces.n_orbs(sp_d[0])
2✔
495
                nsv = self.mospaces.n_orbs(sp_d[2])
2✔
496
                ret_d.extend([[((i, j, a, b), +1 / 2),
2✔
497
                               ((j, i, a, b), -1 / 2),
498
                               ((i, j, b, a), -1 / 2),
499
                               ((j, i, b, a), +1 / 2)]
500
                              for i in range(nso) for j in range(i)
501
                              for a in range(nsv) for b in range(a)])
502
            elif sp_d[2] == sp_d[3]:
2!
503
                nso = self.mospaces.n_orbs(sp_d[0])
2✔
504
                nsc = self.mospaces.n_orbs(sp_d[1])
2✔
505
                nsv = self.mospaces.n_orbs(sp_d[2])
2✔
506
                ret_d.extend([[((i, j, a, b), +1 / np.sqrt(2)),
2✔
507
                               ((i, j, b, a), -1 / np.sqrt(2))]
508
                              for i in range(nso) for j in range(nsc)
509
                              for a in range(nsv) for b in range(a)])
510
            else:
511
                nso = self.mospaces.n_orbs(sp_d[0])
×
512
                nsc = self.mospaces.n_orbs(sp_d[1])
×
513
                nsv = self.mospaces.n_orbs(sp_d[2])
×
514
                nsw = self.mospaces.n_orbs(sp_d[3])
×
515
                ret_d.append([((i, j, b, a), 1)
×
516
                              for i in range(nso) for j in range(nsc)
517
                              for a in range(nsv) for b in range(nsw)])
518

519
            def sortfctn(x):
2✔
520
                return min(reduce_index(n_orbsa_d, idx) for idx, factor in x)
2✔
521
            ret_d.sort(key=sortfctn)
2✔
522
            ret_d.sort(key=sortfctn)
2✔
523
            ret.extend(ret_d)
2✔
524

525
        if any(b not in ("ph", "pphh") for b in self.axis_blocks):
2!
526
            raise NotImplementedError("Blocks other than ph and pphh "
×
527
                                      "not implemented")
528
        return ret
2✔
529

530
    def to_ndarray(self, out=None):
2✔
531
        """
532
        Return the ADC matrix object as a dense numpy array. Converts the sparse
533
        internal representation of the ADC matrix to a dense matrix and return
534
        as a numpy array.
535

536
        Notes
537
        -----
538

539
        This method is only intended to be used for debugging and
540
        visualisation purposes as it involves computing a large amount of
541
        matrix-vector products and the returned array consumes a considerable
542
        amount of memory.
543

544
        The resulting matrix has no spin symmetry imposed, which means that
545
        its eigenspectrum may contain non-physical excitations (e.g. with linear
546
        combinations of α->β and α->α components in the excitation vector).
547

548
        This function has not been sufficiently tested to be considered stable.
549
        """
550
        # TODO Update to ph / pphh
551
        # TODO Still uses deprecated functions
552
        import tqdm
2✔
553

554
        from adcc import guess_zero
2✔
555

556
        # Get zero amplitude of the appropriate symmetry
557
        # (TODO: Only true for C1, where there is only a single irrep)
558
        ampl_zero = guess_zero(self)
2✔
559
        assert self.mospaces.point_group == "C1"
2✔
560

561
        # Build the shape of the returned array
562
        # Since the basis of the doubles block is not the unit vectors
563
        # this *not* equal to the shape of the AdcMatrix object
564
        basis = {b: self.dense_basis(b) for b in self.axis_blocks}
2✔
565
        mat_len = sum(len(basis[b]) for b in basis)
2✔
566

567
        if out is None:
2!
568
            out = np.zeros((mat_len, mat_len))
2✔
569
        else:
570
            if out.shape != (mat_len, mat_len):
×
571
                raise ValueError("Output array has shape ({0:}, {1:}), but "
×
572
                                 "shape ({2:}, {2:}) is required."
573
                                 "".format(*out.shape, mat_len))
574
            out[:] = 0  # Zero all data in out.
×
575

576
        # Check for the cases actually implemented
577
        if any(b not in ("ph", "pphh") for b in self.axis_blocks):
2!
578
            raise NotImplementedError("Blocks other than ph and pphh "
×
579
                                      "not implemented")
580
        if "ph" not in self.axis_blocks:
2!
581
            raise NotImplementedError("Block 'ph' needs to be present")
×
582

583
        # Extract singles-singles block (contiguous)
584
        assert "ph" in self.axis_blocks
2✔
585
        n_orbs_ph = [self.mospaces.n_orbs(sp) for sp in self.axis_spaces["ph"]]
2✔
586
        n_ph = np.prod(n_orbs_ph)
2✔
587
        assert len(basis["ph"]) == n_ph
2✔
588
        view_ss = out[:n_ph, :n_ph].reshape(*n_orbs_ph, *n_orbs_ph)
2✔
589
        for i in range(n_orbs_ph[0]):
2✔
590
            for a in range(n_orbs_ph[1]):
2✔
591
                ampl = ampl_zero.copy()
2✔
592
                ampl.ph[i, a] = 1
2✔
593
                view_ss[:, :, i, a] = (self @ ampl).ph.to_ndarray()
2✔
594

595
        # Extract singles-doubles and doubles-doubles block
596
        if "pphh" in self.axis_blocks:
2✔
597
            assert self.axis_blocks == ["ph", "pphh"]
2✔
598
            view_sd = out[:n_ph, n_ph:].reshape(*n_orbs_ph, len(basis["pphh"]))
2✔
599
            view_dd = out[n_ph:, n_ph:]
2✔
600
            for j, bas1 in tqdm.tqdm(enumerate(basis["pphh"]),
2✔
601
                                     total=len(basis["pphh"])):
602
                ampl = ampl_zero.copy()
2✔
603
                for idx, val in bas1:
2✔
604
                    ampl.pphh[idx] = val
2✔
605
                ret_ampl = self @ ampl
2✔
606
                view_sd[:, :, j] = ret_ampl.ph.to_ndarray()
2✔
607

608
                for i, bas2 in enumerate(basis["pphh"]):
2✔
609
                    view_dd[i, j] = sum(val * ret_ampl.pphh[idx]
2✔
610
                                        for idx, val in bas2)
611

612
            out[n_ph:, :n_ph] = np.transpose(out[:n_ph, n_ph:])
2✔
613
        return out
2✔
614

615

616
class AdcMatrixShifted(AdcMatrix):
2✔
617
    def __init__(self, matrix, shift=0.0):
2✔
618
        """
619
        Initialise a shifted ADC matrix. Applying this class to a vector ``v``
620
        represents an efficient version of ``matrix @ v + shift * v``.
621

622
        Parameters
623
        ----------
624
        matrix : AdcMatrix
625
            Matrix which is shifted
626
        shift : float
627
            Value by which to shift the matrix
628
        """
629
        super().__init__(matrix.method, matrix.ground_state,
2✔
630
                         block_orders=matrix.block_orders,
631
                         intermediates=matrix.intermediates)
632
        self.shift = shift
2✔
633

634
    def matvec(self, in_ampl):
2✔
635
        out = super().matvec(in_ampl)
2✔
636
        out = out + self.shift * in_ampl
2✔
637
        return out
2✔
638

639
    def to_ndarray(self, out=None):
2✔
640
        super().to_ndarray(self, out)
×
641
        out = out + self.shift * np.eye(*out.shape)
×
642
        return out
×
643

644
    def block_apply(self, block, in_vec):
2✔
645
        ret = super().block_apply(block, in_vec)
×
646
        inblock, outblock = block.split("_")
×
647
        if inblock == outblock:
×
648
            ret += self.shift * in_vec
×
649
        return ret
×
650

651
    def diagonal(self):
2✔
652
        out = super().diagonal()
2✔
653
        out = out + self.shift  # Shift the diagonal
2✔
654
        return out
2✔
655

656
    def block_view(self, block):
2✔
657
        raise NotImplementedError("Block-view not yet implemented for "
×
658
                                  "shifted ADC matrices.")
659
        # TODO The way to implement this is to ask the inner matrix to
660
        #      a block_view and then wrap that in an AdcMatrixShifted.
661

662

663
class AdcMatrixProjected(AdcMatrix):
2✔
664
    def __init__(self, matrix, excitation_blocks, core_orbitals=None,
2✔
665
                 outer_virtuals=None):
666
        """
667
        Initialise a projected ADC matrix, i.e. represents the expression
668
        ``P @ M @ P`` where ``P`` is a projector onto a subset of
669
        ``excitation_blocks``.
670

671
        The ``excitation_blocks`` are defined by partitioning the ``o1`` occupied
672
        and ``v1`` virtual space of the ``matrix.mospaces`` into a core-occupied
673
        ``c``, valence-occupied ``o``, inner-virtual ``v`` and outer-virtual ``w``.
674
        This matrix will only keep selected blocks in the amplitudes non-zero, which
675
        are selected in the ``excitation_blocks`` list
676
        (e.g. ``["cv", "ccvv", "ocvv"]``).
677

678
        For details on the option how to select the spaces, see the documentation
679
        in :py:`adcc.ReferenceState.__init__` (``outer_virtuals`` follows the same
680
        rules as ``frozen_virtuals``).
681

682
        Parameters
683
        ----------
684
        matrix : AdcMatrix
685
            Matrix which is projected
686
        excitation_blocks : list
687
            Excitation blocks to keep in the Amplitudes.
688
        core_orbitals : int or list or tuple, optional
689
            The orbitals to be put into the ``c`` core space.
690
        outer_virtuals : int or list or tuple, optional
691
            The virtuals to be put into the ``w`` outer-virtual space.
692
        """
693
        from .projection import Projector, SubspacePartitioning
2✔
694

695
        for sp in excitation_blocks:
2✔
696
            if not any(len(sp) == len(ax_sp)
2!
697
                       for ax_sp in matrix.axis_spaces.values()):
698
                raise ValueError(f"Invalid partition block {sp}.")
×
699

700
        super().__init__(matrix.method, matrix.ground_state,
2✔
701
                         block_orders=matrix.block_orders,
702
                         intermediates=matrix.intermediates)
703
        partitioning = SubspacePartitioning(matrix.mospaces, core_orbitals,
2✔
704
                                            outer_virtuals)
705

706
        projectors = {}
2✔
707
        for block in matrix.axis_spaces.keys():
2✔
708
            block_partitions = [sp for sp in excitation_blocks
2✔
709
                                if len(sp) == len(matrix.axis_spaces[block])]
710
            projectors[block] = Projector(matrix.axis_spaces[block],
2✔
711
                                          partitioning, block_partitions)
712
        self.projectors = projectors
2✔
713

714
    def apply_projection(self, in_ampl):
2✔
715
        return AmplitudeVector(**{
2✔
716
            block: self.projectors[block] @ in_ampl[block]
717
            for block in in_ampl.keys()
718
        })
719

720
    def matvec(self, in_ampl):
2✔
721
        in_proj = self.apply_projection(in_ampl)
2✔
722
        out = super().matvec(in_proj)
2✔
723
        return self.apply_projection(out)
2✔
724

725
    def block_apply(self, block, in_vec):
2✔
726
        inblock, outblock = block.split("_")
×
727
        in_proj = self.projectors[inblock].apply(in_vec)
×
728
        ret = super().block_apply(block, in_proj)
×
729
        return self.projectors[outblock].apply(ret)
×
730

731
    def diagonal(self):
2✔
732
        blocks = {}
2✔
733
        for (block, diagblock) in super().diagonal().items():
2✔
734
            # On the diagonal don't set the ignored amplitudes to zero,
735
            # but instead set them to a very large value to (a) avoid these being
736
            # selected for the initial guess and (b) ensure the preconditioning
737
            # naturally reduces components along this direction.
738
            P = self.projectors[block]
2✔
739
            one = ones_like(diagblock)
2✔
740
            blocks[block] = P @ diagblock + 100000 * (one - P @ one)
2✔
741
        return AmplitudeVector(**blocks)
2✔
742

743
    def block_view(self, block):
2✔
744
        raise NotImplementedError("Block-view not yet implemented for "
×
745
                                  "projected ADC matrices.")
746
        # TODO The way to implement this is to ask the inner matrix to
747
        #      a block_view and then wrap that in an AdcMatrixProjected.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc