MSINDO (semiempirical INDO)¶
See also the tutorial for a worked walkthrough.
MSINDO (Modified Symmetrically-orthogonalised INDO) is a semiempirical SCF-MO method of the INDO family, built on Slater-type orbitals with a symmetric (Löwdin) orthogonalisation folded into the one-electron resonance integrals. It was developed by Ahlswede, Bredow, Geudtner & Jug at the Mulliken Center for Theoretical Chemistry, University of Bonn.
vibe-qc carries an independent re-implementation of MSINDO, it imports no
MSINDO source code. The method is validated out-of-process against a reference
MSINDO build (see examples/regression/msindo/); validated INDO routes and the
closed-shell NDDO scope reproduce reference MSINDO total energies to well
under 1 µHa. MSINDO is production within that documented scope: INDO
H to Xe (Z 1 to 54), NDDO H, Li to F and Na to Cl, closed-shell analytic
gradients. The periodic cyclic-cluster route belongs to the experimental SECCM
line; see the status note in that section and
Methods: theory and status.
Licensing. vibe-qc’s re-implementation ships under MPL-2.0; the bundled MSINDO parameters are redistributed by permission of the Mulliken Center / University of Bonn. See the licence inventory.
What is MSINDO?¶
MSINDO belongs to the INDO (Intermediate Neglect of Differential Overlap) family
of semiempirical methods. Like CNDO and INDO, it invokes the zero differential
overlap (ZDO) approximation, neglecting all two-electron integrals that involve
overlap densities χ_μ(r)χ_ν(r) where χ_μ and χ_ν are centred on different
atoms. All that remains is a one-centre Coulomb/exchange block and a two-centre
monopole γ_AB.
The defining feature that sets MSINDO apart from other INDO implementations is its symmetric (Löwdin) S⁻¹ᐟ² orthogonalization, folded into the one-electron resonance integrals. This means:
The basis is implicitly orthonormal, the working overlap matrix is I (the identity), and the Roothaan-Hall equation is the standard eigenvalue problem
FC = Cε.The orthogonalization correction (
HORTH) modifies each atom-pair’s resonance block, coupling all pairs globally. A small geometry change on one pair therefore shifts the orthogonalization of every other pair, the origin of the “global Löwdin coupling” that makes a naive per-pair FD gradient fail.MSINDO uses Slater-type orbitals (not Gaussians) with s, p, and polarization d functions (up to 9 basis functions per atom:
s, p_x, p_y, p_z, d_z², d_xz, d_yz, d_x²−y², d_xy). All integrals, overlap, Coulomb γ, nuclear attraction, kinetic, are evaluated analytically in the STO basis.
The INDO parametrization covers H-Xe (Z = 1-54), with NDDO parameters for
H, Li-F, Na-Cl. Each element carries valence exponents (MUS, MUP,
MUD), frozen-core pseudopotential exponents (TAU*) and charges (FCP*),
ionisation potentials (IPOT*), and orbital-exponent Slater-Condon scaling
(MUSE, MUPE, MUDE). Closed-shell H-Xe INDO energies are validated against
the MSINDO oracle; open-shell UHF is validated on the documented light-element
and heavy d/p-block fixtures.
Theory¶
MSINDO (Ahlswede, Bernd and Jug, Karl (1999). *Consistent modifications of SINDO1: I. Approximations and parameters.* Journal of Computational Chemistry, **20**(6), 563–571. doi:[10.1002/(SICI)1096-987X(19990430)20:6<563::AID-JCC1>3.0.CO;2-2](https://doi.org/10.1002/(SICI)1096-987X(19990430)20:6<563::AID-JCC1>3.0.CO;2-2), Ahlswede, Bernd and Jug, Karl (1999). *Consistent modifications of SINDO1: II. Applications to first- and second-row elements.* Journal of Computational Chemistry, **20**(6), 572–578. doi:[10.1002/(SICI)1096-987X(19990430)20:6<572::AID-JCC2>3.0.CO;2-1](https://doi.org/10.1002/(SICI)1096-987X(19990430)20:6<572::AID-JCC2>3.0.CO;2-1)) is the 1999 revision of SINDO1. Like every INDO method it solves Roothaan-Hall equations in a minimal valence basis, here of Slater-type orbitals with s, p and (from Al onwards and for the transition metals) d functions, and it departs from the CNDO and INDO textbook schemes in three places: the orthogonalisation correction, the pseudopotential treatment of the core, and a parametrisation fitted for solids and surfaces as well as molecules.
INDO integral approximations¶
In the zero-differential-overlap approximation every two-electron integral that contains a two-centre charge distribution \(\chi_\mu^A\chi_\nu^B\) (\(A \ne B\)) is dropped. INDO keeps, beyond CNDO, all one-centre integrals with their full angular structure, so that
On one centre the integrals are expressed through Slater-Condon parameters
\(F^0, F^2, G^1, \dots\), which MSINDO evaluates analytically from the STO
exponents and scales with the per-element MUSE/MUPE/MUDE factors (the
GMUNU block packs them for up to nine functions per atom); between two
centres only the spherically averaged monopole Coulomb integral
\(\gamma_{AB}\) between valence shells survives. All one-electron integrals,
overlap, kinetic energy and nuclear attraction, are computed analytically
over the STOs rather than parametrised, and rotated from the diatomic frame
into the molecular frame (ROTINT). The closed-shell Fock matrix is
with the INDO integral set inserted, which reduces to the one-centre
Coulomb/exchange rules plus the \(\gamma_{AB}\) terms; the UHF variant builds
Coulomb from the total and exchange from the same-spin density. The
electronic energy is \(E_{\mathrm{el}} = \tfrac12\mathrm{Tr}[\mathbf P(\mathbf H + \mathbf F)]\),
the total adds the repulsion \(\sum_{I<J} Z_I^{\mathrm{core}}Z_J^{\mathrm{core}}/R_{IJ}\)
of the effective core charges, and the binding energy subtracts the
parametrised atomic reference energies ATENG.
The orthogonalisation correction¶
ZDO is exact only in an orthogonal basis, so the Hamiltonian the ZDO integrals belong to is the Loewdin-transformed one, \(\mathbf H^{\lambda} = \mathbf S^{-1/2}\mathbf H\mathbf S^{-1/2}\). MSINDO does not parametrise this away: it expands the transformation in the overlap,
and adds the resulting correction to every atom-pair block of the
resonance integrals (HORTH, assembled from the auxiliary LMUNU
quantities of the local diatomic frame). The correction is repulsive at
short range and is what gives INDO the Pauli wall that keeps atoms from
collapsing. Because the matrix products run over all atoms, a geometry
change on one pair shifts the correction on every other pair; that is the
“global Loewdin coupling” that makes naive per-pair finite differences of
the gradient fail and why the analytic gradient carries the full
derivative. After the correction the working overlap is the identity and
the SCF is the ordinary eigenvalue problem \(\mathbf F\mathbf C = \mathbf C\boldsymbol\varepsilon\).
The remaining resonance is an empirical term (DELTAH) with the per-pair
parameters AL that also correct for the missing penetration.
Pseudopotential and penetration terms¶
From sodium onwards the inner shells are not carried explicitly. Their
effect on the valence electrons enters through the frozen-core
pseudopotential V2CORE: for a valence orbital on atom \(A\) and the core of
atom \(B\), the nuclear attraction \(-Z_B/r\) is replaced by the attraction of
the effective core charge, and a repulsive term built from the squared
Slater overlaps of the valence orbital with each core shell of \(B\),
weighted by the fitted core charges FCP1S, FCP2S, FCP2P, …, is
added to the diagonal of the core Hamiltonian. This is a
Phillips-Kleinman-type projection in minimal form: it keeps the valence
orbitals out of the core region without orthogonalising them to explicit
core functions. A penetration correction (VCORRK/VCORRL) restores the
difference between the point-charge core attraction and the attraction of
a spherical shell charge, using the monopole \(\gamma\) between the valence
shell and the core shells. The element-specific ingredients are the valence
exponents MUS, MUP, MUD, the core exponents TAU* and charges
FCP*, the ionisation potentials IPOT* that fix the diagonal ENEG, and
the Slater-Condon scalings.
The SCF loop in four steps¶
Core Hamiltonian H⁰, per-atom diagonal
ENEG(Z, shell)plus per-pair resonance blocks built from STO overlap, the nuclear attraction pseudopotentialV2CORE, the orthogonalization correctionHORTH, the empirical resonanceDELTAH, and the local→global rotationROTINT.Two-electron GMUNU, one-centre Slater-Condon Coulomb/exchange packed into a 9×9 block per atom; two-centre monopole Coulomb γ matrix.
Fock,
F = H + G(P), where the INDO G-matrix builds Coulomb from the total density and exchange from the same-spin density (UHF) or half the total density (RHF), with the one-centre INDO Coulomb/exchange rules and the EINZI d-shell hybrid block.Energy,
E_elec = ½ Tr[P(H+F)],E_total = E_elec + Σ_{I<J} CZ_I·CZ_J/R_IJ,E_binding = E_total − Σ ATENG(Z).
Excited states, propagators and solvation¶
Everything beyond the ground-state SCF reuses vibe-qc’s reference-agnostic
engines over the INDO integral tensor rather than MSINDO’s own drivers.
CIS diagonalises the singles block
\(\langle\Phi_i^a|\hat H - E_0|\Phi_j^b\rangle = \delta_{ij}\delta_{ab}(\varepsilon_a - \varepsilon_i) + 2(ia|jb) - (ij|ab)\)
for singlets (the exchange-only form for triplets) with the INDO integrals,
so its excitation energies are the rigorous unscaled ones, not the
empirically scaled SCALEDCIS values of the reference program;
excited-state gradients follow a root by the overlap of its amplitude
vector or, for non-degenerate states, analytically through the CPHF
Z-vector. CISD and MP2/UMP2 are the general determinant CI and the
canonical second-order energy over the same MO integrals. OVGF/GF2
solves the Dyson equation with the diagonal second-order self-energy,
\(\varepsilon_p^{\mathrm{qp}} = \varepsilon_p + \Sigma_{pp}(\varepsilon_p^{\mathrm{qp}})\),
iterated to self-consistency and optionally renormalised with the
third-order term; it corrects Koopmans ionisation potentials for relaxation
and correlation. COSMO embeds the solute in a conductor scaled by
\(f(\varepsilon) = (\varepsilon-1)/(\varepsilon+0.5)\); the solute-cavity
coupling is MSINDO-specific, a distributed-multipole coupling through the
two-centre penetration integrals \(\langle\mu|\,1/|\mathbf r-\mathbf s_i|\,|\nu\rangle\),
because a point-charge coupling over-screens the near-surface segments, and
the reaction field is folded into the SCF. Each of these has its own
section below.
The cyclic cluster construction in words¶
For solids MSINDO does not use Bloch functions. The cyclic cluster model takes one supercell, imposes cyclic (Born-von Karman) boundary conditions on it directly in real space, and treats every atom as the centre of its own Wigner-Seitz cell of the supercell lattice: an interaction between atoms \(A\) and \(B\) is counted with the image of \(B\) that lies inside \(A\)’s Wigner-Seitz cell, with fractional weights when an image sits on a cell face. The result is a finite, molecule-like Hamiltonian, one SCF at the Gamma point of the supercell with no k-point sampling, that has the full translational symmetry of the crystal, so that a defect, a surface or a localised state is described without an artificial periodic array of copies. What the truncation loses is the Coulomb tail beyond the Wigner-Seitz cell; for ionic systems it is restored by a classical Madelung embedding, an Ewald-summed potential of the SCF net charges minus the part the cluster already contains, which enters the Fock diagonal. Because every quantity is a real-space sum over a finite cluster, the energy converges with the supercell size towards the dense-k limit rather than with a k-mesh, and the analytic gradient is the derivative at fixed Wigner-Seitz topology. The cyclic cluster model guide gives the equations; the CCM sections below give the API.
NDDO parametrisation¶
MSINDO’s default mode in the reference program is NDDO (Neglect of
Diatomic Differential Overlap) rather than INDO. NDDO retains the two-centre
dipole-monopole and dipole-dipole integrals that INDO discards. It is a
separate parametrization (nddoparam.f), the orbital exponents, IPOTs,
and one-centre factors all differ from the INDO set. vibe-qc’s validated
closed-shell molecular energy and analytic-gradient scope is H, Li-F, and
Na-Cl. Al through Cl (Z = 13-17) use a 3s3p3d basis with polarization d
functions; the shipped energy and analytic derivative include their source
SPDD terms. Open-shell NDDO remains unsupported and fails closed.
Supported elements¶
Range |
Z |
Elements |
Notes |
|---|---|---|---|
Main group (INDO) |
1-20 |
H-Ca |
H, He, Li, Be, B, C, N, O, F, Ne, Na, Mg, Al, Si, P, S, Cl, Ar, K, Ca |
Transition metals (INDO) |
21-30 |
Sc-Zn |
3d valence shell, EINZI d-block |
Post-3d main group (INDO) |
31-36 |
Ga-Kr |
4s4p valence + [Ar]3d frozen core |
4d / 5s block (INDO) |
37-48 |
Rb-Cd |
Rb/Sr, Y-Mo, Tc-Pd trajectory-SCF fixtures, Ag/Cd |
5p main group (INDO) |
49-54 |
In-Xe |
In/Sn plus Sb-Xe trajectory-SCF fixtures |
NDDO, closed-shell molecular |
1,3-9,11-17 |
H, Li-F, Na-Cl |
Separate parametrization; RHF energy and analytic gradient, including SPDD terms for Al-Cl |
Elements beyond Xe are not bundled; the engine raises a clean
NotImplementedError for any Z outside the supported set.
Valence shells per element¶
Element(s) |
Valence |
Basis |
|---|---|---|
H, He |
1s |
s only |
Li, Be |
2s |
s/p (2p polarization on hydrogen-bonded H via |
B-Ne |
2s2p |
s/p |
Na, Mg |
3s |
s/p |
Al-Ar |
3s3p3d |
s/p/d (3d = polarization) |
K, Ca |
4s |
s/p |
Sc-Zn |
3d4s |
s/p/d (3d = valence) |
Ga-Br |
4s4p + [Ar]3d core |
s/p/d (3d frozen core + 4d polarization) |
For Na and heavier elements, the inner shells (1s, then 2s2p, then 3s3p) are
replaced by the V2CORE frozen-core pseudopotential, Slater overlaps of the
valence orbitals with the core Slater functions, weighted by FCP* charges,
added to the core Hamiltonian diagonal.
Running MSINDO¶
Through run_job (recommended)¶
method="msindo" plugs into the normal run_job flow, no basis set is
required (the Slater basis is built in):
from vibeqc import Atom, Molecule, run_job
# H2S (bohr in Molecule; run_job handles Å/bohr automatically)
h2s = Molecule([
Atom(16, [0.0, 0.0, 0.0]),
Atom(1, [1.819, 0.0, 1.751]),
Atom(1, [-1.819, 0.0, 1.751]),
])
result = run_job(h2s, method="msindo")
print(result.energy) # total energy (Hartree)
run_job writes the usual job artefacts (.out, .bibtex, .system,
.xyz, etc.) and emits the MSINDO citations automatically.
Through the direct engine API¶
For programmatic use, e.g. in a script that scans a potential-energy surface,
computes finite-difference gradients, or drives an external optimizer, call
run_msindo directly:
from vibeqc.semiempirical.methods import msindo
r = msindo.run_msindo(
[16, 1, 1],
[[0.0, 0.0, 0.0], [0.9627, 0.0, 0.9264], [-0.9627, 0.0, 0.9264]],
) # coords are Å
print(r.total_energy, r.binding_energy, r.converged, r.n_iter)
The direct API returns a MsindoResult dataclass:
@dataclass
class MsindoResult:
total_energy: float # total energy (Hartree)
electronic_energy: float # electronic part = ½Tr[P(H+F)]
binding_energy: float # total − Σ ATENG
mo_energies: np.ndarray # MO eigenvalues (Hartree)
density: np.ndarray # density matrix P (n_basis × n_basis)
n_iter: int # SCF iterations
converged: bool # did the SCF converge?
NDDO mode¶
Pass nddo=True to run_msindo for the NDDO (default MSINDO) mode:
r_indo = msindo.run_msindo([9, 1], [[0, 0, 0], [0, 0, 0.917]])
r_nddo = msindo.run_msindo([9, 1], [[0, 0, 0], [0, 0, 0.917]], nddo=True)
print(r_indo.total_energy) # -24.3089965036 Ha (INDO)
print(r_nddo.total_energy) # -23.0436304974 Ha (NDDO)
Closed-shell (RHF) only; H, Li-F, Na-Cl. Open-shell or out-of-range elements
raise NotImplementedError.
Open-shell (UHF)¶
Pass a multiplicity (2S+1) > 1, or a charge that makes the valence-electron
count odd, and the engine runs spin-unrestricted MSINDO (two spin densities, a
Coulomb term from the total density and exchange from the same-spin density;
fockop.f). Validated against reference MSINDO UHF to < 3 × 10⁻⁸ Ha on OH•,
NO•, the O₂ triplet, CH₃• and NH₂•.
from vibeqc import Atom, Molecule, run_job
# OH radical (doublet)
oh = Molecule([Atom(8, [0, 0, 0]), Atom(1, [0, 0, 1.833])], multiplicity=2)
print(run_job(oh, method="msindo").energy) # -16.3330865643 Ha
# O2 triplet ground state
o2 = Molecule([Atom(8, [0, 0, 0]), Atom(8, [0, 0, 2.283])], multiplicity=3)
print(run_job(o2, method="msindo").energy) # -31.5123087290 Ha
# Charged system (e.g. H2O+ cation)
h2o_plus = Molecule([Atom(8, [0, 0, 0]),
Atom(1, [0, 1.43, 1.11]),
Atom(1, [0, -1.43, 1.11])],
charge=+1, multiplicity=2)
print(run_job(h2o_plus, method="msindo").energy)
The UHF engine includes the s/p/d one-centre Fock terms. Its production claim is fixture-based: the light s/p radical set and the named heavy trajectory-SCF fixtures are validated against the oracle, while near-degenerate heavy cases remain basin-sensitive.
UHF reduces to RHF for closed-shell¶
When multiplicity=1 and nelec is even, the engine runs the closed-shell
RHF path, the UHF path is only invoked for genuine open-shell systems. Setting
multiplicity=1 on a system with unpaired electrons triggers the UHF path
automatically (the odd electron count selects it).
Periodic: Cyclic Cluster Model (CCM)¶
MSINDO SECCM energy and gradient routes are experimental, including the
seccm, se-ccm, msindo-seccm, msindo-ccm, and ccm aliases. The route
plan, status registry, .out, .system, and QVF run provenance carry this
designation. Native execution, agreement for a reference case, or SCF
convergence does not establish production maturity or electronic stability.
Inspect the separate per-point stability audit described below; the default
state policy remains stability="reference". This designation does not change
the maturity of molecular MSINDO.
MSINDO treats periodic solids and surfaces with the Cyclic Cluster Model
(Bredow, Geudtner & Jug, J. Comput. Chem. 22, 89 (2001); Peintinger &
Bredow, J. Comput. Chem. 35, 839 (2014)): a finite cyclic cluster (a
supercell) in which every atom carries a Wigner-Seitz cell describing its
periodic environment, with the long-range electrostatics added by a classical
Ewald (Madelung) embedding. vibe-qc’s implementation lives in
vibeqc.semiempirical.methods.msindo_ccm and is validated out-of-process against
reference MSINDO (every number below reproduces the oracle to < 1 µHa).
Status: experimental
The cyclic cluster model belongs to the SECCM line, which is experimental
overall and is not part of the release gate. The MSINDO route is the most
mature member of that line: its closed-shell energies are validated against
the reference program within the scope below, and run_job(method="ccm")
routes the CCM citations into the .out, .bibtex and .system manifest.
Open shells, optimize=True through run_job, and elements beyond Xe fail
closed. The label vocabulary is defined in
Methods: theory and status.
Quick-start: bulk MgO¶
from vibeqc.semiempirical.methods.msindo_ccm import run_ccm
# Bulk MgO - rocksalt Mg4O4 cell (Angstrom), 3 lattice vectors (CCM3D)
a = 4.21
fcc = [(0, 0, 0), (0.5, 0.5, 0), (0.5, 0, 0.5), (0, 0.5, 0.5)]
Z = [12] * 4 + [8] * 4
coords = ([[f * a for f in p] for p in fcc]
+ [[(p[0] + 0.5) * a, p[1] * a, p[2] * a] for p in fcc])
cell = [[a, 0, 0], [0, a, 0], [0, 0, a]]
print(run_ccm(Z, coords, cell, madelung=True).total_energy) # ≈ -66.20383741 Ha
run_ccm(atomic_numbers, coords_angstrom, translations_angstrom, *, madelung=)
takes the cluster geometry and the 1-3 supercell lattice vectors, one for a
polymer (CCM1D), two for a surface (CCM2D), three for a bulk (CCM3D).
madelung=True switches on the Ewald embedding (a 1-D finite sum, the 2-D
surface Ewald, or the 3-D bulk Ewald, chosen by the number of vectors); leave it
off (NOEWALD) for non-polar/covalent systems.
The 2-D and 3-D direct and reciprocal sums are truncated by physical Ewald
decay (exp(-30)), with coefficient bounds derived from the direct and
reciprocal lattice metrics. Both sums range over sets of physical vectors,
{K != 0 : |K| <= K_cut} and {L : |v + L| <= r_cut}, never over a fixed box
of integer coefficients. Equivalent unimodular choices of the translation vectors
therefore give the same Madelung energy and analytic gradient, even when a
short lattice vector needs large near-cancelling coefficients in one
representation. A pathologically unreduced representation that would require
an unsafe candidate allocation fails closed and asks for a reduced lattice
basis.
The 2-D surface kernel is the Parry Ewald: eq. (7) of Parry, Surf. Sci.
49, 433 (1975) (erratum Surf. Sci. 54, 195 (1976)), with the K = 0
term in the per-pair form of de Leeuw & Perram, Mol. Phys. 37, 1313
(1979), eqs. (8) and (12). Both halves of that split converge absolutely and
exponentially, so truncating each at exp(-30) is well defined. The
conditional convergence of a bare Coulomb lattice sum, where the answer depends
on the shape of the summation region, is removed by the Ewald split itself and
is not a property of eq. (7).
Through run_job¶
The same calculation runs through the standard run_job entry point with
method="ccm", passing the lattice vectors in a CCMOptions. This is the
recommended path for a one-shot energy: it writes the usual job artefacts
(.out, .system, …) and routes the periodic-CCM citations (Peintinger &
Bredow 2014; Bredow, Geudtner & Jug 2001) into the .out / .bibtex /
.references and the .system manifest’s [citations] provenance block.
from vibeqc import Atom, Molecule, run_job
from vibeqc.molecule import ANGSTROM_TO_BOHR as A2B
from vibeqc.semiempirical.methods.msindo_ccm import CCMOptions
a = 4.21
fcc = [(0, 0, 0), (0.5, 0.5, 0), (0.5, 0, 0.5), (0, 0.5, 0.5)]
mg = [Atom(12, [f * a * A2B for f in p]) for p in fcc]
ox = [Atom(8, [(p[0] + 0.5) * a * A2B, p[1] * a * A2B, p[2] * a * A2B])
for p in fcc]
mgo = Molecule(mg + ox) # Atom coords are bohr
cell = [[a, 0, 0], [0, a, 0], [0, 0, a]] # CCMOptions lattice is Angstrom
res = run_job(mgo, method="ccm",
ccm_options=CCMOptions(translations=cell, madelung=True))
print(res.energy) # ≈ -66.20383741 Ha
Through run_job, CCM is closed-shell (RHF) single-point only. Each
boundary is a clean error, never a silent gap: an open-shell
cell (multiplicity != 1) or optimize=True raises NotImplementedError
(use ccm_optimize, below, for relaxation), a call without ccm_options
raises ValueError, and an element beyond the engine’s scope (H-Xe) raises
NotImplementedError naming the supported set.
The public reaction-path driver is a separate validated workflow:
run_neb(..., method="seccm", ccm_options=...) uses the analytic cyclic
gradient for each image. It accepts molecular endpoints carrying an explicit
cyclic cell, or PeriodicSystem endpoints when the CCMOptions.translations
match the active lattice vectors exactly. The latter can be written directly
as a periodic reaction-path QVF. The run_neb(method="seccm") driver remains
closed-shell MSINDO only. DFTB0 also has a narrowly gated 1-D H/C
reaction-path route through run_neb(method="dftb0", seccm_topology=...).
The other semiempirical Hamiltonians expose experimental direct SECCM
adapters through run_scc_dftb_seccm, run_pm6_seccm, run_omx_seccm,
and run_gfn2_seccm; see Semiempirical Methods.
Valid cyclic clusters¶
The cluster must be a valid cyclic cluster, a supercell with no atom lying
on a Wigner-Seitz cell face, so each other atom appears exactly once in every
atom’s WS cell (round(Σ WS weight) == NATOMS − 1). An invalid cluster raises
ValueError; an odd-N evenly-spaced cell is the most robust shape. Madelung
runs additionally require an electroneutral cell.
The cluster shape must also satisfy stoichiometric constraints for the Ewald embedding, the total charge of the cell must be zero, and the atoms should carry chemically sensible charges (the Madelung potential is built from the SCF net charges).
Surfaces, adsorption and relaxation¶
A 2-D slab (two lattice vectors) gives a surface; adding adsorbate atoms to the
cell models a periodic adlayer. ccm_gradient_fd returns the nuclear gradient
(Ha/bohr) and ccm_optimize relaxes selected atoms (e.g. an adsorbate on a
frozen slab):
from vibeqc.semiempirical.methods.msindo_ccm import ccm_optimize
# relax the adsorbate atoms, holding the slab (indices 0..7) fixed
relaxed_coords, result = ccm_optimize(
Z, coords, cell, madelung=True, frozen=list(range(8)))
The adsorption energy is E[slab+adsorbate] − E[slab] − E[adsorbate(gas)]. For
H₂O on MgO(100) this is −6.7 kcal/mol at fixed geometry and −8.2 kcal/mol after
relaxation. A full worked example, bulk → surface → fixed/relaxed adsorption,
is in examples/semiempirical/11_msindo_ccm.py.
CCM runs both through
run_job(method="ccm", …)(above, single-point energies with the full citation/manifest surface) and through the lower-levelmsindo_ccmPython API (gradients, relaxation, energy decomposition). Reaction paths run throughrun_neb(method="seccm", ccm_options=...)and use the analytic fixed-topology gradient. Adsorption examples target the ionic oxides MgO and Al₂O₃, INDO/CCM is not parametrized for metallic bonding, so metal surfaces are out of method scope.
SECCM stability audit¶
Every converged closed-shell CCM SCF is followed by a restricted orbital-stability audit (issue #249). A vanishing SCF residual certifies a stationary point, not a minimum. The Hcore guess can start DIIS from an ambiguous projector, and even a well-resolved start can converge onto a saddle: a C3 chain bent by 3e-3 bohr converges 16.6 mHa above the continuous lower branch. The audit finds the lowest orbital-rotation curvature; a negative one marks a saddle.
What happens next is set by the stability policy:
|
Behaviour |
|---|---|
|
Keeps the reference program’s Hcore/DIIS state at a resolved initial frontier. A saddle is repaired where that trajectory is ill-defined, an unresolved Hcore frontier (gap at most |
|
Attempts descent from every detected saddle to a stable minimum. Use it for scans, forces and equations of state, while checking state continuity; local stability does not prove global uniqueness. |
For a frontier with one occupied orbital or one hole and at most eight
orbitals, both implementations first minimize the initial projector’s energy
within that subspace. The "reference" policy enables this at an unresolved
Hcore gap (at most 1e-6 Ha). The explicit "lowest" policy also considers
the highest occupied and lowest virtual whole Hcore blocks at a resolved
gap. Other occupations retain the established Hcore path. This bounded
search does not prove global state uniqueness.
After minimizing the projector, the solver checks curvature toward the next
whole adjacent Hcore block. Negative curvature below -1e-8 triggers
expansion if the full block fits the eight-orbital limit. The solver
continues from its current projector in the enlarged space. It then recovers
the seed’s occupied orbitals and uses the existing SOSCF direction with an
energy-decreasing line search for the full SCF. Continuing from the actual
seed avoids the DIIS basin change that selected a higher stable C19 state
under a 0.004 Angstrom local bond distortion.
Every seed Fock evaluation and every orbital line-search trial counts against
max_iter. Only the remaining budget is available for stability restarts.
On exhaustion the native kernel and Python solver retain their last
accepted physical density and energy with converged=False; the public
native route raises a convergence error. Results expose
frontier_seed_evaluations, frontier_seed_completed,
frontier_seed_orbitals, frontier_seed_expansions, and
orbital_optimization_used; output records the executed work. Explicit
starting densities bypass seed construction and retain their DIIS
continuation for tracking a chosen state. An unresolved primary frontier
outside the rank-one seed scope retains the earlier SOSCF fallback after
29 DIIS updates.
The C19 permutation and local-distortion checks compare fresh solutions
with density continuation. For bent C3, the resolved-gap "lowest" seed
selects a continuous lower branch. The old pinned DIIS energies remain
separate controls: rotating the lower-state atomic p orbitals and following
that density recovers the higher branch. Local stability alone therefore
does not establish that the lowest state was selected. These state-selection
repairs still require independent numerical acceptance before issue closure.
A repair scans rotations along the unstable mode for both signs, re-converges the lower-energy seed first with the whole remaining budget and the other sign only if the first does not descend, then audits the result again. A mode whose rotations cannot lower the energy by 1 µHa is accepted as stable within the SCF’s resolution, and its eigenvalue is still reported. The density used for analytic and finite-difference gradients follows the same selection. The Hamiltonian and integer occupations are unchanged.
Small bare clusters deserve a look at their charges. For bare diamond 2x2x2
the neutral state is a saddle of the WS-truncated bare Coulomb model, and
"lowest" descends to its minimum, which alternates +/-2.95 e on the two
sublattices 2.75 Ha lower. The Madelung-embedded cluster and the 3x3x3
clusters are neutral stable minima under both policies. run_job records
the largest net atomic charge (seccm_max_abs_net_charge), and
msindo_ccm.net_atomic_charges computes them from any CCM density.
Forces are defined per SCF state. On a nearly linear chain the lowest state can break the chain plane’s mirror symmetry, with its pi pattern rotated about the axis, and it then has a mirror-image partner of equal energy. The lowest-energy surface then has a cusp at the planar geometry: an out-of-plane displacement lowers the energy linearly in its magnitude either way (a C3 chain bent by 3e-3 bohr, issue #249). The analytic gradient is the exact derivative of the state it converged to. A finite difference of independent SCFs across such a cusp is not the derivative of either branch.
max_iter bounds the cumulative SCF iterations across these restarts and
conv_tol sets the SCF threshold. Both are CCMOptions fields for run_job
and run_seccm, with defaults 200 and 1e-9, alongside stability; the
analytic-gradient callback re-converges with gradient_conv_tol, default
1e-10. Under the reference policy at a resolved Hcore gap, an unconverged
stability audit returns an unverified result and run_job warns. When a corrective
restart is required, an unresolved analysis, exhausted restart budget or
failed repair raises a diagnostic error. The result records
stability_checked, stability_analysis_converged, stability_eigenvalue
(the energy Hessian eigenvalue), n_stability_restarts and
internal_instability. run_job prints the verdict, the executed SCF
controls and policy, and the electrostatics kernel; the 1-D MSINDO embedding
is a truncated +/-2-shell bare sum, not an Ewald sum. This is a local minimum
check within restricted HF, not a global-minimum guarantee or a test against
unrestricted spin solutions. See Lehtola et al. (2020), section
10 and issue #249.
CCM gradient details¶
ccm_gradient_fd computes the nuclear gradient by central finite differences
(Ha/bohr), holding the Wigner-Seitz topology fixed while displacing atoms.
This matches MSINDO’s fixed-weight analytic gradient. The WS is built once
before the gradient loop; within each displacement step, the image positions
follow the displaced central atoms but the origin labels and weights stay
constant. This avoids the ~1 × 10⁻² Ha/bohr discontinuity that a naive
rebuild-the-WS-per-displacement FD would produce at WS face boundaries.
ccm_gradient_analytic
(vibeqc.semiempirical.methods.msindo_ccm_gradient_analytic) returns the
analytic CCM nuclear gradient, the exact derivative of the CCM total
energy, including the analytic 1-D (finite ±T,±2T lattice sum), 2-D
(Parry-Heyes slab Ewald) and 3-D (Ewald bulk) Madelung gradient. It reproduces
the FD gradient to < 1 × 10⁻⁶ Ha/bohr and the MSINDO oracle to < 2 × 10⁻⁵ on
mirror-symmetric Wigner-Seitz cells. (On asymmetric ionic Madelung cells the
oracle’s own central-atom-only assembly deviates from the FD of its own energy
by up to ~2 × 10⁻² Ha/bohr; the vibe-qc gradient stays exact.) Closed-shell
(RHF) only. For H-Kr, both ccm_gradient_analytic and ccm_gradient_fd use
the C++ bindings and match the Python reference to ~10⁻¹⁰ Ha/bohr. Rb-Xe
uses the deterministic Python reference, as does an installation without the
native binding. Once a native calculation is selected, an exception or
nonconverged result propagates rather than falling back to Python.
Gradients, geometry optimization, and transition states¶
Molecular gradients¶
The finite-difference gradient msindo_gradient_fd remains available as a
diagnostic reference:
from vibeqc.semiempirical.methods.msindo import msindo_gradient_fd
Z = [8, 1, 1]
xyz = [[0, 0, 0], [0, 0.757, -0.467], [0, -0.757, -0.467]] # Å
g = msindo_gradient_fd(Z, xyz) # Ha/bohr, shape (n_atoms, 3)
The production closed-shell analytic gradient msindo_gradient_analytic uses
the C++ m_indo.gradient_analytic kernel for INDO and the complete NDDO
multipole contraction, including the Al-Cl SPDD terms. Both differentiate the
energy actually returned by their route and match a central finite-difference
step ladder to better than 1e-6 Ha/bohr. run_job(..., method="msindo", nddo=True) exposes the NDDO gradient lazily for H, Li-F, and Na-Cl. Open-shell
NDDO gradients fail closed.
Geometry optimization¶
msindo_optimize wraps scipy’s L-BFGS-B:
from vibeqc.semiempirical.methods.msindo import msindo_optimize
result = msindo_optimize(Z, xyz_start, fmax=4.5e-4, max_iter=200)
print(result.energy, result.coords_angstrom, result.n_iter)
Returns MsindoOptimizeResult with energy, coords_angstrom, n_iter,
fmax, converged, and history (list of (energy, grad_max, coords) per
step).
Nudged Elastic Band (NEB)¶
MSINDO is the first semiempirical backend for vibe-qc’s NEB driver:
from vibeqc import run_neb
# NH3 umbrella inversion: reactant + product = pyramidal NH3 + mirror image
result = run_neb(reactant_mol, product_mol, method="msindo",
n_images=7, climbing_image=True)
ts = result.transition_state_index
barrier_kcal = (result.energies[ts] - result.energies[0]) * 627.509474
print(f"{barrier_kcal:.2f}") # ≈ 8 kcal/mol
Each image cost is 6N SCF calls per outer iteration (FD gradient), so NEB
with MSINDO is practical for up to ~10-15 atoms. Band evaluation is
parallelised across images. The TS at the climbing image is a genuine
first-order saddle (validated by FD Hessian: exactly one imaginary mode).
Full example: examples/semiempirical/22_msindo_neb.py.
Excited states (CIS)¶
MSINDO excited states use vibe-qc’s general CIS / Tamm-Dancoff kernel
(vibeqc.excited) over the INDO integral set, the same reference-agnostic code
that HF and DFT use, fed the INDO ERIProvider blocks instead of libint ones.
msindo_cis returns vertical singlet or triplet excitation energies and the CIS
amplitudes:
from vibeqc.semiempirical.methods.msindo import msindo_cis
Z = [8, 1, 1]
xyz = [[0, 0, 0.117], [0, 0.757, -0.467], [0, -0.757, -0.467]] # Å
s = msindo_cis(Z, xyz, spin="singlet", n_states=4)
print(s.excitation_energies_ev[0]) # S1 (eV)
Rigorous vs. scaled CIS. This is the rigorous INDO CIS. Reference MSINDO additionally applies empirical CIS scaling knobs (
SCALEDCIS,UJCORR/CISCORR) for spectroscopic fitting, so its excitation energies differ from the unscaled values here. vibe-qc ships the rigorous variant by design.
Excited-state gradients¶
A CIS excited-state total energy is E(R) = E_SCF(R) + ω_state(R), so its
nuclear gradient is obtained by central-differencing that total energy.
msindo_cis_gradient_fd does this with state tracking, at each displaced
geometry it follows the CIS root whose amplitude vector overlaps the reference
state’s, so the gradient stays on one state even where roots reorder
(vibeqc.excited_gradient):
from vibeqc.semiempirical.methods.msindo import msindo_cis_gradient_fd
# Gradient (Ha/bohr) of the first singlet excited state (S1).
g = msindo_cis_gradient_fd(Z, xyz, state=1, spin="singlet")
# state=0 is the ground state - identical to msindo_gradient_fd.
The state index is spectroscopic: 0 = ground state, k ≥ 1 = the k-th CIS
root. The same machinery is reference-agnostic,
vibeqc.excited_gradient.make_hf_cis_energy_fn gives the identical gradients for
a Hartree-Fock reference.
Analytic CIS gradient (CPHF / Z-vector)¶
vibeqc.semiempirical.methods.msindo_cis.cis_gradient computes the analytic
excited-state nuclear gradient via the CPHF Z-vector relaxed density and the CIS
two-particle density, contracting the per-pair integral derivatives, the same
engine as the ground-state analytic gradient, plus the orbital-relaxation
response (A·Z = −L):
from vibeqc.semiempirical.methods.msindo_cis import cis_gradient, run_cis
cis = run_cis(Z, xyz, spin="singlet", n_states=4)
g = cis_gradient(Z, xyz, cis, state=0, spin="singlet") # S1 gradient (Ha/bohr)
It reproduces cis_gradient_fd to ~1e-6 Ha/bohr for both singlet and triplet
non-degenerate states (closed-shell only; open-shell raises). At or near a
state degeneracy the per-state CIS gradient is mathematically ill-defined, so
msindo_cis_gradient_fd remains the documented, robust fallback, and is
what the state-tracking / conical-intersection paths above use, since they must
operate where roots cross. Reference for the analytic CIS gradient: Foresman,
Head-Gordon, Pople & Frisch, J. Phys. Chem. 96, 135 (1992).
Conical intersections (MECI)¶
A minimum-energy conical intersection (MECI) is the lowest-energy geometry where
two states are degenerate. msindo_meci optimizes one with the method-agnostic
penalty-function optimizer in vibeqc.conical, fed the INDO finite-difference
CIS gradients:
from vibeqc.semiempirical.methods.msindo import msindo_meci
# Optimize the S1/S0 minimum-energy crossing point.
res = msindo_meci(Z, xyz, lower_state=0, upper_state=1, spin="singlet")
print(res.gap_ev, res.converged) # state gap (eV) → ~0 at the MECI
print(res.coords_angstrom) # the MECI geometry
The optimizer uses the Levine-Coe-Martínez penalty
F_σ = ½(E_I+E_J) + σ·ΔE²/(ΔE+α), escalating σ until the gap closes. It needs
only the two state energies and their gradients, no nonadiabatic derivative
coupling vector, which is exactly what the finite-difference CIS gradients
supply. The upper/lower roles are assigned by energy at each step, so the
objective stays continuous through a state crossing, and a trial step into a
non-convergent region is turned into a smooth barrier that retreats rather than
crashing the run.
This validates the optimizer mechanics on INDO states, not reference-MSINDO CIS energies (which are empirically scaled, see the note above). The optimizer itself is reference-agnostic: any two-state
(E, ∇E)callback (vibeqc.conical.make_cis_meci_fnfrom an HF or MSINDO CIS energy function) drivesvibeqc.conical.optimize_conical_intersection.
A runnable end-to-end demonstration (excited-state gradient + S1/S0 MECI search
on H₂O) is examples/semiempirical/26_msindo_conical.py.
Configuration interaction (CISD)¶
msindo_cisd runs variational CISD, the Hamiltonian diagonalised in the
space of all single and double substitutions out of the closed-shell INDO
reference. It does this by handing the INDO MO integrals to vibe-qc’s
reference-agnostic CI solver (vibeqc.solvers.cisd): the converged INDO
reference’s one-electron core Hamiltonian and the _indo_ao_eri two-electron
tensor are transformed to the MO basis, then the same validated Slater-Condon
engine that powers vibe-qc’s FCI/CASCI builds and diagonalises the CISD matrix.
It is not a port of MSINDO’s own selecting determinant-CI driver
(rhfcisd.f), there are no empirical CIS scalings and no determinant selection.
from vibeqc.semiempirical.methods.msindo import msindo_cisd
r = msindo_cisd([8, 1, 1],
[[0, 0, 0], [0, 0.757, 0.587], [0, -0.757, 0.587]]) # H2O, Å
print(r.e_total) # CISD total energy (Ha)
print(r.e_corr) # correlation energy (E_CISD − E_SCF)
print(r.reference_weight) # squared CI weight of the reference determinant
for desc, coeff, weight in r.dominant_configurations(4):
print(f"{desc:24s} c={coeff:+.4f} w={weight:.4f}")
frozen_core=k freezes the lowest k MOs out of the correlation; nroots=n
reports the lowest n CI roots (r.ci.e_totals); output="stem" writes the
.bibtex / .references citing MSINDO + the variational-CISD method.
Because the singles-plus-doubles space is exact for two correlated electrons,
MSINDO CISD on a two-electron system reproduces the INDO FCI to machine
precision; in general E_FCI ≤ E_CISD ≤ E_SCF, and the singles-only subspace
(vibeqc.solvers.cisd(..., max_excitation=1)) reproduces the msindo_cis
singlet+triplet excitation structure. These are the validation anchors (vs
vibe-qc’s own FCI/CIS), since reference-MSINDO’s CISD uses empirical scalings and
a selecting driver and is therefore not a parity target.
The determinant-space size grows as
(n_occ·n_vir)², so CISD is intended for small molecules;max_det(default 20000) guards against an oversized space.
Post-SCF correlation¶
MP2 (closed-shell)¶
msindo_mp2 runs canonical MP2 over the converged INDO (or NDDO) MOs:
from vibeqc.semiempirical.methods.msindo import msindo_mp2
r = msindo_mp2([8, 1, 1],
[[0, 0, 0], [0, 0.757, 0.587], [0, -0.757, 0.587]]) # H2O
print(r.e_mp2, r.e_corr, r.e_total) # MP2 correction (Ha), correlation, total
# SCS-MP2 and SOS-MP2 scaling
r_scs = msindo_mp2(..., variant="scs")
r_sos = msindo_mp2(..., variant="sos")
The MP2 kernel uses the general vibeqc.correlation.mp2_energy with the INDO
ERI provider. NDDO-MP2 (msindo_mp2(nddo=True)) is also supported and
validated sub-µHa vs the oracle, the NDDO multipole integrals enter only
through the converged MOs, not the correlation integral tensor.
Through run_job: method="msindo", correlation="mp2" (or "scs-mp2",
"sos-mp2"). The result carries a .mp2 attribute.
UMP2 (open-shell)¶
from vibeqc.semiempirical.methods.msindo import msindo_ump2
# O2 triplet
r = msindo_ump2([8, 8], [[0, 0, 0], [0, 0, 2.283]], multiplicity=3)
print(r.e_corr, r.e_total, r.e_corr_aa, r.e_corr_bb, r.e_corr_ab)
The αα channel is the correct 1.0-weighted version, reference MSINDO’s
mp2uhf.f line 62 carries a spurious ¼ (the ββ channel at line 77 uses the
correct factor 1.0, and the routine’s own formula header confirms 1.0), so
the vibe-qc UMP2 ships the corrected formulation.
Ionization potentials and electron affinities (GF2 / OVGF)¶
msindo_ovgf computes quasiparticle ionization potentials and electron
affinities from the one-particle Green’s function, the diagonal second-order
self-energy (GF2), solved self-consistently through the Dyson equation, over the
rigorous INDO integral set (the same tensor that drives MSINDO-MP2). It
corrects the Koopmans (orbital-energy) estimate for relaxation and correlation.
import numpy as np
from vibeqc.semiempirical.methods.msindo import msindo_ovgf
xyz = [[0, 0, 0], [0, 0.757, 0.587], [0, -0.757, 0.587]] # H2O (Å)
r = msindo_ovgf([8, 1, 1], xyz)
print(r.ip_homo_ev) # HOMO ionization potential, eV
print(r.ea_lumo_ev) # LUMO electron affinity, eV (negative: no bound anion)
Electron affinities are surfaced as
EA = −ε_qp(LUMO). In a minimal valence basis (and any non-augmented basis) the diagonal-GF2 EA is qualitative only, the virtual orbitals are not variational and there are no diffuse functions for a bound anion, so a positive value does not establish a stable anion. Use an augmented Gaussian basis (HF/DFT reference) for quantitative EAs.Open shell. Pass a
multiplicity > 1, or an odd valence-electron count, andmsindo_ovgfruns the spin-unrestricted (UHF) reference and returns aMsindoUOVGFResultwith the spin-resolved self-energy (the electron-propagator analogue of UMP2): per-spin frontier orbitals, the first vertical IP (first_ip_ev), and the EA (ea_ev). s/p elements (H-F) only.d = 1.079 ch3 = [[0, 0, 0]] + [[d*np.cos(a), d*np.sin(a), 0] for a in (0.0, 2*np.pi/3, 4*np.pi/3)] r = msindo_ovgf([6, 1, 1, 1], ch3) # CH3• doublet (inferred) print(r.first_ip_ev) # ~10.6 eV (SOMO ionization)
Renormalized GF2. Bare GF2 systematically over-corrects the IP. Pass
renormalize=Trueto dress the second-order self-energy with the full diagonal third-order self-energy and a geometric screening, which trims the overcorrection back toward experiment (H₂O HOMO IP: bare 13.13 → renorm 12.87 eV). Small systems only (the third-order contractions scale steeply).msindo_ovgf([8, 1, 1], xyz, renormalize=True).ip_homo_ev # 12.87 eV
What vibe-qc ships, precisely. This is the rigorous ZDO self-energy and a transparently-documented renormalized second-order Green’s function. It is not reference MSINDO’s
ovgfrhf_neu(a cruder factorized-integral approximation), nor the literal von Niessen-Schirmer-Cederbaum OVGF “A/B/C” (whose exact screening partition is in the 1984 review). Cederbaum 1975 / von Niessen 1984 are cited as the lineage. The general engine lives invibeqc.propagatorand serves HF/DFT (method="ovgf") the same way.
Implicit solvation (COSMO)¶
MSINDO solvation reuses vibe-qc’s reference-pluggable CPCM/COSMO engine
(vibeqc.solvation): the cavity, the segment interaction matrix, the dielectric
screening, the apparent-surface-charge solve, and the polarisation energy are all
shared with the HF/DFT solvation path. Only the solute↔cavity coupling is
MSINDO-specific, and it is a distributed-multipole coupling, two-centre Slater
penetration integrals ⟨μ(A)|1/|r−sᵢ||ν(A)⟩ (the V2INT integrals), with the
solute charge vector built from the core charges, the diagonal AO populations
P_μμ, and the same-atom 2·P_μν p-p / d-d pairs. (A point-charge “monopole”
shortcut over-screens by ~17× because a point ESP is far too singular at the
near-surface cavity points; the distributed multipole is required.)
The screening uses the Klamt-Schüürmann conductor factor
f(ε) = (ε−1)/(ε+0.5), and the reaction field is folded into the SCF directly,
so the density and the surface charge converge together in a single SCF (usually
one extra cycle over the gas phase):
from vibeqc.semiempirical.methods.msindo_cosmo import msindo_cosmo
Z = [8, 1, 1]
xyz = [(0.0, 0.0, 0.0), (0.0, 0.757, 0.587), (0.0, -0.757, 0.587)] # Å
r = msindo_cosmo(Z, xyz, epsilon=78.39) # water
print(r.total_energy) # in-solvent total (Ha)
print(r.e_solv) # solvation energy = total − gas (Ha)
It is closed-shell (RHF) for the validated H-Xe INDO set; epsilon must be > 1.
The same calculation runs through the standard run_job entry point with
solvent= (the GEPOL cavity is used; the in-solvent total becomes
result.energy, with result.e_solv the solvation energy):
from vibeqc import Atom, Molecule, run_job
h2o = Molecule([Atom(8, [0, 0, 0]),
Atom(1, [0, 1.43, 1.11]), Atom(1, [0, -1.43, 1.11])]) # bohr
r = run_job(h2o, method="msindo", solvent="water") # or solvent=78.39
print(r.energy, r.e_solv)
solvent="vacuum" (ε ≤ 1) short-circuits to the gas-phase run; open-shell +
solvent raises (COSMO is RHF-only).
Cavity (cavity=)¶
cavity="gepol"(default), exact oracle parity. MSINDO’s own GEPOL/SAS tessellation: a pentakisdodecahedron (60 triangular faces) around each atom at the COSMOCOSMORradius, refined for segment centres + areas, with the Klamt self-energyA_ii = 1.07/√areaand a1/roff-diagonal capped at½·min(A_ii, A_jj). This reproduces reference MSINDO to ~2 × 10⁻⁹ Ha (H₂O ε=78.39: total −17.0264718 vs oracle −17.0264718; H₂O / HF / CH₄ / H₂S all ≤ 2e-9). vibe-qc builds each atom’s cavity independently, matching the oracle’sNOSYMrun; the reference’s default symmetry path transforms symmetry-equivalent atoms’ segments and can differ by up to ~10 µHa.cavity="lebedev", the shared CPCM cavity. vibe-qc’s Lebedev-on-Bondi cavity (the one HF/DFT CPCM uses). The multipole coupling is identical, on the same cavity it agrees with the Gaussian ESP coupling to ~0.1 mHa, but the absolute E_solv carries that cavity convention: H₂O ε=78.39 gives −6.83 vs the oracle’s −8.26 mHa (the larger Bondi×1.2 cavity under-screens). Theradii,radii_scale,solvent_probe_radius_ang, andn_points_per_spherearguments tune this cavity.
Molecular dynamics¶
MSINDO drives vibe-qc’s general molecular-dynamics engine (vibeqc.md):
from vibeqc.semiempirical.methods.msindo import run_msindo_md
traj = run_msindo_md(Z, coords_ang, dt_fs=0.5, n_steps=500, T_K=300,
thermostat="berendsen")
print(traj.positions.shape) # (n_steps, n_atoms, 3)
print(traj.energies) # total energies per step (Ha)
Available thermostats: NVE, Berendsen, Nosé-Hoover NVT. Velocity-Verlet integration, Maxwell-Boltzmann initialisation, COM removal.
Well-tempered metadynamics¶
from vibeqc.semiempirical.methods.msindo import run_msindo_metadynamics
from vibeqc.metadynamics import DistanceCV
traj, bias = run_msindo_metadynamics(
Z, coords_ang, collective_variables=[DistanceCV(0, 1)], # bond distance
dt_fs=0.5, n_steps=2000, T_K=300,
)
Free energy is recovered as F = −γ/(γ−1)·V_bias (well-tempered ensemble).
Available CVs: DistanceCV, AngleCV (with analytic Cartesian gradients).
Full examples: examples/semiempirical/24_msindo_md.py and
25_msindo_metadynamics.py.
SCF convergence¶
The closed-shell SCF is accelerated with Pulay DIIS, the error vector is the
commutator [F, P] in the symmetrically (Löwdin) orthogonalised basis, where the
overlap is the identity. Convergence is tested on the residual ‖[F, P]‖ rather
than on the energy step: under DIIS a small extrapolation step is not a
converged density, so a step-size test would stop early at the wrong point.
Parameters (pass to run_msindo):
Parameter |
Default |
Meaning |
|---|---|---|
|
200 |
Maximum SCF iterations |
|
1e-9 |
Convergence threshold on ‖[F,P]‖ (Ha) |
|
2 |
DIIS activated after this many iterations |
|
6 |
Number of DIIS error vectors stored |
A bare fixed-point iteration oscillates indefinitely on floppy, realistically sized molecules. The primary DIIS trajectory converges small molecules in a handful of iterations and a 27-atom alcohol (C₈H₁₈O, 54 basis functions) in ~25. Reported aggregate work also includes any root-selection probe and strict refinement described below. Because both backends converge to the same stationary point on the true residual, the C++ and Python engines agree to machine precision (< 10⁻¹³ Ha) on every reference molecule.
For closed-shell H-Ar INDO single points and ground-state analytic gradients, Hcore/DIIS remains the primary trajectory. The solver also evaluates the method’s extended-Hückel start with WICHT iteration as a finite alternate-basin probe. The probe retains the established 2000-cycle recovery floor even when primary DIIS converges, so merely crossing the caller’s strict-DIIS iteration cap cannot change which basin is considered. Every converged probe density is restarted under strict commutator DIIS before the stationary energies are compared. The alternate density is accepted only when the strict solve converges below the primary energy by more than the requested convergence resolution. The WICHT energy-only result is never reported directly. Acetamide exercises the lower-basin selection; cyclopropene exercises rejection of a higher alternate basin. Reported iteration counts include every attempted trajectory. Converged jobs outside the validated H-Ar scope retain their established path without a proactive probe. MP2, OVGF, CIS/CISD, and CIS-gradient reference-SCF routes are unchanged by this single-point fix.
If primary DIIS exhausts its iteration limit, the same WICHT-to-strict sequence retains its extended recovery budget, including outside the proactive H-Ar scope. This recovery is exercised by the exact archived Thiel tetrazine geometry, whose ordinary DIIS trajectory otherwise remains trapped in a cycle. NDDO and open-shell jobs retain their existing SCF routes. The validated heavy-element trajectory set continues to use the MSINDO-faithful WICHT energy path directly because strict DIIS changes its intended oracle-matched occupations. Their analytic-gradient SCF route remains the pre-existing strict stationary DIIS solve; this change does not reinterpret nonstationary WICHT densities as variational analytic-gradient densities.
Validation¶
examples/semiempirical/10_msindo_validate.py runs the full reference set and
prints the deviation from reference MSINDO:
molecule vibe-qc total reference Δ (Ha) conv
H2 -1.1732166873 -1.1732166872 -1.40e-10 OK
HF -24.3089965039 -24.3089965036 -2.94e-10 OK
N2 -19.2858118281 -19.2858118276 -4.56e-10 OK
CO -21.6961833926 -21.6961833869 -5.75e-09 OK
CH4 -8.2956064184 -8.2956064178 -5.83e-10 OK
H2O -17.0182087695 -17.0182087674 -2.06e-09 OK
H2S -11.2354667006 -11.2354666980 -2.63e-09 OK (3d)
SiH4 -6.3561206300 -6.3561206258 -4.16e-09 OK (3d)
PH3 -8.3298767931 -8.3298767745 -1.86e-08 OK (3d)
AlCl3 -44.2269575574 -44.2269575516 -5.77e-09 OK (3d)
OH -16.3330865712 -16.3330865643 -6.85e-09 OK (UHF doublet)
NO -25.3584589711 -25.3584589422 -2.89e-08 OK (UHF doublet)
O2 -31.5123087277 -31.5123087290 +1.32e-09 OK (UHF triplet)
CH3 -7.6239129200 -7.6239129136 -6.39e-09 OK (UHF doublet)
NH2 -10.7659659754 -10.7659659674 -8.01e-09 OK (UHF doublet)
The regression harness (examples/regression/msindo/) can rebuild the reference
energies from source (build_oracle.sh) and re-run the comparison
(runner_msindo.py), vibe-qc never imports MSINDO; it executes a reference
binary out-of-process and parses its output.
Atomic charges¶
Because MSINDO works in an orthogonal basis, Mulliken/Löwdin charges coincide:
q_A = CZ_A − Σ_{μ∈A} P_μμ. For H₂S the engine gives S −0.25, H +0.12,
the expected slight polarity. See the validation example for the snippet.
In the direct API, the charge per atom is:
blocks, _ = msindo._atom_blocks(Z)
for i_atom, (lo, hi) in enumerate(blocks):
pop = float(res.density.diagonal()[lo:hi].sum())
q = msindo.eff_core_charge(Z[i_atom]) - pop
print(f"Atom {i_atom} (Z={Z[i_atom]}): charge = {q:+.3f}")
Visualization¶
A MSINDO result (geometry + atomic charges) can be packaged into a .qvf
archive and explored in vibe-view. See
examples/vibe_view/showcase_msindo_h2s.py for an end-to-end script that
computes H₂S with MSINDO, writes a .qvf, and renders a structure-with-charges
image.
Feature matrix¶
Capability |
Status |
|---|---|
Closed-shell (RHF) |
✅ INDO (H-Xe); NDDO H, Li-F, Na-Cl |
Open-shell (UHF) |
✅ INDO UHF for validated light-element and heavy d/p-block fixtures |
Elements |
H-Xe (Z 1-54): full INDO parameter scope |
s / p shells |
✅ Python and C++ backend |
d shell (polarization and transition-metal valence) |
✅ Python and C++ backend for validated INDO routes; closed-shell NDDO SPDD for Al-Cl |
Periodic (CCM) |
✅ 1-D/2-D/3-D, Ewald Madelung, gradients + relaxation (experimental SECCM line, oracle-validated closed-shell energies) |
CCM via |
✅ |
SECCM reaction paths |
✅ closed-shell MSINDO via |
Molecular gradients |
✅ finite difference; closed-shell INDO analytic gradients via C++; closed-shell NDDO analytic gradients via Python |
Geometry optimization |
✅ L-BFGS-B via |
Transition states (NEB) |
✅ |
Excited states (CIS / TDA) |
✅ singlet/triplet |
Excited-state gradients |
✅ finite-difference, state-tracked |
Conical intersections (MECI) |
✅ penalty-function optimizer |
Implicit solvation (COSMO) |
✅ GEPOL (exact oracle parity) + Lebedev cavity |
MP2 (closed-shell) |
✅ INDO + NDDO references; SCS/SOS scaling |
UMP2 (open-shell) |
✅ spin-resolved αα/ββ/αβ |
Variational CISD |
✅ generic CI over INDO MOs |
Quasiparticle IPs/EAs (GF2/OVGF) |
✅ closed + open shell, renormalized |
Molecular dynamics |
✅ NVE + Berendsen + Nosé-Hoover NVT |
Metadynamics |
✅ well-tempered, DistanceCV + AngleCV |
Atomization energies |
✅ |
ROHF |
⛔ raises |
Cs and heavier (Z > 54) |
⛔ not parametrized |
Open-shell analytic molecular gradients |
⛔ finite-difference fallback only |
Open-shell d-block UHF |
🔶 validated on named fixtures; not a blanket route guarantee |
Open-shell COSMO |
⛔ RHF-only |
Comparison with the original MSINDO manual¶
For users familiar with the Fortran MSINDO program, here is the mapping of the original keyword-driven input to vibe-qc’s Python API:
Wavefunction keywords¶
Original MSINDO keyword |
vibe-qc Python equivalent |
|---|---|
|
Default (neutral, closed-shell) |
|
|
|
|
|
|
|
|
|
|
|
|
Geometry¶
Original input |
vibe-qc equivalent |
|---|---|
|
The default, coords are Cartesian Angstrom |
|
|
Internal (Z-matrix) coordinates |
Not supported; convert to Cartesian first |
CCM¶
Original keyword |
vibe-qc equivalent |
|---|---|
|
|
|
|
|
|
|
|
|
Handled internally (uses the MSINDO default step function) |
Dummy atom |
Not needed, vectors are passed directly as coordinates |
|
|
|
Not yet implemented |
SCF control¶
Original keyword |
vibe-qc equivalent |
|---|---|
|
|
|
SCF acceleration is selected internally; no user-facing damping/DIIS control |
|
Closed-shell H-Ar |
|
|
Solvation¶
Original keyword |
vibe-qc equivalent |
|---|---|
|
|
|
|
|
Default (independent per-atom cavities) |
Excited states¶
Original keyword |
vibe-qc equivalent |
|---|---|
|
|
|
|
|
|
|
Not applied (vibe-qc ships rigorous CIS) |
Miscellaneous¶
Original keyword |
vibe-qc equivalent |
|---|---|
|
Dump matrices via |
|
Not exposed; d is always present |
|
Not yet implemented |
Backend architecture (for developers)¶
MSINDO in vibe-qc is implemented as two cooperating engines:
Python reference engine (
python/vibeqc/semiempirical/methods/msindo.py): covers the validated INDO H-Xe surface and supplies the complete analytic multipole derivative for closed-shell NDDO H, Li-F, and Na-Cl, including the source SPDD contraction for Al-Cl. Open-shell NDDO remains gated.C++ engine (
cpp/include/vibeqc/semiempirical/methods/indo/): header-only, Eigen-based, and pybind-exposed. Production molecular energy calls use this native path across the validated INDO and closed-shell NDDO scopes. Closed-shell INDO analytic gradients are native; the NDDO analytic contraction runs in Python over the exact same energy definition.
The integral kernel (msindo_integrals.py / msindo_integrals.hpp) is a
faithful port of MSINDO’s aintgs.f/bintgs.f/c2int.f/v2int.f/overlap.f
STO engine: analytical Slater overlaps, two-centre Coulomb, nuclear attraction,
Slater-Condon one-centre integrals, and the HARMTR rotation transformation.
Key routines (for those reading the code)¶
Vibe-qc function |
Original MSINDO routine |
Purpose |
|---|---|---|
|
|
Builds H⁰ and two-centre γ |
|
|
INDO Fock matrix |
|
|
DIIS-accelerated SCF loop |
|
|
Per-pair local→global integral assembly |
|
|
One-centre Coulomb/exchange block |
|
|
Frozen-core pseudopotential |
|
|
Löwdin + d-hybrid blocks |
|
|
NDDO multipole 2e Fock |
|
|
Slater-Condon F⁰/F²/G¹ params |
|
|
Per-element ENEG/ATENG |
|
|
WS cell construction |
|
|
Ewald Madelung constants |
Tutorial: end-to-end workflow¶
1. Single-point energy¶
from vibeqc import Atom, Molecule, run_job
# Build a water molecule (bohr)
h2o = Molecule([
Atom(8, [0.0, 0.0, 0.0]),
Atom(1, [0.0, 1.43, 1.11]),
Atom(1, [0.0, -1.43, 1.11]),
])
result = run_job(h2o, method="msindo")
print(f"Total energy: {result.energy:.10f} Ha")
print(f"Binding energy: {result.binding_energy:.6f} Ha")
2. Geometry optimization¶
from vibeqc.semiempirical.methods.msindo import msindo_optimize
Z = [8, 1, 1]
xyz_start = [[0, 0, 0], [0, 0.8, 0.6], [0, -0.8, 0.6]] # Å
result = msindo_optimize(Z, xyz_start)
print(f"Optimized energy: {result.energy:.10f} Ha")
print(f"Optimized geometry (Å):\n{result.coords_angstrom}")
3. Solvation energy¶
from vibeqc.semiempirical.methods.msindo_cosmo import msindo_cosmo
# Water in water
r = msindo_cosmo([8, 1, 1],
[[0, 0, 0], [0, 0.757, 0.587], [0, -0.757, 0.587]],
epsilon=78.39)
print(f"In-water energy: {r.total_energy:.10f} Ha")
print(f"Solvation energy: {r.e_solv * 1000:.3f} mHa")
4. Periodic CCM, bulk → surface → adsorption¶
Full script: examples/semiempirical/11_msindo_ccm.py. In brief:
from vibeqc.semiempirical.methods.msindo_ccm import run_ccm, ccm_gradient_fd, ccm_optimize
a = 4.21 # MgO lattice constant (Å)
# Bulk MgO rocksalt cell
fcc = [(0,0,0), (0.5,0.5,0), (0.5,0,0.5), (0,0.5,0.5)]
Z_bulk = [12,12,12,12, 8,8,8,8]
C_bulk = [[f*a for f in p] for p in fcc[:4]] + \
[[(p[0]+0.5)*a, p[1]*a, p[2]*a] for p in fcc[:4]]
cell_3d = [[a,0,0], [0,a,0], [0,0,a]]
e_bulk = run_ccm(Z_bulk, C_bulk, cell_3d, madelung=True).total_energy
# Surface slab (2-D)
Z_slab = [12,8,8,12, 8,12,12,8]
C_slab = [[0,0,0], [a*0.5,0,0], [0,a*0.5,0], [a*0.5,a*0.5,0],
[0,0,a*0.5], [a*0.5,0,a*0.5], [0,a*0.5,a*0.5], [a*0.5,a*0.5,a*0.5]]
cell_2d = [[a,0,0], [0,a,0]]
e_slab = run_ccm(Z_slab, C_slab, cell_2d, madelung=True).total_energy
# Add water adsorbate
Z_ads = Z_slab + [8,1,1]
C_ads = C_slab + [[a*0.5, 0, a*0.5+2.2],
[a*0.5+0.76, 0, a*0.5+2.76],
[a*0.5-0.76, 0, a*0.5+2.76]]
e_sys = run_ccm(Z_ads, C_ads, cell_2d, madelung=True).total_energy
# Gas-phase water for the adsorption energy
from vibeqc.semiempirical.methods import msindo
e_h2o_gas = msindo.run_msindo([8,1,1],
[[0,0,0.1173], [0,0.7572,-0.4692], [0,-0.7572,-0.4692]]).total_energy
E_ads = e_sys - e_slab - e_h2o_gas # Hartree
print(f"Adsorption energy: {E_ads * 627.509:.2f} kcal/mol")
5. CIS excited states¶
from vibeqc.semiempirical.methods.msindo import msindo_cis
Z = [8, 1, 1]
xyz = [[0, 0, 0.117], [0, 0.757, -0.467], [0, -0.757, -0.467]] # Å
singlets = msindo_cis(Z, xyz, spin="singlet", n_states=5)
for i, ev in enumerate(singlets.excitation_energies_ev, 1):
print(f"S{i} {ev:.4f} eV")
6. NEB, reaction path¶
from vibeqc import Atom, Molecule, run_neb
import numpy as np
# NH3 umbrella flip: reactant and its mirror
def nh3(flip=False):
r, angle = 1.012, np.deg2rad(106.67)
sin_a = np.sqrt(2*(1-np.cos(angle))/3)
coords = [[0,0,0]]
for k in range(3):
phi = k * 2*np.pi/3
z = -r*np.sqrt(1-sin_a**2) if not flip else +r*np.sqrt(1-sin_a**2)
coords.append([r*sin_a*np.cos(phi), r*sin_a*np.sin(phi), z])
return Molecule([Atom(7, c) for c in coords[:1]]
+ [Atom(1, c) for c in coords[1:]])
res = run_neb(nh3(False), nh3(True), method="msindo", n_images=7, climbing=True)
print(f"Barrier: {res.barrier_kcal:.2f} kcal/mol")
Citing¶
When you publish MSINDO results from vibe-qc, cite the method papers (vibe-qc emits these automatically):
B. Ahlswede, K. Jug, Consistent modifications of SINDO1: I. Approximations and parameters, J. Comput. Chem. 20, 563 (1999).
B. Ahlswede, K. Jug, Consistent modifications of SINDO1: II. Applications to first- and second-row elements, J. Comput. Chem. 20, 572 (1999).
For NDDO calculations, also cite:
B. Voigt, On Bridging the Gap between the INDO and the NDDO Schemes, Theor. Chim. Acta 31, 289 (1973), DOI
10.1007/BF00527556.M. J. S. Dewar, W. Thiel, A semiempirical model for the two-center repulsion integrals in the NDDO approximation, Theor. Chim. Acta 46, 89 (1977), DOI
10.1007/BF00548085.
For periodic (CCM) calculations, cite:
T. Bredow, G. Geudtner, K. Jug, The Cyclic Cluster Model: A quantum chemical approach to periodic systems, J. Comput. Chem. 22, 89 (2001).
M. F. Peintinger, T. Bredow, The Cyclic Cluster Model revisited, J. Comput. Chem. 35, 839 (2014).
For COSMO solvation runs, also cite the conductor model and the CPCM/GEPOL
cavity/solver papers (emitted automatically when you pass output=):
A. Klamt, G. Schüürmann, COSMO: a new approach to dielectric screening in solvents…, J. Chem. Soc. Perkin Trans. 2, 799 (1993).
See also the citation guide.