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: object

The counterpoise-corrected outcome for one cluster geometry.

energy is the corrected total energy of the cluster (same absolute scale as the cluster job’s raw energy), not an interaction energy. gradient is full-cluster length (E_h/bohr), or None if any job result lacked a gradient. bsse_correction is energy - <raw cluster energy> (negative: BSSE always raises the apparent binding, so the correction lowers the magnitude of binding). interaction_energy/uncorrected_interaction_energy are relative to the separated fragments (CP-corrected and not, respectively) – the conventional binding-energy reference point, distinct from energy. gradient_fallback is True when the translational-invariance guard fired and gradient was replaced by the uncorrected cluster gradient (energy is unaffected either way). net_force is the magnitude (E_h/bohr) of the net force summed over the combined gradient before any fallback – None when there is no gradient. net_force measures how inconsistent (noisy) the correction is, which is a different question from how large it is – gradient_correction_magnitude is 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 with gradient_fallback means 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-cluster job 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-cluster job, 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 memory nacl-curve-crossing-investigation.md for 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_magnitude on the returned CPResult to 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: object

One job’s outcome, as an engine step reports it.

gradient, if given, must be in the same order as the owning JobSpec.atom_indices (i.e. gradient[k] is the force on the atom at spec.atom_indices[k]), in E_h/bohr; None for 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 in specs.

  • 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: object

One 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 jobs generate_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: object

One 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); None for 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 CLUSTER and FRAGMENT_IN_CLUSTER jobs this is every atom in the cluster, in cluster order (ghosts included) – see the architecture doc’s confirmed-for-ORCA indexing property. For FRAGMENT_ALONE jobs it is only that fragment’s atoms.

  • ghost_indices (frozenset[int]) – The subset of atom_indices that 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 JobSpec for a cluster of N fragments.

One cluster job (every atom real), N fragment-in-cluster jobs (fragment i real, every other fragment ghosted, full cluster atom list), and N fragment-alone jobs (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 ValueError if fragments is not a valid partition, or (when given) is inconsistent with cluster_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_charge equals the sum of the fragment charges;

  • if given, cluster_multiplicity is 1 (the only multiplicity a set of closed-shell fragments can combine to, in this phase);

  • if atomic_numbers is given (the full cluster’s, indexed the same way as Fragment.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: object

The counterpoise-corrected outcome for one cluster geometry.

energy is the corrected total energy of the cluster (same absolute scale as the cluster job’s raw energy), not an interaction energy. gradient is full-cluster length (E_h/bohr), or None if any job result lacked a gradient. bsse_correction is energy - <raw cluster energy> (negative: BSSE always raises the apparent binding, so the correction lowers the magnitude of binding). interaction_energy/uncorrected_interaction_energy are relative to the separated fragments (CP-corrected and not, respectively) – the conventional binding-energy reference point, distinct from energy. gradient_fallback is True when the translational-invariance guard fired and gradient was replaced by the uncorrected cluster gradient (energy is unaffected either way). net_force is the magnitude (E_h/bohr) of the net force summed over the combined gradient before any fallback – None when there is no gradient. net_force measures how inconsistent (noisy) the correction is, which is a different question from how large it is – gradient_correction_magnitude is 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 with gradient_fallback means 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: object

One 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 jobs generate_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: object

One job’s outcome, as an engine step reports it.

gradient, if given, must be in the same order as the owning JobSpec.atom_indices (i.e. gradient[k] is the force on the atom at spec.atom_indices[k]), in E_h/bohr; None for 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: object

One 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); None for 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 CLUSTER and FRAGMENT_IN_CLUSTER jobs this is every atom in the cluster, in cluster order (ghosts included) – see the architecture doc’s confirmed-for-ORCA indexing property. For FRAGMENT_ALONE jobs it is only that fragment’s atoms.

  • ghost_indices (frozenset[int]) – The subset of atom_indices that 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 in specs.

  • 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 JobSpec for a cluster of N fragments.

One cluster job (every atom real), N fragment-in-cluster jobs (fragment i real, every other fragment ghosted, full cluster atom list), and N fragment-alone jobs (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 ValueError if fragments is not a valid partition, or (when given) is inconsistent with cluster_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_charge equals the sum of the fragment charges;

  • if given, cluster_multiplicity is 1 (the only multiplicity a set of closed-shell fragments can combine to, in this phase);

  • if atomic_numbers is given (the full cluster’s, indexed the same way as Fragment.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.