Skip to content

API Documentation

Source code in qcmanybody/manybody.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def __init__(
    self,
    molecule: Molecule,
    bsse_type: Sequence[BsseEnum],
    levels: Mapping[Union[int, Literal["supersystem"]], str],
    return_total_data: bool,
    supersystem_ie_only: bool,
    embedding_charges: Mapping[int, Sequence[float]],
):
    self.embedding_charges = embedding_charges
    self.molecule = molecule
    self.bsse_type = [BsseEnum(x) for x in bsse_type]
    self.return_total_data = return_total_data
    self.supersystem_ie_only = supersystem_ie_only
    self.nfragments = len(molecule.fragments)

    self.levels = levels

    # Levels without supersystem
    self.levels_no_ss = {int(k): v for k, v in levels.items() if k != "supersystem"}

    # Just a set of all the modelchems
    self.mc_levels = set(self.levels.values())

    self.max_nbody = max(self.levels_no_ss.keys())

    if len(self.bsse_type) == 0:
        raise ValueError("No BSSE correction specified")

    if BsseEnum.vmfc in self.bsse_type and len(set(self.levels.values())) == 1:
        # For single-modelchem VMFC, NOCP & sometimes CP are produced for free
        if BsseEnum.nocp not in self.bsse_type:
            self.bsse_type.append(BsseEnum.nocp)
        if BsseEnum.cp not in self.bsse_type and self.max_nbody == self.nfragments:
            self.bsse_type.append(BsseEnum.cp)

    self.return_bsse_type = self.bsse_type[0]

    ###############################
    # Build nbodies_per_mc_level
    # TODO - use Lori's code
    # TODO - dict to list of lists to handle non-contiguous levels
    # TODO multilevel and supersystem_ie_only=T not allowed together
    # TODO supersystem in levels is not to be trusted -- nfrag only and skips levels
    max_level = max(self.levels_no_ss.keys())

    if set(range(1, max_level + 1)) != set(self.levels_no_ss.keys()):
        raise ValueError(f"Levels must be contiguous from 1 to {max_level}")

    self.nbodies_per_mc_level: Dict[str, list] = {mc_level: [] for mc_level in self.mc_levels}
    for k, v in self.levels_no_ss.items():
        self.nbodies_per_mc_level[v].append(k)

    self.nbodies_per_mc_level = {k: sorted(v) for k, v in self.nbodies_per_mc_level.items()}

    # Supersystem is always at the end
    if "supersystem" in levels:
        ss_mc = levels["supersystem"]
        self.nbodies_per_mc_level[ss_mc].append("supersystem")

    # To be built on the fly
    self.mc_compute_dict = None

    if not np.array_equal(np.concatenate(self.molecule.fragments), np.arange(len(self.molecule.symbols))):
        raise ValueError("""QCManyBody: non-contiguous fragments could be implemented but aren't at present""")

    # Build size and slices dictionaries. Assumes fragments are contiguous
    self.fragment_size_dict = {}
    self.fragment_slice_dict = {}
    iat = 0
    for ifr in range(1, self.nfragments + 1):
        nat = len(self.molecule.fragments[ifr - 1])
        self.fragment_size_dict[ifr] = nat
        self.fragment_slice_dict[ifr] = slice(iat, iat + nat)
        iat += nat

format_calc_plan

format_calc_plan(sset: str = 'all') -> Tuple[str, Dict[str, Dict[int, int]]]

Formulate per-modelchem and per-body job count data and summary text.

Parameters:

Name Type Description Default
sset str

Among {"all", "nocp", "cp", "vmfc_compute"}, which data structure to return.

'all'

Returns:

Type Description
info

A text summary with per- model chemistry and per- n-body-level job counts.

Dict[str, Dict[int, int]]

Data structure with outer key mc-label, inner key 1-indexed n-body, value job count.

Source code in qcmanybody/manybody.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def format_calc_plan(self, sset: str = "all") -> Tuple[str, Dict[str, Dict[int, int]]]:
    """Formulate per-modelchem and per-body job count data and summary text.

    Parameters
    ----------
    sset
        Among {"all", "nocp", "cp", "vmfc_compute"}, which data structure to return.

    Returns
    -------
    info
        A text summary with per- model chemistry and per- n-body-level job counts.
    Dict[str, Dict[int, int]]
        Data structure with outer key mc-label, inner key 1-indexed n-body, value job count.
    """
    # Rearrange compute_list from key nb having values (species) to compute all of that nb
    #   to key nb having values counting that nb.
    compute_list_count = {}
    for mc, compute_dict in self.compute_map.items():
        compute_list_count[mc] = {}
        for sub in compute_dict:  # all, nocp, cp, vmfc
            all_calcs = set().union(*compute_dict[sub].values())
            compute_list_count[mc][sub] = Counter([len(frag) for (frag, _) in all_calcs])

    info = []
    for mc, counter in compute_list_count.items():
        all_counter = counter["all"]
        info.append(f"    Model chemistry \"{mc}\" (???):    {sum(all_counter.values())}")
        for nb, count in sorted(all_counter.items()):
            other_counts = [f"{sub}: {counter[sub][nb]}" for sub in ["nocp", "cp", "vmfc_compute"]]
            info.append(f"        Number of {nb}-body computations: {count:6} ({', '.join(other_counts)})")
        info.append("")
    info = "\n".join(info)

    logger.info(info)
    return info, {mc: dsset[sset] for mc, dsset in compute_list_count.items()}

iterate_molecules

iterate_molecules() -> Iterable[Tuple[str, str, Molecule]]

Iterate over all the molecules needed for the computation.

Yields model chemistry, label, and molecule.

Source code in qcmanybody/manybody.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def iterate_molecules(self) -> Iterable[Tuple[str, str, Molecule]]:
    """Iterate over all the molecules needed for the computation.

    Yields model chemistry, label, and molecule.
    """

    done_molecules = set()

    for mc, compute_dict in self.compute_map.items():
        # TODO - this is a bit of a hack. Lots of duplication when reaching higher nbody
        for compute_list in compute_dict["all"].values():
            for real_atoms, basis_atoms in compute_list:
                label = labeler(mc, real_atoms, basis_atoms)
                if label in done_molecules:
                    continue

                ghost_atoms = list(set(basis_atoms) - set(real_atoms))

                # Shift to zero-indexing
                real_atoms_0 = [x - 1 for x in real_atoms]
                ghost_atoms_0 = [x - 1 for x in ghost_atoms]
                mol = self.molecule.get_fragment(real_atoms_0, ghost_atoms_0, orient=False, group_fragments=False)
                mol = mol.copy(update={"fix_com": True, "fix_orientation": True})

                if self.embedding_charges:
                    embedding_frags = list(set(range(1, self.nfragments + 1)) - set(basis_atoms))
                    charges = []
                    for ifr in embedding_frags:
                        positions = self.molecule.get_fragment(ifr-1).geometry.tolist()
                        charges.extend([[chg, i] for i, chg in zip(positions, self.embedding_charges[ifr])])
                    mol.extras["embedding_charges"] = charges

                done_molecules.add(label)
                yield mc, label, mol

analyze

analyze(component_results: Dict[str, Dict[str, Union[float, ndarray]]])

Parameters:

Name Type Description Default
component_results Dict[str, Dict[str, Union[float, ndarray]]]

Nested dictionary with results from all individual molecular system calculations, including all subsystem/basis combinations, all model chemistries, and all properties (e.g., e/g/h).

For example, the below is the format for a nocp gradient run on a helium dimer with 1-body at CCSD and 2-body at MP2. The outer string key can be generated with the qcmanybody.utils.labeler function. The inner string key is any property; QCManyBody presently knows how to process energy/gradient/Hessian.

{'["ccsd", [1], [1]]': {'energy': -2.87, 'gradient': array([[0., 0., 0.]])}, '["ccsd", [2], [2]]': {'energy': -2.87, 'gradient': array([[0., 0., 0.]])}, '["mp2", [1], [1]]': {'energy': -2.86, 'gradient': array([[0., 0., 0.]])}, '["mp2", [2], [2]]': {'energy': -2.86, 'gradient': array([[0., 0., 0.]])}, '["mp2", [1, 2], [1, 2]]': {'energy': -5.73, 'gradient': array([[ 0., 0., 0.0053], [ 0., 0., -0.0053]])}, }

required
Return
Source code in qcmanybody/manybody.py
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
    def analyze(
        self,
        component_results: Dict[str, Dict[str, Union[float, np.ndarray]]],
    ):
        """

        Parameters
        ----------
        component_results
            Nested dictionary with results from all individual molecular system
            calculations, including all subsystem/basis combinations, all model
            chemistries, and all properties (e.g., e/g/h).

            For example, the below is the format for a nocp gradient run on a
            helium dimer with 1-body at CCSD and 2-body at MP2. The outer string
            key can be generated with the ``qcmanybody.utils.labeler`` function.
            The inner string key is any property; QCManyBody presently knows how
            to process energy/gradient/Hessian.

            {'["ccsd", [1], [1]]': {'energy': -2.87, 'gradient': array([[0., 0., 0.]])},
             '["ccsd", [2], [2]]': {'energy': -2.87, 'gradient': array([[0., 0., 0.]])},
             '["mp2", [1], [1]]': {'energy': -2.86, 'gradient': array([[0., 0., 0.]])},
             '["mp2", [2], [2]]': {'energy': -2.86, 'gradient': array([[0., 0., 0.]])},
             '["mp2", [1, 2], [1, 2]]': {'energy': -5.73, 'gradient': array([[ 0., 0., 0.0053], [ 0., 0., -0.0053]])},
            }

        Return
        ------

        """

        # All properties that were passed to us
        # * seed with "energy" so free/no-op jobs can process
        available_properties: Set[str] = {"energy"}
        for property_data in component_results.values():
            available_properties.update(property_data.keys())

        # reorganize to component_results_inv[property][label] = 1.23
        component_results_inv = {k: {} for k in available_properties}

        for cluster_label, property_data in component_results.items():
            for property_label, property_value in property_data.items():
                component_results_inv[property_label][cluster_label] = property_value

        # Remove any missing data
        component_results_inv = {k: v for k, v in component_results_inv.items() if v}
        if not component_results_inv:
            # Note B: Rarely, "no results" is expected, like for CP-only,
            #   rtd=False, and max_nbody=1. We'll add a dummy entry so
            #   processing can continue.
            component_results_inv["energy"] = {'["dummy", [1000], [1000]]': 0.0}

        # Actually analyze
        is_embedded = bool(self.embedding_charges)
        component_properties = defaultdict(dict)
        all_results = {}
        nbody_dict = {}
        stdout = ""
#        all_results["energy_body_dict"] = {"cp": {1: 0.0}}

        for property_label, property_results in component_results_inv.items():
            # Expand gradient and hessian
            if property_label == "gradient":
                property_results = {k: self.resize_gradient(v, delabeler(k)[2]) for k, v in property_results.items()}
            if property_label == "hessian":
                property_results = {k: self.resize_hessian(v, delabeler(k)[2]) for k, v in property_results.items()}

            r = self._analyze(property_label, property_results)
            for k, v in property_results.items():
                component_properties[k]["calcinfo_natom"] = len(self.molecule.symbols)
                component_properties[k][f"return_{property_label}"] = v
            all_results.update(r)

        for bt in self.bsse_type:
            stdout += print_nbody_energy(
                all_results["energy_body_dict"][bt],
                f"{bt.formal()} ({bt.abbr()})",
                self.nfragments,
                is_embedded,
                self.supersystem_ie_only,
                self.max_nbody if self.has_supersystem else None,
            )

        for property_label in available_properties:
            for bt in self.bsse_type:
                nbody_dict.update(
                    collect_vars(
                        bt.upper(),
                        property_label.upper(),
                        all_results[f"{property_label}_body_dict"][bt],
                        self.max_nbody,
                        is_embedded,
                        self.supersystem_ie_only,
                        self.has_supersystem,
                    )
                )

        all_results["results"] = nbody_dict
        all_results["component_properties"] = component_properties

        # Make dictionary with "1cp", "2cp", etc
        ebd = all_results["energy_body_dict"]
        all_results["energy_body_dict"] = {str(k) + bt: v for bt in ebd for k, v in ebd[bt].items()}
        all_results["stdout"] = stdout

        return all_results

qcmanybody.computer.ManyBodyComputer

Bases: BaseComputerQCNG

input_data class-attribute instance-attribute

input_data: ManyBodyInput = Field(..., description='Input schema containing the relevant settings for performing the many body expansion. This is entirely redundant with the piecemeal assembly of this Computer class and is only stored to be available for error handling and exact reconstruction of ManyBodyResult.')

bsse_type class-attribute instance-attribute

bsse_type: List[BsseEnum] = Field([cp], description=description)

molecule class-attribute instance-attribute

molecule: Molecule = Field(..., description='Target molecule for many body expansion (MBE) or interaction energy (IE) analysis. Fragmentation should already be defined in `fragments` and related fields.')

driver class-attribute instance-attribute

driver: DriverEnum = Field(..., description='The computation driver; i.e., energy, gradient, hessian. In case of ambiguity (e.g., MBE gradient through finite difference energies or MBE energy through composite method), this field refers to the *target* derivative, not any *means* specification.')

embedding_charges class-attribute instance-attribute

embedding_charges: Optional[Dict[int, List[float]]] = Field(None, description="Atom-centered point charges to be used to speed up nbody-level convergence. Charges are placed on molecule fragments whose basis sets are not included in the computation. (An implication is that charges aren't invoked for bsse_type=cp.) Keys: 1-based index of fragment. Values: list of atom charges for that fragment.", json_schema_extra={'shape': ['nfr', '<varies: nat in ifr>']})

return_total_data class-attribute instance-attribute

return_total_data: Optional[bool] = Field(None, validate_default=True, description=description)

levels class-attribute instance-attribute

levels: Optional[Dict[Union[int, Literal['supersystem']], str]] = Field(None, validate_default=True, description=description + "Examples above are processed in the ManyBodyComputer, and once processed, only the values should be used. The keys turn into nbodies_per_mc_level, as notated below. * {1: 'ccsd(t)', 2: 'mp2', 'supersystem': 'scf'} -> nbodies_per_mc_level=[[1], [2], ['supersystem']] * {2: 'ccsd(t)/cc-pvdz', 3: 'mp2'} -> nbodies_per_mc_level=[[1, 2], [3]] ")

max_nbody class-attribute instance-attribute

max_nbody: Optional[int] = Field(None, validate_default=True, description=description)

supersystem_ie_only class-attribute instance-attribute

supersystem_ie_only: Optional[bool] = Field(False, validate_default=True, description=description)

task_list class-attribute instance-attribute

task_list: Dict[str, Any] = {}

qcmb_calculator class-attribute instance-attribute

qcmb_calculator: Optional[Any] = Field(None, description='Low-level interface')

nfragments property

nfragments: int

nbodies_per_mc_level property

nbodies_per_mc_level: List[List[Union[int, Literal['supersystem']]]]

Config

set_bsse_type classmethod

set_bsse_type(v: Any) -> List[BsseEnum]
Source code in qcmanybody/computer.py
207
208
209
210
211
212
213
214
215
216
217
@validator("bsse_type", pre=True)
@classmethod
def set_bsse_type(cls, v: Any) -> List[BsseEnum]:
    if not isinstance(v, list):
        v = [v]
    # emulate ordered set
    # * bt.lower() as return (w/i `list(dict.fromkeys([bt.lower() ...`)
    #   works until aliases added to BsseEnum
    # * BsseEnum[bt].value as return works for good vals, but passing bad
    #   vals through as bt lets pydantic raise a clearer error message
    return list(dict.fromkeys([(BsseEnum[bt.lower()].value if bt.lower() in BsseEnum.__members__ else bt.lower()) for bt in v]))

set_embedding_charges classmethod

set_embedding_charges(v, values)
Source code in qcmanybody/computer.py
220
221
222
223
224
225
226
227
228
229
230
231
@validator("embedding_charges", pre=True)
@classmethod
# v2: def set_embedding_charges(cls, v: Any, info: FieldValidationInfo) -> Dict[int, List[float]]:
def set_embedding_charges(cls, v, values): # -> Dict[int, List[float]]:
    # print(f"hit embedding_charges validator with {v}", end="")
    nfr = len(values["molecule"].fragments)
    # v2: if len(v) != info.data["nfragments"]:
    if v and len(v) != nfr:
        raise ValueError(f"embedding_charges dict should have entries for each 1-indexed fragment ({nfr})")

    # print(f" ... setting embedding_charges={v}")
    return v

set_return_total_data classmethod

set_return_total_data(v: Optional[bool], values) -> bool
Source code in qcmanybody/computer.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
@validator("return_total_data", always=True)
@classmethod
# v2: def set_return_total_data(cls, v: Optional[bool], info: FieldValidationInfo) -> bool:
def set_return_total_data(cls, v: Optional[bool], values) -> bool:
    # print(f"hit return_total_data validator with {v}", end="")
    if v is not None:
        rtd = v
    # v2: elif info.data["driver"] in ["gradient", "hessian"]:
    elif values["driver"] in ["gradient", "hessian"]:
        rtd = True
    else:
        rtd = False

    # v2: if info.data.get("embedding_charges", False) and rtd is False:
    if values.get("embedding_charges", False) and rtd is False:
        raise ValueError("Cannot return interaction data when using embedding scheme.")

    # print(f" ... setting rtd={rtd}")
    return rtd

set_levels classmethod

set_levels(v: Any, values) -> Dict[Union[int, Literal['supersystem']], str]
Source code in qcmanybody/computer.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
@validator("levels", always=True)
@classmethod
# v2: def set_levels(cls, v: Any, info: FieldValidationInfo) -> Dict[Union[int, Literal["supersystem"]], str]:
def set_levels(cls, v: Any, values) -> Dict[Union[int, Literal["supersystem"]], str]:
    # print(f"hit levels validator with {v}", end="")

    if v is None:
        pass
        # TODO levels = {plan.max_nbody: method}
        #v = {info.data["nfragments"]: "???method???"}
        #v = {len(info.data["molecule"].fragments): "???method???"}
        # v2: v = {len(info.data["molecule"].fragments): "(auto)"}
        v = {len(values["molecule"].fragments): "(auto)"}
    else:
        # rearrange bodies in order with supersystem last lest body count fail in organization loop below
        v = dict(sorted(v.items(), key=lambda item: 1000 if item[0] == "supersystem" else item[0]))

    # print(f" ... setting levels={v}")
    return v

set_max_nbody classmethod

set_max_nbody(v: Any, values) -> int
Source code in qcmanybody/computer.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
@validator("max_nbody", always=True)
@classmethod
# v2: def set_max_nbody(cls, v: Any, info: FieldValidationInfo) -> int:
def set_max_nbody(cls, v: Any, values) -> int:
    # print(f"hit max_nbody validator with {v}", end="")
    # v2: levels_max_nbody = max(nb for nb in info.data["levels"] if nb != "supersystem")
    # v2: nfr = len(info.data["molecule"].fragments)
    levels_max_nbody = max(nb for nb in values["levels"] if nb != "supersystem")
    nfr = len(values["molecule"].fragments)
    # print(f" {levels_max_nbody=} {nfr=}", end="")

    #ALT if v == -1:
    if v is None:
        v = levels_max_nbody
    elif v < 0 or v > nfr:
        raise ValueError(f"max_nbody={v} should be between 1 and {nfr}.")
    elif v != levels_max_nbody:
        #raise ValueError(f"levels={levels_max_nbody} contradicts user max_nbody={v}.")
        # TODO reconsider logic. move this from levels to here?
        # v2: info.data["levels"] = {v: "(auto)"}
        values["levels"] = {v: "(auto)"}
    else:
        pass
        # TODO once was           return min(v, nfragments)

    # print(f" ... setting max_nbody={v}")
    return v

set_supersystem_ie_only classmethod

set_supersystem_ie_only(v: Optional[bool], values) -> bool
Source code in qcmanybody/computer.py
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
@validator("supersystem_ie_only", always=True)
@classmethod
# v2: def set_supersystem_ie_only(cls, v: Optional[bool], info: FieldValidationInfo) -> bool:
def set_supersystem_ie_only(cls, v: Optional[bool], values) -> bool:
    # print(f"hit supersystem_ie_only validator with {v}", end="")
    sio = v
    # v2: _nfr = len(info.data["molecule"].fragments)
    _nfr = len(values["molecule"].fragments)

    # v2: _max_nbody = info.data["max_nbody"]
    # get(..., None) b/c in v1, all fields processed even if max_nbody previously failed
    _max_nbody = values.get("max_nbody", None)
    if (sio is True) and (_max_nbody != _nfr):
        raise ValueError(f"Cannot skip intermediate n-body jobs when max_nbody={_max_nbody} != nfragments={_nfr}.")

    if (sio is True) and ("vmfc" in values["bsse_type"]):
        raise ValueError(f"Cannot skip intermediate n-body jobs when VMFC in bsse_type={values['bsse_type']}. Use CP instead.")

    # print(f" ... setting {sio=}")
    return sio

from_manybodyinput classmethod

from_manybodyinput(input_model: ManyBodyInput, build_tasks: bool = True)
Source code in qcmanybody/computer.py
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
@classmethod
def from_manybodyinput(cls, input_model: ManyBodyInput, build_tasks: bool = True):

    computer_model = cls(
        molecule=input_model.molecule,
        driver=input_model.specification.driver,
        # v2: **input_model.specification.keywords.model_dump(exclude_unset=True),
        **input_model.specification.keywords.dict(exclude_unset=True),
        input_data=input_model,  # storage, to reconstitute ManyBodyResult
    )
    nb_per_mc = computer_model.nbodies_per_mc_level

    # print("\n<<<  (ZZ 1) QCEngine harness ManyBodyComputerQCNG.from_qcschema_ben  >>>")
    # v2: pprint.pprint(computer_model.model_dump(), width=200)
    # pprint.pprint(computer_model.dict(), width=200)
    # print(f"nbodies_per_mc_level={nb_per_mc}")

    comp_levels = {}
    for mc_level_idx, mtd in enumerate(computer_model.levels.values()):
        for lvl1 in nb_per_mc[mc_level_idx]:
            key = "supersystem" if lvl1 == "supersystem" else int(lvl1)
            comp_levels[key] = mtd

    specifications = {}
    for mtd, spec in computer_model.input_data.specification.specification.items():
        spec = spec.dict()
        specifications[mtd] = {}
        specifications[mtd]["program"] = spec.pop("program")
        specifications[mtd]["specification"] = spec
        specifications[mtd]["specification"]["driver"] = computer_model.driver  # overrides atomic driver with mb driver
        specifications[mtd]["specification"].pop("schema_name", None)

    computer_model.qcmb_calculator = ManyBodyCalculator(
        computer_model.molecule,
        computer_model.bsse_type,
        comp_levels,
        computer_model.return_total_data,
        computer_model.supersystem_ie_only,
        computer_model.embedding_charges,
    )

    if not build_tasks:
        return computer_model

    component_properties = {}
    component_results = {}

    for chem, label, imol in computer_model.qcmb_calculator.iterate_molecules():
        inp = AtomicInput(molecule=imol, **specifications[chem]["specification"])
        # inp = AtomicInput(molecule=imol, **specifications[chem]["specification"], extras={"psiapi": True})  # faster for p4

        if imol.extras.get("embedding_charges"):  # or test on self.embedding_charges ?
            if specifications[chem]["program"] == "psi4":
                charges = imol.extras["embedding_charges"]
                fkw = inp.keywords.get("function_kwargs", {})
                fkw.update({"external_potentials": charges})
                inp.keywords["function_kwargs"] = fkw
            else:
                raise RuntimeError(f"Don't know how to handle external charges in {specifications[chem]['program']}")

        _, real, bas = delabeler(label)
        result = qcng.compute(inp, specifications[chem]["program"])
        component_results[label] = result

        if not result.success:
            # print(result.error.error_message)
            raise RuntimeError("Calculation did not succeed! Error:\n" + result.error.error_message)

        # pull out stuff
        props = {"energy", "gradient", "hessian"}

        component_properties[label] = {}

        for p in props:
            if hasattr(result.properties, f"return_{p}"):
                v = getattr(result.properties, f"return_{p}")
                # print(f"  {label} {p}: {v}")
                if v is not None:
                    component_properties[label][p] = v

    # print("\n<<<  (ZZ 2) QCEngine harness ManyBodyComputerQCNG.from_qcschema_ben component_properties  >>>")
    # pprint.pprint(component_properties, width=200)

    analyze_back = computer_model.qcmb_calculator.analyze(component_properties)
    analyze_back["nbody_number"] = len(component_properties)
    # print("\n<<<  (ZZ 3) QCEngine harness ManyBodyComputerQCNG.from_qcschema_ben analyze_back  >>>")
    # pprint.pprint(analyze_back, width=200)

    return computer_model.get_results(external_results=analyze_back, component_results=component_results)

plan

plan()
Source code in qcmanybody/computer.py
460
461
462
def plan(self):
    # uncalled function
    return [t.plan() for t in self.task_list.values()]

compute

compute(client: Optional[FractalClient] = None) -> None

Run quantum chemistry.

NOTE: client logic removed (compared to psi4.driver.ManyBodyComputer)

Source code in qcmanybody/computer.py
464
465
466
467
468
469
470
def compute(self, client: Optional["qcportal.FractalClient"] = None) -> None:
    """Run quantum chemistry.

    NOTE: client logic removed (compared to psi4.driver.ManyBodyComputer)
    """
    for t in self.task_list.values():
        t.compute(client=client)

get_results

get_results(external_results: Dict, component_results: Dict, client: Optional[FractalClient] = None) -> ManyBodyResult

Return results as ManyBody-flavored QCSchema.

Source code in qcmanybody/computer.py
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
    def get_results(self, external_results: Dict, component_results: Dict, client: Optional["qcportal.FractalClient"] = None) -> ManyBodyResult:
        """Return results as ManyBody-flavored QCSchema."""

        ret_energy = external_results.pop("ret_energy")
        ret_ptype = ret_energy if self.driver == "energy" else external_results.pop(f"ret_{self.driver.name}")
        ret_gradient = external_results.pop("ret_gradient", None)
        nbody_number = external_results.pop("nbody_number")
        component_properties = external_results.pop("component_properties")
        stdout = external_results.pop("stdout")

        # load QCVariables
        qcvars = {
            'NUCLEAR REPULSION ENERGY': self.molecule.nuclear_repulsion_energy(),
            'NBODY NUMBER': nbody_number,
        }

        properties = {
            "calcinfo_nmc": len(self.nbodies_per_mc_level),
            "calcinfo_nfr": self.nfragments,  # or len(self.molecule.fragments)
            "calcinfo_natom": len(self.molecule.symbols),
            "calcinfo_nmbe": nbody_number,
            "nuclear_repulsion_energy": self.molecule.nuclear_repulsion_energy(),
            "return_energy": ret_energy,
        }

        for k, val in external_results.items():
            if k == "results":
                k = "nbody"
            qcvars[k] = val

        qcvars['CURRENT ENERGY'] = ret_energy
        if self.driver == 'gradient':
            qcvars['CURRENT GRADIENT'] = ret_ptype
            properties["return_gradient"] = ret_ptype
        elif self.driver == 'hessian':
            qcvars['CURRENT GRADIENT'] = ret_gradient
            qcvars['CURRENT HESSIAN'] = ret_ptype
            properties["return_gradient"] = ret_gradient
            properties["return_hessian"] = ret_ptype

#        build_out(qcvars)
        atprop = build_manybodyproperties(qcvars["nbody"])
        # print("ATPROP")
        # v2: pp.pprint(atprop.model_dump())
        # pp.pprint(atprop.dict())

#        output_data = {
#            "schema_version": 1,
#            "molecule": gamessmol,  # overwrites with outfile Cartesians in case fix_*=F
#            "extras": {**input_model.extras},
#            "native_files": {k: v for k, v in outfiles.items() if v is not None},
#            "properties": atprop,

#####
#        nbody_model = self.get_results(client=client)
#        ret = nbody_model.return_result
#
#        wfn = core.Wavefunction.build(self.molecule, "def2-svp", quiet=True)
#
#        # TODO all besides nbody may be better candidates for extras than qcvars. energy/gradient/hessian_body_dict in particular are too simple for qcvars (e.g., "2")

        # print("QCVARS PRESCREEN")
        # pp.pprint(qcvars)

        for qcv, val in qcvars.items():
            if not isinstance(val, dict):
                qcvars[qcv] = val

        # v2: component_results = self.model_dump()['task_list']  # TODO when/where include the indiv outputs
        #?component_results = self.dict()['task_list']  # TODO when/where include the indiv outputs
#        for k, val in component_results.items():
#            val['molecule'] = val['molecule'].to_schema(dtype=2)

        # print("QCVARS")
        # pp.pprint(qcvars)

        nbody_model = ManyBodyResult(
            **{
                'input_data': self.input_data,
                #'molecule': self.molecule,
                # v2: 'properties': {**atprop.model_dump(), **properties},
                'properties': {**atprop.dict(), **properties},
                'component_properties': component_properties,
                "component_results": component_results,
                'provenance': provenance_stamp(__name__),
                'extras': {
                    'qcvars': qcvars,
                },
                'return_result': ret_ptype,
                "stdout": stdout,
                'success': True,
            })

#        logger.debug('\nNBODY QCSchema:\n' + pp.pformat(nbody_model.model_dump()))

        return nbody_model

ManyBodyComputer

key type required description default
input_data True Input schema containing the relevant settings for performing the many body expansion. This is entirely redundant with the piecemeal assembly of this Computer class and is only stored to be available for error handling and exact reconstruction of ManyBodyResult. None
bsse_type False Requested BSSE treatments. First in list determines which interaction or total energy/gradient/Hessian returned. []
molecule True Target molecule for many body expansion (MBE) or interaction energy (IE) analysis. Fragmentation should already be defined in fragments and related fields. None
driver True The computation driver; i.e., energy, gradient, hessian. In case of ambiguity (e.g., MBE gradient through finite difference energies or MBE energy through composite method), this field refers to the target derivative, not any means specification. None
embedding_charges typing.List[float] False Atom-centered point charges to be used to speed up nbody-level convergence. Charges are placed on molecule fragments whose basis sets are not included in the computation. (An implication is that charges aren't invoked for bsse_type=cp.) Keys: 1-based index of fragment. Values: list of atom charges for that fragment. None
return_total_data False When True, returns the total data (energy/gradient/Hessian) of the system, otherwise returns interaction data. Default is False for energies, True for gradients and Hessians. Note that the calculation of counterpoise corrected total energies implies the calculation of the energies of monomers in the monomer basis, hence specifying return_total_data = True may carry out more computations than return_total_data = False. For gradients and Hessians, return_total_data = False is rarely useful. None
levels False Dictionary of different levels of theory for different levels of expansion. Note that the primary method_string is not used when this keyword is given. supersystem computes all higher order n-body effects up to the number of fragments; this higher-order correction uses the nocp basis, regardless of bsse_type. A method fills in for any lower unlisted nbody levels. Note that if both this and max_nbody are provided, they must be consistent. Examples: SUPERSYSTEM definition suspect* {1: 'ccsd(t)', 2: 'mp2', 'supersystem': 'scf'} * {2: 'ccsd(t)/cc-pvdz', 3: 'mp2'} * Now invalid: {1: 2, 2: 'ccsd(t)/cc-pvdz', 3: 'mp2'} Examples above are processed in the ManyBodyComputer, and once processed, only the values should be used. The keys turn into nbodies_per_mc_level, as notated below. * {1: 'ccsd(t)', 2: 'mp2', 'supersystem': 'scf'} -> nbodies_per_mc_level=[[1], [2], ['supersystem']] * {2: 'ccsd(t)/cc-pvdz', 3: 'mp2'} -> nbodies_per_mc_level=[[1, 2], [3]] None
max_nbody False Maximum number of bodies to include in the many-body treatment. Possible: max_nbody <= nfragments. Default: max_nbody = nfragments. None
supersystem_ie_only False Target the supersystem total/interaction energy (IE) data over the many-body expansion (MBE) analysis, thereby omitting intermediate-body calculations. When False (default), compute each n-body level in the MBE up through max_nbody. When True (only allowed for max_nbody = nfragments ), only compute enough for the overall interaction/total energy: max_nbody-body and 1-body. When True, properties INTERACTION {driver} THROUGH {max_nbody}-BODY will always be available; TOTAL {driver} THROUGH {max_nbody}-BODY will be available depending on return_total_data ; and {max_nbody}-BODY CONTRIBUTION TO {driver} won't be available (except for dimers). This keyword produces no savings for a two-fragment molecule. But for the interaction energy of a three-fragment molecule, for example, 2-body subsystems can be skipped with supersystem_ie_only=True. Do not use with vmfc in bsse_type as it cannot produce savings. False
task_list typing.Any False {}
qcmb_calculator typing.Any False Low-level interface None