Metadata-Version: 2.4
Name: pgsym
Version: 0.2.3
Summary: Point-group symmetry tools on the PySCF atomic-orbital basis
Author-email: Kevin Ackermann <ackermann@thphys.uni-heidelberg.de>
Maintainer-email: Kevin Ackermann <ackermann@thphys.uni-heidelberg.de>
License-Expression: GPL-3.0-or-later
Project-URL: Repository, https://github.com/ackiphys/pointgroupsymmetry/tree/main
Project-URL: Changelog, https://github.com/ackiphys/pointgroupsymmetry/blob/main/CHANGELOG.md
Keywords: quantum chemistry,computational chemistry,point group,molecular symmetry,group theory,character table,irreducible representation,symmetry-adapted basis,pyscf,self-consistent field,geometry optimization
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Chemistry
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.23
Requires-Dist: scipy>=1.8
Requires-Dist: pyscf>=2.1
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Provides-Extra: geomopt
Requires-Dist: geometric>=1.0; extra == "geomopt"
Provides-Extra: all
Requires-Dist: pgsym[geomopt,test]; extra == "all"
Dynamic: license-file

# pgsym

Point-group symmetry tools on the [PySCF](https://pyscf.org/) atomic-orbital basis.

`pgsym` builds matrix representations of molecular point-group operations **directly on the
AO basis** of a PySCF `Mole`, and uses them to:

- detect the point group from the geometry, or build any named group (`Cₙ`, `Cₙᵥ`, `Dₙₕ`,
  `Dₙd`, `Sₙ`, cubic `T/Td/Th/O/Oh`, icosahedral `I/Ih`, linear `C∞ᵥ`/`D∞ₕ`, atomic `O(3)`);
- compute **character tables** (Burnside–Dixon) with Mulliken labels and diatomic term symbols;
- build **irrep projectors** and a **symmetry-adapted basis**, and **block-diagonalise** any
  symmetry-invariant AO operator (Fock, density, …) per irrep;
- **label MOs** by irrep and report **per-irrep occupations** (real *or* complex coefficients);
- run **symmetry-conforming SCF** with optional **per-irrep occupation constraints**;
- **symmetrise geometries/gradients** and run **symmetry-constrained geometry optimisation**.

It is complementary to PySCF's own symmetry handling: operator-centric, works on any `Mole`
(no `symmetry=True` required), and interoperates with PySCF's irrep names.

## Installation

Requires Python ≥ 3.9.

### From PyPI

```bash
pip install pgsym            # runtime (numpy, scipy, pyscf)
pip install pgsym[geomopt]   # + geomeTRIC, for sym_optimize (geometry optimisation)
pip install pgsym[all]       # everything, incl. test deps
```

### From a local checkout

Clone the repository, then install from the project root (add `-e` for an
editable/development install):

```bash
pip install -e .            # runtime (numpy, scipy, pyscf)
pip install -e .[geomopt]   # + geomeTRIC, for sym_optimize (geometry optimisation)
pip install -e .[all]       # everything, incl. test deps
```

The `examples/` and `tests/` trees ship in the source distribution, so a local
checkout (or an unpacked sdist) with `[all]` has everything needed to run the
full test suite and the example scripts. PyPI wheels contain the library only.

## Quickstart

Detect the group, run a symmetry-conforming RHF, and print an irrep-labelled report:

```python
import pyscf.scf
from pyscf import gto
from pgsym import SymmetryDetection, apply_sym_eig

mol = gto.M(atom="O 0 0 0.117; H 0 0.757 -0.469; H 0 -0.757 -0.469", basis="sto-3g")

pg = SymmetryDetection(mol).point_group     # -> C2v
pg.irrep_names()                            # assign Mulliken labels (A1, B1, ...)

mf = pyscf.scf.RHF(mol)
apply_sym_eig(mf, pg)                        # every eig step now stays symmetry-pure
mf.kernel()
mf.analyze()                                 # irrep-labelled MO table + per-irrep occupations
```

## Guided tour

### Point groups: construct or detect

```python
from pgsym import PointGroup, SymmetryDetection, Rotation, Reflection

pg = PointGroup.C2v(mol)                     # named factory (axis/origin/tol optional)
pg = SymmetryDetection(mol).point_group      # auto-detect from geometry

# or from explicit generators (the group is the closure):
c2    = Rotation(mol, [0, 0, 1]).representation(2)
sigma = Reflection(mol, [0, 1, 0]).representation()
pg    = PointGroup(mol, [c2, sigma], name="C2v")

pg.order                                     # 4
pg.is_symmetric(mol.intor("int1e_ovlp"))     # True: the overlap is group-invariant
```

### Character tables, projectors, and symmetry-blocked diagonalisation

```python
pg.character_table(); pg.irrep_names()
pg.print_character_table()
pg.decompose_ao()                            # AO multiplicity of each irrep

P = pg.irrep_projectors()                    # {label: (nao, nao) real projector}, sum = I

S = mol.intor("int1e_ovlp")
H = mol.intor("int1e_kin") + mol.intor("int1e_nuc")
e, C, labels = pg.symmetry_eigh(H, S)        # generalised eigenproblem, solved per irrep
```

### MO labelling and per-irrep occupations

```python
from pgsym import classify_mo_irreps, irrep_occupations

labels = classify_mo_irreps(mo_coeff, S, pg)     # mo_coeff shape (2, nao, nmo); real OR complex
occ    = irrep_occupations(dm, pg, S)            # {irrep: electron count}
```

### Symmetry-conforming SCF and constrained occupations

`apply_sym_eig(mf, pg)` overrides only the `eig` step of a mean-field object, so every
iteration diagonalises the Fock per irrep and the MOs stay symmetry-pure — works for RHF,
UHF, ROHF, and the `x2c` classes (call it *after* `mf.x2c()`); `mf.pgs = None` restores the
original solver exactly.

Passing `irrep_nelec` additionally constrains the occupations per irrep (PySCF-style): listed
irreps are held at their electron count and the remaining electrons fill by aufbau. This lets
the SCF hold — or converge to — a specific symmetry root, including an *excited* configuration
ordinary aufbau would never reach:

```python
mf = pyscf.scf.RHF(mol)
apply_sym_eig(mf, pg, irrep_nelec={"A1": 6, "B1": 2, "B2": 2})   # {irrep: n} or {irrep: (na, nb)}
mf.kernel()
```

A scalar `n` splits `na = n - n//2`, `nb = n//2` for UHF/ROHF (it is the irrep total for RHF);
a `(na, nb)` tuple is used verbatim. The standalone `symmetry_constrained_occupation(H, pg,
occupations, ...)` does the same filling for a given Fock without running an SCF.

### Linear molecules and PySCF irrep names

Linear molecules (`C∞v`/`D∞h`) are labelled with diatomic term symbols (`Sigmag+`, `Pig`,
`Deltau`, …), which map losslessly to PySCF's linear-group irrep names (`A1g`, `E1gx`, …):

```python
from pgsym import pyscf_to_term, term_to_pyscf

pyscf_to_term("Dooh", "A1u")     # -> "Sigmau+"
term_to_pyscf("Dooh", "Piu")     # -> ["E1ux", "E1uy"]
```

Set `pg.pyscf_names = True` (or pass `pyscf_names=True` to `classify_mo_irreps`,
`naming="pyscf"` to `irrep_occupations`) to render output in PySCF naming.
`symmetry_constrained_occupation` and `irrep_nelec` accept occupation targets keyed by internal
labels, term symbols, **or** PySCF names interchangeably.

For PySCF's finite abelian groups (`D2h` and its subgroups: `C2h`, `C2v`, `D2`, `Cs`, `Ci`,
`C2`, `C1`) the Mulliken labels already coincide with PySCF's (including `Cs` = `A'`/`A"`), so
PySCF irrep names work directly; `pyscf_to_mulliken` / `mulliken_to_pyscf` provide the
(identity) conversion. Note the C₂ᵥ `B1`/`B2` *plane* convention can differ from PySCF's — when
cross-checking a symmetry-breaking state against PySCF, compare the physical state (read its
per-irrep counts in this package's labels) rather than matching by irrep name.

### Geometry symmetrisation and constrained optimisation

```python
from pgsym import symmetrize_molecule, sym_optimize

new_mol, report = symmetrize_molecule(mol, tol=1e-1)   # snap a distorted geometry onto exact symmetry

mf     = pyscf.scf.RHF(mol)
mol_eq = sym_optimize(mf)                               # relax while staying in the point group
```

`sym_optimize` auto-detects the group from the starting geometry and needs the `geomopt` extra;
`apply_sym_grad(mf, pg)` exposes the symmetry-projected gradient scanner for your own optimiser
call, and `symmetrize_gradient(grad, mol, pg)` projects a single `(natm, 3)` gradient.

## Examples

Runnable, self-contained scripts live in [`examples/`](examples/) — run any directly, e.g.
`python examples/01_point_groups.py`. See [`examples/README.md`](examples/README.md) for the
full index.

| # | Script | Shows |
|---|--------|-------|
| 01 | `01_point_groups.py` | building a `PointGroup` (factory / generators); group invariance |
| 02 | `02_symmetry_detection.py` | auto-detecting the point group of several molecules |
| 03 | `03_character_tables.py` | character table, Mulliken names, AO decomposition |
| 04 | `04_projectors_and_blocked_diag.py` | projectors and per-irrep `symmetry_eigh` |
| 05 | `05_mo_irreps_and_occupations.py` | MO irrep labels and per-irrep occupations |
| 06 | `06_constrained_occupation.py` | `symmetry_constrained_occupation` (degenerate split; capacity guard) |
| 07 | `07_symmetry_conforming_scf.py` | `apply_sym_eig`; `analyze()`; the `pgs=None` fallback |
| 08 | `08_constrained_occupation_scf.py` | `irrep_nelec`; an excited root cross-checked against PySCF |
| 09 | `09_linear_molecules.py` | linear-group term symbols ↔ PySCF names |
| 10 | `10_symmetrize_geometry.py` | `symmetrize_molecule` |
| 11 | `11_symmetry_constrained_optimization.py` | `sym_optimize` / `apply_sym_grad` (needs `[geomopt]`) |

## Documentation

The full reference manual is [`docs/manual.pdf`](docs/manual.pdf) (build with `make` in
`docs/`), covering:

- **Overview & installation** — capabilities, package layout, and how it relates to PySCF.
- **Theory** — the group-theoretic background and algorithms (Burnside–Dixon character tables,
  isotypic projectors, symmetry-adapted bases, Reynolds averaging).
- **API reference** — every public class and function, with signatures and source references.
- **Worked examples** — annotated versions of the scripts above, with expected output.

## Testing

```bash
pytest
```

The suite runs the standalone verification scripts under `tests/` as subprocesses. Real-SCF
tests are skipped automatically if the BLAS backend can't be loaded.

## License

GPL-3.0-or-later. See [`LICENSE`](LICENSE).
