seamm_bsse package#
Submodules#
seamm_bsse.combine module#
Combine the results of the 2N + 1 counterpoise jobs into a corrected total energy and gradient.
Padding-only, per the architecture doc: the cluster and
fragment-in-cluster jobs are already full-cluster-length (ghosts keep
their place in the atom list), so only the fragment-alone jobs – genuine
subsets, by construction – need to be padded into the full-cluster-length
gradient before summing.
- class seamm_bsse.combine.CPResult(energy: float, gradient: List[List[float]] | None, bsse_correction: float, interaction_energy: float, uncorrected_interaction_energy: float, gradient_fallback: bool = False, net_force: float | None = None, gradient_correction_magnitude: float | None = None)[source]#
Bases:
objectThe counterpoise-corrected outcome for one cluster geometry.
energyis the corrected total energy of the cluster (same absolute scale as the cluster job’s raw energy), not an interaction energy.gradientis full-cluster length (E_h/bohr), orNoneif any job result lacked a gradient.bsse_correctionisenergy - <raw cluster energy>(negative: BSSE always raises the apparent binding, so the correction lowers the magnitude of binding).interaction_energy/uncorrected_interaction_energyare relative to the separated fragments (CP-corrected and not, respectively) – the conventional binding-energy reference point, distinct fromenergy.gradient_fallbackisTruewhen the translational-invariance guard fired andgradientwas replaced by the uncorrected cluster gradient (energyis unaffected either way).net_forceis the magnitude (E_h/bohr) of the net force summed over the combined gradient before any fallback –Nonewhen there is no gradient.net_forcemeasures how inconsistent (noisy) the correction is, which is a different question from how large it is –gradient_correction_magnitudeis the size (E_h/bohr, a full-cluster Frobenius norm) of the BSSE gradient correction actually applied (gradient - <raw cluster gradient>), always computed when a gradient is available (whether or not the fallback fires). A caller wondering whether the uncorrected-gradient fallback is trustworthy for a given point – rather than just trusting the “large separation” assumption baked into the guard’s tolerance – can check this directly: a small value means there was little physical correction to lose either way; a large value together withgradient_fallbackmeans the fallback is discarding something that actually mattered, and the point is worth excluding/rerunning rather than silently accepted.- bsse_correction: float#
- energy: float#
- gradient: List[List[float]] | None#
- gradient_correction_magnitude: float | None = None#
- gradient_fallback: bool = False#
- interaction_energy: float#
- net_force: float | None = None#
- uncorrected_interaction_energy: float#
- seamm_bsse.combine.DEFAULT_GRADIENT_TOLERANCE = 0.0003#
Net-force tolerance (E_h/bohr) above which a CP-corrected gradient is treated as corrupted – a translational-invariance guard. A converged CP-corrected gradient of an isolated cluster always sums to ~0 net force; ORCA (RIJCOSX) can silently corrupt the far-ghost Pulay force on a
fragment-in-clusterjob while the energy stays fine (no warning, no linear dependence flagged), and this sum is the only reliable detector.Calibrated (2026-08-07) against a real 40-point Na+/Cl- R-scan (job 3867): across the 37 healthy points the combined net force never exceeds 9.6e-5 E_h/bohr; the 3 anomalous points (all traced to exactly this RIJCOSX/ghost-centre noise in a
fragment-in-clusterjob, confirmed by hand-recombining the raw per-job gradients – NOT a wrong SCF branch, which was ruled out by checking Mulliken charges/orbital degeneracy on the same points) sit at 9.5e-4 to 1.3e-3 – a clean >10x gap. This value is the geometric mean of that gap. The prior value (0.02) was ~200x too loose to catch this class of point at all. See the workspace memorynacl-curve-crossing-investigation.mdfor the full investigation (including why a real, distinct, and much smaller symmetry-breaking effect near the ionic/covalent curve crossing does NOT trip this guard – correctly, since the true BSSE correction is already negligible wherever that effect shows up).Where this fires, the physical BSSE correction to the forces is negligible anyway (see
gradient_correction_magnitudeon the returnedCPResultto check this directly for a given point), so falling back to the uncorrected cluster gradient is safe.
- class seamm_bsse.combine.JobResult(energy: float, gradient: Sequence[Sequence[float]] | None = None)[source]#
Bases:
objectOne job’s outcome, as an engine step reports it.
gradient, if given, must be in the same order as the owningJobSpec.atom_indices(i.e.gradient[k]is the force on the atom atspec.atom_indices[k]), in E_h/bohr;Nonefor an energy-only run.- energy: float#
- gradient: Sequence[Sequence[float]] | None = None#
- seamm_bsse.combine.combine(specs: Sequence[JobSpec], results: Dict[str, JobResult], n_atoms: int, gradient_tolerance: float = 0.0003) CPResult[source]#
Assemble the counterpoise-corrected energy/gradient from the 2N + 1 job results.
- Parameters:
specs (Sequence[JobSpec]) – The job specs from
generate_job_specs().results (dict[str, JobResult]) – The outcome of each job, keyed by
JobSpec.label. Must have an entry for every label inspecs.n_atoms (int) – The number of atoms in the full cluster (the length of the returned gradient).
gradient_tolerance (float) – Net-force tolerance (E_h/bohr) for the translational-invariance guard – see
DEFAULT_GRADIENT_TOLERANCE.
seamm_bsse.fragment module#
The Fragment input to a counterpoise correction.
- class seamm_bsse.fragment.Fragment(label: str, atom_indices: Sequence[int], charge: int = 0, multiplicity: int = 1)[source]#
Bases:
objectOne fragment of a cluster for an N-fragment counterpoise correction.
- Parameters:
label (str) – A short, unique identifier for the fragment (e.g.
"A","Na+","water_1"). Used to name the jobsgenerate_job_specs()produces for this fragment.atom_indices (Sequence[int]) – The 0-based indices of this fragment’s atoms in the full cluster’s atom list.
charge (int) – The fragment’s formal charge.
multiplicity (int) – The fragment’s spin multiplicity. Only
1(closed-shell) is currently supported – see the architecture doc’s “out of scope” section. Kept as a field now for forward compatibility.
- atom_indices: Sequence[int]#
- charge: int = 0#
- label: str#
- multiplicity: int = 1#
seamm_bsse.job_specs module#
Generate the 2N + 1 job specs a full-cluster-basis, N-fragment counterpoise
correction needs, from a list of Fragment.
See the architecture doc
(docs/developer_guide/campaigns/2026-08-03/bsse_architecture.rst) for the
physics and the confirmed ORCA ghost-indexing property this shape relies on.
- seamm_bsse.job_specs.CLUSTER = 'cluster'#
The three kinds of job a counterpoise correction runs.
- class seamm_bsse.job_specs.JobSpec(label: str, kind: str, fragment_label: str | None, atom_indices: Tuple[int, ...], ghost_indices: FrozenSet[int], charge: int, multiplicity: int)[source]#
Bases:
objectOne of the 2N + 1 calculations a counterpoise correction needs.
- Parameters:
label (str) – A unique identifier for this job, e.g.
"cluster","A-in-cluster","A-alone".kind (str) – One of
CLUSTER,FRAGMENT_IN_CLUSTER,FRAGMENT_ALONE.fragment_label (str | None) – The fragment this job’s real atoms belong to (the fragment being evaluated);
Nonefor the"cluster"job, which has no single owning fragment.atom_indices (tuple[int, ...]) – The global (full-cluster) atom indices present in this job, in the order a caller’s result (energy/gradient) must be returned in. For
CLUSTERandFRAGMENT_IN_CLUSTERjobs this is every atom in the cluster, in cluster order (ghosts included) – see the architecture doc’s confirmed-for-ORCA indexing property. ForFRAGMENT_ALONEjobs it is only that fragment’s atoms.ghost_indices (frozenset[int]) – The subset of
atom_indicesthat are ghost centres (basis functions only, no nucleus/electrons) in this job.charge (int) – The charge to run this job at.
multiplicity (int) – The spin multiplicity to run this job at.
- atom_indices: Tuple[int, ...]#
- charge: int#
- fragment_label: str | None#
- ghost_indices: FrozenSet[int]#
- kind: str#
- label: str#
- multiplicity: int#
- seamm_bsse.job_specs.generate_job_specs(fragments: Sequence[Fragment]) List[JobSpec][source]#
Return the 2N + 1
JobSpecfor a cluster of N fragments.One
clusterjob (every atom real), Nfragment-in-clusterjobs (fragment i real, every other fragment ghosted, full cluster atom list), and Nfragment-alonejobs (only fragment i’s atoms, its own basis). At N = 2 this is exactly the classic 5-calculation Boys-Bernardi scheme.
seamm_bsse.validate module#
Sanity checks on a fragment list, shared by generate_job_specs() and
callers that want to cross-check fragments against a known cluster
charge/multiplicity (e.g. an engine step checking its fragment table against
the actual system’s configuration.charge).
- seamm_bsse.validate.validate_fragments(fragments: Sequence[Fragment], cluster_charge: int | None = None, cluster_multiplicity: int | None = None, atomic_numbers: Sequence[int] | None = None) None[source]#
Raise
ValueErroriffragmentsis not a valid partition, or (when given) is inconsistent withcluster_charge/cluster_multiplicity.Checks:
at least one fragment;
no atom index is claimed by more than one fragment;
every fragment is closed-shell (
multiplicity == 1) – open-shell fragments are not yet supported (see the architecture doc);if given,
cluster_chargeequals the sum of the fragment charges;if given,
cluster_multiplicityis1(the only multiplicity a set of closed-shell fragments can combine to, in this phase);if
atomic_numbersis given (the full cluster’s, indexed the same way asFragment.atom_indices), every fragment has an even electron count at its assigned charge – a fragment charge that leaves it with an odd number of electrons cannot be closed-shell, whatever the cluster’s total charge sums to. This is the case a per-fragment charge typo (e.g. swapping which fragment an ion’s charge is assigned to) produces: the cluster-charge check above still passes, but the fragment-alone/ fragment-in-cluster sub-job for that fragment cannot converge as a singlet and the underlying QM code errors out deep in that sub-job instead of at flowchart validation time.
Module contents#
Engine-agnostic N-fragment counterpoise (BSSE) job-spec generation and result combination for SEAMM.
Given a cluster’s fragment assignment and per-fragment charge/multiplicity,
generates the 2N + 1 job specs the full-cluster-basis counterpoise correction
needs, and combines the 2N + 1 results into a corrected total energy and
gradient. No quantum chemistry, no engine-specific input/output handling –
that stays in each engine’s own SEAMM step (orca_step first). See
docs/developer_guide/campaigns/2026-08-03/bsse_architecture.rst for the
full design.
- class seamm_bsse.CPResult(energy: float, gradient: List[List[float]] | None, bsse_correction: float, interaction_energy: float, uncorrected_interaction_energy: float, gradient_fallback: bool = False, net_force: float | None = None, gradient_correction_magnitude: float | None = None)[source]#
Bases:
objectThe counterpoise-corrected outcome for one cluster geometry.
energyis the corrected total energy of the cluster (same absolute scale as the cluster job’s raw energy), not an interaction energy.gradientis full-cluster length (E_h/bohr), orNoneif any job result lacked a gradient.bsse_correctionisenergy - <raw cluster energy>(negative: BSSE always raises the apparent binding, so the correction lowers the magnitude of binding).interaction_energy/uncorrected_interaction_energyare relative to the separated fragments (CP-corrected and not, respectively) – the conventional binding-energy reference point, distinct fromenergy.gradient_fallbackisTruewhen the translational-invariance guard fired andgradientwas replaced by the uncorrected cluster gradient (energyis unaffected either way).net_forceis the magnitude (E_h/bohr) of the net force summed over the combined gradient before any fallback –Nonewhen there is no gradient.net_forcemeasures how inconsistent (noisy) the correction is, which is a different question from how large it is –gradient_correction_magnitudeis the size (E_h/bohr, a full-cluster Frobenius norm) of the BSSE gradient correction actually applied (gradient - <raw cluster gradient>), always computed when a gradient is available (whether or not the fallback fires). A caller wondering whether the uncorrected-gradient fallback is trustworthy for a given point – rather than just trusting the “large separation” assumption baked into the guard’s tolerance – can check this directly: a small value means there was little physical correction to lose either way; a large value together withgradient_fallbackmeans the fallback is discarding something that actually mattered, and the point is worth excluding/rerunning rather than silently accepted.- bsse_correction: float#
- energy: float#
- gradient: List[List[float]] | None#
- gradient_correction_magnitude: float | None = None#
- gradient_fallback: bool = False#
- interaction_energy: float#
- net_force: float | None = None#
- uncorrected_interaction_energy: float#
- class seamm_bsse.Fragment(label: str, atom_indices: Sequence[int], charge: int = 0, multiplicity: int = 1)[source]#
Bases:
objectOne fragment of a cluster for an N-fragment counterpoise correction.
- Parameters:
label (str) – A short, unique identifier for the fragment (e.g.
"A","Na+","water_1"). Used to name the jobsgenerate_job_specs()produces for this fragment.atom_indices (Sequence[int]) – The 0-based indices of this fragment’s atoms in the full cluster’s atom list.
charge (int) – The fragment’s formal charge.
multiplicity (int) – The fragment’s spin multiplicity. Only
1(closed-shell) is currently supported – see the architecture doc’s “out of scope” section. Kept as a field now for forward compatibility.
- atom_indices: Sequence[int]#
- charge: int = 0#
- label: str#
- multiplicity: int = 1#
- class seamm_bsse.JobResult(energy: float, gradient: Sequence[Sequence[float]] | None = None)[source]#
Bases:
objectOne job’s outcome, as an engine step reports it.
gradient, if given, must be in the same order as the owningJobSpec.atom_indices(i.e.gradient[k]is the force on the atom atspec.atom_indices[k]), in E_h/bohr;Nonefor an energy-only run.- energy: float#
- gradient: Sequence[Sequence[float]] | None = None#
- class seamm_bsse.JobSpec(label: str, kind: str, fragment_label: str | None, atom_indices: Tuple[int, ...], ghost_indices: FrozenSet[int], charge: int, multiplicity: int)[source]#
Bases:
objectOne of the 2N + 1 calculations a counterpoise correction needs.
- Parameters:
label (str) – A unique identifier for this job, e.g.
"cluster","A-in-cluster","A-alone".kind (str) – One of
CLUSTER,FRAGMENT_IN_CLUSTER,FRAGMENT_ALONE.fragment_label (str | None) – The fragment this job’s real atoms belong to (the fragment being evaluated);
Nonefor the"cluster"job, which has no single owning fragment.atom_indices (tuple[int, ...]) – The global (full-cluster) atom indices present in this job, in the order a caller’s result (energy/gradient) must be returned in. For
CLUSTERandFRAGMENT_IN_CLUSTERjobs this is every atom in the cluster, in cluster order (ghosts included) – see the architecture doc’s confirmed-for-ORCA indexing property. ForFRAGMENT_ALONEjobs it is only that fragment’s atoms.ghost_indices (frozenset[int]) – The subset of
atom_indicesthat are ghost centres (basis functions only, no nucleus/electrons) in this job.charge (int) – The charge to run this job at.
multiplicity (int) – The spin multiplicity to run this job at.
- atom_indices: Tuple[int, ...]#
- charge: int#
- fragment_label: str | None#
- ghost_indices: FrozenSet[int]#
- kind: str#
- label: str#
- multiplicity: int#
- seamm_bsse.combine(specs: Sequence[JobSpec], results: Dict[str, JobResult], n_atoms: int, gradient_tolerance: float = 0.0003) CPResult[source]#
Assemble the counterpoise-corrected energy/gradient from the 2N + 1 job results.
- Parameters:
specs (Sequence[JobSpec]) – The job specs from
generate_job_specs().results (dict[str, JobResult]) – The outcome of each job, keyed by
JobSpec.label. Must have an entry for every label inspecs.n_atoms (int) – The number of atoms in the full cluster (the length of the returned gradient).
gradient_tolerance (float) – Net-force tolerance (E_h/bohr) for the translational-invariance guard – see
DEFAULT_GRADIENT_TOLERANCE.
- seamm_bsse.generate_job_specs(fragments: Sequence[Fragment]) List[JobSpec][source]#
Return the 2N + 1
JobSpecfor a cluster of N fragments.One
clusterjob (every atom real), Nfragment-in-clusterjobs (fragment i real, every other fragment ghosted, full cluster atom list), and Nfragment-alonejobs (only fragment i’s atoms, its own basis). At N = 2 this is exactly the classic 5-calculation Boys-Bernardi scheme.
- seamm_bsse.validate_fragments(fragments: Sequence[Fragment], cluster_charge: int | None = None, cluster_multiplicity: int | None = None, atomic_numbers: Sequence[int] | None = None) None[source]#
Raise
ValueErroriffragmentsis not a valid partition, or (when given) is inconsistent withcluster_charge/cluster_multiplicity.Checks:
at least one fragment;
no atom index is claimed by more than one fragment;
every fragment is closed-shell (
multiplicity == 1) – open-shell fragments are not yet supported (see the architecture doc);if given,
cluster_chargeequals the sum of the fragment charges;if given,
cluster_multiplicityis1(the only multiplicity a set of closed-shell fragments can combine to, in this phase);if
atomic_numbersis given (the full cluster’s, indexed the same way asFragment.atom_indices), every fragment has an even electron count at its assigned charge – a fragment charge that leaves it with an odd number of electrons cannot be closed-shell, whatever the cluster’s total charge sums to. This is the case a per-fragment charge typo (e.g. swapping which fragment an ion’s charge is assigned to) produces: the cluster-charge check above still passes, but the fragment-alone/ fragment-in-cluster sub-job for that fragment cannot converge as a singlet and the underlying QM code errors out deep in that sub-job instead of at flowchart validation time.