"""BIPOLE-style periodic RKS driver in CRYSTAL's electrostatic gauge.
The DFT counterpart of :mod:`vibeqc.pbc_bipole`. Pure functionals use
the exact analytical-FT, G=0-dropped periodic Hartree J in the same
neutral-background convention as the GDF reference; hybrid functionals
retain the CRYSTAL-gauge Ewald split so the HF exchange correction can
share the native ``build_jk_2e_real_space`` component builder.
This driver keeps the same multi-k SCF scaffold as the RHF BIPOLE
driver: shared Ewald a across V_ne/E_nn/J^LR, analytic V_ne via
AO-pair Fourier transforms, real-space energy accounting, and
the maintained convergence accelerators (DIIS, MOM, level-shift schedule,
damping). The historical ODA hook fails closed.
"""
from __future__ import annotations
from .guess import periodic_guess_capabilities
from .guess import periodic_result_selection
from .periodic_k_density import real_space_density_from_per_k_density
import warnings
from dataclasses import dataclass, field
from typing import List, Optional, Sequence, Tuple, Union
import numpy as np
from ._vibeqc_core import (
BasisSet,
BlochKMesh,
EwaldOptions,
Functional,
GridOptions,
InitialGuess,
LatticeMatrixSet,
LatticeSumOptions,
PeriodicKSOptions,
PeriodicXCDensityDomain,
PeriodicSystem,
SCFIteration,
bloch_sum,
build_grid,
build_xc_periodic_kpoints,
direct_lattice_cells,
ewald_nuclear_repulsion,
nuclear_repulsion_per_cell,
real_space_density_from_kpoints,
real_space_density_from_kpoints_fractional,
)
from .bipole_domain import resolve_bipole_xc_density_reach
from .periodic_xc_diagnostics import check_xc_grid_electron_count
from .bipole_ext_el_pole import compute_ext_el_spheropole
from .guess import (
_coerce_periodic_driver_guess,
initial_density_closed_shell,
periodic_fock_guess_k,
)
from .level_shift_schedule import LevelShiftSchedule
from .mom import select_occupied_by_max_overlap as _mom_select
from .oda import compute_oda_lambda as _compute_oda_lambda
from .oda import oda_mix_densities as _oda_mix
from .pbc_bipole_common import (
bipole_electron_count,
bipole_hf_acceptance,
bipole_unshifted_orbitals,
bipole_confirmation_phase,
bipole_final_density_phase,
PBCBipoleEnergyComponents,
_bloch_sum_blocks,
_bloch_sum_blocks_multi_k,
bvk_torus_covered,
_compute_nuclear_lattice_ewald_reciprocal_ft,
_crystal_ewald_options,
_density_set_gamma_or_lattice,
_emit_bipole_semantic_diagnostic,
_expand_ibz_kmesh_for_ewald_j,
_lattice_contract,
_zero_cross_cell_density,
bloch_h_core_and_orthogonalizer,
build_bipole_one_electron_lattice,
prepare_bipole_lattice_options,
resolve_bipole_lattice_options,
reject_bipole_physical_legacy_gauge,
raise_bipole_ewald_real_cutoff,
resolve_bipole_ewald_alpha,
reject_bipole_ecp_options,
reject_bipole_fractional_legacy_gauge,
reject_bipole_quartet_far_field,
reject_bipole_lone_non_gamma_kpoint,
reject_bipole_solver_options,
reject_bipole_unsupported_ks_functional,
restricted_bipole_commutator_norm,
bipole_terminal_check,
refresh_bipole_terminal_trace,
resolve_fock_mixing,
resolve_auto_fock_mixing,
resolve_bipole_sr_image_extent,
resolve_bipole_fock_symmetry,
resolve_incremental_jk,
warn_bipole_exact_j_core_tail,
warn_bipole_charged_cell,
warn_bipole_legacy_multik_gauge,
smearing_basin_warning,
validate_bipole_kmesh,
)
from .pbc_bipole_fock import (
_sr_density_cells,
BipoleFockContext,
BipoleOutputCellFarmingExecution,
BipoleScreenedExchangeExecution,
build_bipole_restricted_fock,
split_k_density_list,
)
from .periodic_grid import build_periodic_becke_grid
from .periodic_rhf_multi_k_ewald import (
_damp_lattice_matrix,
_diag_in_orth_basis,
)
from .periodic_scf_accelerators import (
DynamicDamping,
MultiKPeriodicSCFAccelerator,
)
from .periodic_screened_exchange import resolve_periodic_exchange
from .progress import ProgressLogger, resolve_progress
from .scf_divergence import check_scf_divergence
from .smearing import (
SmearingOptions,
)
from .smearing import (
closed_shell_periodic_occupations as _closed_shell_periodic_occupations,
occupations_are_per_k_integer_aufbau as _occupations_are_per_k_integer_aufbau,
)
from .symmetry_integrals_reduced import (
compute_nuclear_lattice_reduced,
)
__all__ = [
"PBCBipoleRKSResult",
"run_pbc_bipole_rks",
]
[docs]
@dataclass
class PBCBipoleRKSResult:
"""Result of :func:`run_pbc_bipole_rks`."""
energy: float
e_electronic: float
e_nuclear: float
e_xc: float
e_coulomb: float
e_hf_exchange: float
n_iter: int
converged: bool
mo_energies: List[np.ndarray]
mo_coeffs: List[np.ndarray]
fock: List[np.ndarray]
overlap: List[np.ndarray]
hcore: List[np.ndarray]
density: LatticeMatrixSet
runtime_backend: str = "pbc-bipole"
scf_trace: List[SCFIteration] = field(default_factory=list)
e_ext_el_spheropole: Optional[float] = None
ewald_alpha_bohr_inv: Optional[float] = None
# Absolute internal ket-image radius used by an erfc SR build, if any.
# The analytic-gradient preview uses this to reject domain mismatch.
sr_image_extent_bohr: Optional[float] = None
# True when the Fock build used the group-invariant pair-resolved domain.
pair_resolved_fock_domain: bool = False
# Dudarev DFT+U contribution per unit cell (Hartree). 0 unless the
# caller passed ``dft_plus_u=[HubbardSite(...)]``.
e_dft_plus_u: float = 0.0
energy_components: List[PBCBipoleEnergyComponents] = field(
default_factory=list,
)
functional: str = ""
# Smearing diagnostics (zero when smearing_temperature == 0).
smearing_temperature: float = 0.0
fermi_level: float = 0.0
entropy: float = 0.0 # Entropy of the committed density used by energy.
free_energy: float = 0.0
occupations: List[np.ndarray] = field(default_factory=list)
# Gauge provenance (option (b)): True when the run used the
# corrected gauge (full-Bloch density, no spheropole, a_HF-scaled
# Ewald exchange split for hybrids).
exchange_ewald_split: bool = False
exchange_exxdiv: Optional[str] = None
# Non-None when finite-T smearing straddled the HOMO-LUMO gap and may
# have selected a near-metallic basin (ionic-Γ basin trap); carries
# the warning message. See smearing_basin_warning.
basin_warning: Optional[str] = None
# Cartesian k-points (bohr^-1) and weights this result spans, in the
# same order as the per-k ``mo_coeffs`` / ``mo_energies`` lists. Lets
# optional Gamma-only / single-k output writers locate Gamma instead of
# assuming the first k-point is Gamma. See PBCBipoleRHFResult.
kpoints_cart: Optional[np.ndarray] = None
kpoint_weights: Optional[np.ndarray] = None
# Effective previous-Fock weight after the shared automatic default is
# resolved. This can differ from ``options.fock_mixing``.
fock_mixing: float = 0.0
screened_exchange_execution: Optional[
BipoleScreenedExchangeExecution
] = None
# Corrected-gauge overlap-fold convergence diagnostic. Appended to
# preserve the positional signature of the historical result fields.
overlap_fold_drift: Optional[float] = None
# BIPOLE-EXACT-ZONE increment 1: radius (bohr) of the exact erfc
# output zone when restricted below the operator cutoff (None =
# exact zone == cutoff). Analytic-gradient consumers fail closed
# on a set value (FD remains the production force path).
exact_zone_bohr: Optional[float] = None
output_cell_farming_execution: Optional[
BipoleOutputCellFarmingExecution
] = None
restart_basis: object = None
restart_lattice: object = None
guess_selection: object = None
restart_mesh: object = None
restart_kpoints: object = None
restart_weights: object = None
# Ewald accuracy used by the SCF; gradients must retain its finite sums.
ewald_precision: float = 1e-8
resolved_domain: object = None
executed_support: object = None
chi_scf_acceptance: object = None
chi_xc_metadata: object = None
accepted_density_k: object = None
# Same terminal quadrature as e_xc, without density renormalization.
xc_grid_electrons: Optional[float] = None
xc_grid_electron_error: Optional[float] = None
xc_periodic_density_bras: bool = False
e_hf_exchange_components: dict[str, float] = field(default_factory=dict)
@dataclass
class _PBCBipoleRKSFockBuild:
"""Internal Fock-build bundle for the BIPOLE RKS driver."""
f2e_real: LatticeMatrixSet
f_k_list: List[np.ndarray]
e_j_short_range: Optional[float] = None
e_j_long_range: Optional[float] = None
e_exchange: Optional[float] = None
e_j_multipole: Optional[float] = None
# k-space exchange correction (Ewald exchange split, a_HF-scaled):
# lives in F(k), so its energy must be added to the
# lattice-contracted E_2e by the caller.
e_2e_k_correction: float = 0.0
# The q+G=0 (Madelung / exxdiv='ewald') share of e_2e_k_correction on
# its own, so the gauge is visible next to the total (#82).
e_exchange_finite_size: Optional[float] = None
# XC energy of the SAME grid quadrature whose V_xc entered f_k_list.
# Consuming this instead of re-running build_xc_periodic_kpoints keeps the
# iteration's V_xc and e_xc from one build (they used to come from
# two independent quadratures per iteration -- one discarded).
e_xc: Optional[float] = None
# Raw quadrature only: assembled Fock matrices may later receive +U.
xc_result: object = None
n_electrons_grid: Optional[float] = None
periodic_density_bras: bool = False
screened_exchange_execution: Optional[
BipoleScreenedExchangeExecution
] = None
output_cell_farming_execution: Optional[
BipoleOutputCellFarmingExecution
] = None
e_hf_exchange_components: dict[str, float] = field(default_factory=dict)
[docs]
def run_pbc_bipole_rks(
system: PeriodicSystem,
basis: BasisSet,
kmesh: BlochKMesh,
options=None,
*,
historical_domain: bool = False,
functional: Optional[str] = None,
linear_dep_threshold: float = 1e-7,
fock_mixing: Optional[float] = None,
canonical_orth_normalize_diag_first: bool = True,
level_shift_schedule: Optional[LevelShiftSchedule] = None,
use_mom: bool = False,
use_oda: bool = False,
oda_trust_lambda_max: float = 1.0,
use_incremental_fock: bool = True,
use_ewald_j_split: Optional[bool] = None,
ewald_omega: Optional[float] = None,
ewald_precision: float = 1e-8,
v_ne_grid_options: Optional[GridOptions] = None,
use_multipole_far_field: Optional[bool] = False,
multipole_l_max: int = 2,
use_exact_ft_j: bool = False,
use_exchange_ewald_split: Optional[bool] = None,
exchange_exxdiv: str = "ewald",
use_fock_symmetry: Optional[bool] = None,
use_fock_symmetry_reduce: Optional[bool] = None,
sr_image_precision: Optional[float] = 1e-6,
sr_image_extent_bohr: Optional[float] = None,
exact_zone_bohr: Optional[float] = None,
farm_output_cells: bool = False,
output_cell_farming_strategy: str = "cyclic",
output_cell_farming_task_kind: str = "direct-eri-output-cell",
progress: Union[bool, ProgressLogger, None] = None,
verbose: Optional[int] = None,
initial_density: Optional[Sequence[np.ndarray]] = None,
initial_density_k: Optional[Sequence] = None,
dft_plus_u: Optional[List["HubbardSite"]] = None,
bz_integration: Optional[str] = None,
_support_recorder=None,
_chi_xc=None,
xc_density_domain: PeriodicXCDensityDomain = (
PeriodicXCDensityDomain.AUTO
),
) -> PBCBipoleRKSResult:
"""Multi-k closed-shell RKS via the CRYSTAL-gauge BIPOLE scaffold.
Physical pair support is the default. ``historical_domain=True`` selects
the historical index ball for reproduction. This keyword overrides
``options.lattice_opts.pair_complete_1e`` without mutating caller options.
Parameters
----------
system, basis, kmesh
As in :func:`run_pbc_bipole_rhf`.
options
:class:`PeriodicKSOptions` or None for PBE defaults.
functional
XC functional name (overrides ``options.functional`` if given).
fock_mixing
Optional previous-Fock weight override in ``[0, 1)``. The caller's
``options.fock_mixing`` field is not rewritten. A resolved zero can
still activate the documented DIIS-off KS automatic value.
use_exchange_ewald_split, exchange_exxdiv
Exchange/gauge convention (option (b), 2026-06-11). ``None`` =
auto: the corrected gauge is ON at 3D Γ-only sampling under the
Ewald J split -- full-Bloch density (no Γ-locality projection,
which also fixes the XC grid density r(r): the projected
density dropped the cross-cell AO products), no EXT
EL-SPHEROPOLE term, and for hybrids (a_HF > 0) the Ewald
exchange split ``K = K_SR(erfc w) + K_LR(reciprocal K!=0) +
(ξ_M - pi/(Vw^2)).S.D.S`` scaled by a_HF. Pure functionals need
no exchange machinery -- the gauge change alone applies (and
the wasted full-Coulomb K traversal of the legacy path is
skipped). See :func:`run_pbc_bipole_rhf` for the full
convention notes. The corrected gauge is the default at BOTH
Γ and multi-k (Phase-5 flip 2026-06-13): at multi-k it adds the
q = k-k′ LR-exchange channels + BvK-supercell Madelung
correction (requires a Monkhorst-Pack mesh; an ad-hoc k-list
falls back to the legacy gauge under the auto default). Pass
``use_exchange_ewald_split=False`` for the legacy gauge.
bz_integration
``None`` / ``"smearing"`` use the existing Aufbau or
Fermi-Dirac occupation path. ``"gilat"`` selects the
parameter-free Gilat-Raubenheimer net at T = 0; it cannot be
combined with finite-temperature smearing.
sr_image_precision, sr_image_extent_bohr
M5 erfc SR image-domain controls; see
:func:`run_pbc_bipole_rhf`. Pure semilocal RKS routes its Hartree J
through the same padded SR+LR erfc composition as every other route
(exact-FT retirement, 2026-07-18), so these controls apply to it.
Screened hybrids pad the shared SR J base; their dedicated screened
exchange traversal is a separate implementation path.
use_exact_ft_j
Opt-in cross-check oracle (default ``False``): route the pure
semilocal Hartree J through the exact analytic-FT Ewald builder
(``VIBEQC_J_EWALD3D_KE`` reciprocal cutoff, default 200 Ha) instead
of the production padded SR+LR composition. The oracle's finite-ke
reciprocal tail undercounts *absolute* totals on dense-core cells
(~0.5-1 Ha at ke=200 on MgO-class bases; the run warns with the
required ke estimate) while cancelling in energy differences. Pure
functionals on the corrected split only; fails closed otherwise.
use_fock_symmetry_reduce
``None`` auto-enables pair-resolved reduction for attached-symmetry
crystals on the corrected split. See :func:`run_pbc_bipole_rhf` for
the explicit opt-out and enforcement-only forms.
farm_output_cells
Distribute complete direct-ERI real-space output blocks across MPI
ranks and gather them before incremental Fock bookkeeping. Every
task retains the full internal translation sum. Generic BIPOLE
callers leave this off; the chi-CCM restricted four-center selector
supplies ``"chi-direct-output-cell"`` as its execution label.
Screened hybrids fail closed because their separate J and screened-K
phases require a multi-phase execution record.
All other parameters
As in :func:`run_pbc_bipole_rhf`.
Returns
-------
PBCBipoleRKSResult
"""
reject_bipole_quartet_far_field(
use_multipole_far_field,
driver="run_pbc_bipole_rks",
)
reject_bipole_lone_non_gamma_kpoint(
kmesh,
driver="run_pbc_bipole_rks",
)
from ._vibeqc_core import PeriodicKSOptions as _PKSOpts
caller_supplied_options = options is not None
opts = options if caller_supplied_options else _PKSOpts()
guess = _coerce_periodic_driver_guess(
getattr(opts, "initial_guess", None),
driver="run_pbc_bipole_rks",
supported=periodic_guess_capabilities('bipole', 'RKS', dim=getattr(system, "dim", 3), multi_k=(len(kmesh.kpoints) > 1 or np.prod(kmesh.mesh) > 1), transport='lattice'),
restart_supplied=initial_density is not None or initial_density_k is not None,
)
_patom_seed_pending = guess == InitialGuess.PATOM
reject_bipole_ecp_options(
opts,
driver="run_pbc_bipole_rks",
basis=basis,
system=system,
)
reject_bipole_solver_options(opts, driver="run_pbc_bipole_rks")
if int(opts.max_iter) < 1:
raise ValueError("run_pbc_bipole_rks: max_iter must be at least 1")
if not isinstance(farm_output_cells, (bool, np.bool_)):
raise TypeError("run_pbc_bipole_rks: farm_output_cells must be bool")
farm_output_cells = bool(farm_output_cells)
if output_cell_farming_strategy not in ("block", "cyclic"):
raise ValueError(
"run_pbc_bipole_rks: output_cell_farming_strategy must be "
"'block' or 'cyclic'"
)
if (
not isinstance(output_cell_farming_task_kind, str)
or not output_cell_farming_task_kind.strip()
):
raise ValueError(
"run_pbc_bipole_rks: output_cell_farming_task_kind must be a "
"non-empty string"
)
if use_oda:
raise NotImplementedError(
"run_pbc_bipole_rks: ODA is unavailable because a mixed "
"line-search density has no single orbital representation; "
"use DIIS instead"
)
if functional is not None:
opts.functional = str(functional)
if not getattr(opts, "functional", None):
opts.functional = "pbe"
smearing_T = float(getattr(opts, "smearing_temperature", 0.0))
if smearing_T < 0.0:
raise ValueError("run_pbc_bipole_rks: smearing_temperature must be >= 0")
if bz_integration is not None:
bz_integration = str(bz_integration).strip().lower()
if bz_integration not in ("smearing", "gilat"):
raise ValueError(
"run_pbc_bipole_rks: bz_integration must be None, "
f"'smearing', or 'gilat'; got {bz_integration!r}"
)
use_gilat = bz_integration == "gilat"
if use_gilat and smearing_T > 0.0:
raise NotImplementedError(
"run_pbc_bipole_rks: bz_integration='gilat' is a "
"sharp-Fermi-surface occupation backend and cannot be combined "
"with finite-temperature smearing."
)
use_fractional_occupations = smearing_T > 0.0 or use_gilat
func = Functional(opts.functional, 1) # spin-unpolarised
if _chi_xc is not None:
_chi_xc.require_functional(func)
reject_bipole_unsupported_ks_functional(
func,
driver="run_pbc_bipole_rks",
)
uses_external_xc = bool(getattr(func, "is_external", False))
if uses_external_xc:
from .periodic_external_xc import (
_grid_options_for_external,
_require_zero_temperature_external_xc,
)
_require_zero_temperature_external_xc(
smearing_T,
where="run_pbc_bipole_rks",
)
opts.grid = _grid_options_for_external(
func,
opts.grid if caller_supplied_options else None,
where="run_pbc_bipole_rks",
)
if xc_density_domain != PeriodicXCDensityDomain.PERIODIC_LATTICE:
raise ValueError(
"run_pbc_bipole_rks: external XC requires "
"xc_density_domain=PERIODIC_LATTICE"
)
if not bool(getattr(opts, "use_periodic_becke", False)):
raise ValueError(
"run_pbc_bipole_rks: external XC requires "
"use_periodic_becke=True"
)
# CAM exchange assembly. alpha_hf is the FULL-RANGE exact-exchange
# fraction and drives the Ewald exchange-split machinery (xi_M,
# K_LR q-channels, K_corr). Screened hybrids (hse06) have c_full
# = 0 -- none of that machinery engages; their erfc short-range K
# is added inside build_bipole_restricted_fock (no seam, no
# far-field, per the CRYSTAL RSH treatment). Ordinary periodic routes
# reject LR-heavy RSH; the chi callback supplies its validated assembly.
exx = (
_chi_xc.exchange_assembly if _chi_xc is not None
else resolve_periodic_exchange(func, where="run_pbc_bipole_rks")
)
alpha_hf = float(exx.c_full)
if _chi_xc is not None and uses_external_xc:
raise NotImplementedError("chi XC callback requires a native functional")
if farm_output_cells and exx.is_screened:
raise NotImplementedError(
"run_pbc_bipole_rks: direct-ERI output-cell farming is not "
"available for screened hybrids because the current single-phase "
"execution record cannot attest both the base J and separate "
"screened-K traversals"
)
# CRYSTAL-style auto-FMIXING: fires only for DFT without DIIS (see
# resolve_auto_fock_mixing for the Gap-B measurement basis).
requested_fock_mixing = resolve_fock_mixing(
opts,
fock_mixing,
where="run_pbc_bipole_rks",
)
fock_mixing_value = resolve_auto_fock_mixing(
requested_fock_mixing,
alpha_hf=alpha_hf,
use_diis=bool(opts.use_diis),
where="run_pbc_bipole_rks",
)
lat_opts: LatticeSumOptions = resolve_bipole_lattice_options(
opts.lattice_opts, historical_domain=historical_domain,
)
plog = resolve_progress(progress, verbose=verbose)
(
use_ewald_j_split,
use_ewald_j_split_auto,
lat_opts_2e,
lat_opts_1e,
) = prepare_bipole_lattice_options(system, lat_opts, use_ewald_j_split, plog)
plog.info(
f"PBC BIPOLE RKS (CRYSTAL-gauge) / cutoff {lat_opts.cutoff_bohr:.2f} bohr"
)
if exx.is_screened:
plog.info(
f" functional = {opts.functional}, screened exchange "
f"K = {exx.c_full:g}*K_full + "
f"{exx.c_sr:g}*K_erfc(omega={exx.omega_screen:g} bohr^-1)"
)
else:
plog.info(
f" functional = {opts.functional}, hf_exchange_fraction = {alpha_hf}"
)
if use_gilat:
plog.info(" bz integration = Gilat-Raubenheimer sharp-Fermi net")
plog.info(
f" F^2e (J + V_xc{'+ K' if exx.needs_exchange else ''}) : "
f"{'EWALD_J_SPLIT' if use_ewald_j_split else lat_opts_2e.coulomb_method.name}"
)
n_elec = system.n_electrons()
if n_elec % 2 != 0:
raise ValueError(
f"run_pbc_bipole_rks: closed-shell RKS requires even electron "
f"count; got {n_elec}"
)
if system.multiplicity != 1:
raise ValueError(
f"run_pbc_bipole_rks: requires multiplicity=1; got {system.multiplicity}"
)
n_occ = n_elec // 2
_kmesh_ibz = kmesh
_ir_mapping = np.asarray(getattr(kmesh, "ir_mapping", []), dtype=int).reshape(-1)
k_points = list(_kmesh_ibz.kpoints)
weights = np.asarray(_kmesh_ibz.weights, dtype=float)
_input_true_multik = (
len(k_points) > 1
or _ir_mapping.size > 1
or int(np.prod(np.asarray(getattr(kmesh, "mesh", (1, 1, 1))))) > 1
)
if use_ewald_j_split and _ir_mapping.size > 0:
# IBZ inputs run on the EXPANDED full mesh (correctness: the
# IBZ-native replication shortcut lacked the star AO rotations
# D(R.k) = P(R).D(k).P(R)ᵀ and broke non-trivial crystals -- MgO
# probe 2026-06-10, 8.25 Ha. See vibeqc.periodic_k_symmetry for
# the transport groundwork and pbc_bipole.py for the full note).
kmesh_full = _expand_ibz_kmesh_for_ewald_j(system, kmesh, plog)
if len(list(kmesh_full.kpoints)) > len(k_points):
kmesh = kmesh_full
k_points = list(kmesh.kpoints)
weights = np.asarray(kmesh.weights, dtype=float)
_ir_mapping = np.asarray([], dtype=int)
k_points_full = k_points
weights_full = weights
else:
k_points_full = k_points
weights_full = weights
n_k = len(k_points)
if n_k == 0:
raise ValueError("kmesh has no k-points")
if not np.isclose(weights.sum(), 1.0):
raise ValueError(f"kmesh.weights must sum to 1; got {weights.sum():.6f}")
if (
_input_true_multik
and initial_density is None
and initial_density_k is None
and getattr(opts, "initial_guess", InitialGuess.AUTO) == InitialGuess.READ
):
raise NotImplementedError(
"run_pbc_bipole_rks: multi-k READ requires an explicit complete "
"real-space initial_density block set; a Gamma-only read_density "
"cannot reconstruct the per-k Bloch density."
)
plog.info(
f"k-mesh: {n_k} k-point{'s' if n_k != 1 else ''}, "
f"weights sum = {weights.sum():.4f}"
)
# ---- Exchange/gauge resolution (option (b), 2026-06-11) ----------
# Mirrors run_pbc_bipole_rhf: corrected gauge at 3D Γ under the J
# split (full-Bloch density, no spheropole; a_HF-scaled Ewald
# exchange split for hybrids). Multi-k: explicit opt-in runs the
# q!=0 LR-exchange channels (Phase 3); auto stays legacy pending
# certification.
if exchange_exxdiv not in ("ewald", "none"):
raise ValueError(
f"run_pbc_bipole_rks: exchange_exxdiv must be 'ewald' or "
f"'none'; got {exchange_exxdiv!r}"
)
_x_split_auto = use_exchange_ewald_split is None
exchange_split_active = (
bool(use_ewald_j_split) if _x_split_auto else bool(use_exchange_ewald_split)
)
if exchange_split_active and not use_ewald_j_split:
raise ValueError(
"run_pbc_bipole_rks: use_exchange_ewald_split=True requires "
"the Ewald J split (use_ewald_j_split=True)."
)
# Multi-k split (option (b) Phase 3): q-channel tables, the
# BvK-torus density fold, and the supercell Madelung correction
# all need the true Monkhorst-Pack dimensions; ad-hoc k-lists
# (mesh = (1,1,1) placeholders) are rejected.
_bvk_mesh: Optional[Tuple[int, int, int]] = None
if exchange_split_active and n_k > 1:
_mesh_attr = tuple(int(x) for x in getattr(kmesh, "mesh", (1, 1, 1)))
if int(np.prod(_mesh_attr)) != n_k:
if _x_split_auto:
plog.info(
" multi-k corrected exchange gauge needs a "
"Monkhorst-Pack mesh (BvK-torus fold + supercell ξ_M); "
"this ad-hoc k-list has no mesh metadata -> falling back "
"to the legacy gauge. Pass a monkhorst_pack(...) mesh "
"for the corrected gauge."
)
exchange_split_active = False
else:
raise ValueError(
"run_pbc_bipole_rks: the Ewald exchange split at multi-k "
"requires a Monkhorst-Pack BlochKMesh carrying its mesh "
f"dimensions (got mesh={_mesh_attr} for {n_k} k-points). "
"Build the mesh via monkhorst_pack(...); ad-hoc k-point "
"lists are not supported on the corrected gauge."
)
else:
_bvk_mesh = _mesh_attr
if _bvk_mesh is None and n_k > 1:
# Non-split multi-k routes (lower-dimensional cells, the legacy
# gauge) carry the same BvK-periodic real-space density, so every
# per-k consumer folds it on the torus too whenever the mesh
# carries its Monkhorst-Pack dimensions. An ad-hoc k list has no
# torus and keeps the unprojected Bloch sum.
_mesh_attr = tuple(int(x) for x in getattr(kmesh, "mesh", (1, 1, 1)))
if int(np.prod(_mesh_attr)) == n_k:
_bvk_mesh = _mesh_attr
if uses_external_xc:
validate_bipole_kmesh(
kmesh,
driver="run_pbc_bipole_rks external XC",
require_complete=True,
)
if not exchange_split_active:
raise NotImplementedError(
"run_pbc_bipole_rks: periodic external XC requires the "
"corrected Ewald exchange gauge. Leave "
"use_exchange_ewald_split enabled and use a complete "
"Monkhorst-Pack mesh or its valid expandable IBZ reduction."
)
reject_bipole_fractional_legacy_gauge(
use_ewald_j_split=bool(use_ewald_j_split),
exchange_split_active=exchange_split_active,
fractional_occupations=use_fractional_occupations,
driver="run_pbc_bipole_rks",
)
reject_bipole_physical_legacy_gauge(
physical_pairs=lat_opts_2e.pair_complete_1e, dimension=system.dim,
use_ewald_j_split=use_ewald_j_split,
exchange_split_active=exchange_split_active, driver="run_pbc_bipole_rks",
)
warn_bipole_legacy_multik_gauge(system, exchange_split_active, n_k, plog)
warn_bipole_charged_cell(system, plog)
_xi_madelung = 0.0
if exchange_split_active and exchange_exxdiv == "ewald" and (alpha_hf > 0.0 or _patom_seed_pending):
if n_k > 1:
from .bipole_fock_ewald import probe_charge_madelung_supercell
assert _bvk_mesh is not None
_xi_madelung = probe_charge_madelung_supercell(system, _bvk_mesh)
else:
from .bipole_fock_ewald import probe_charge_madelung
_xi_madelung = probe_charge_madelung(system)
overlap_fold_drift: Optional[float] = None
needs_pair_difference_xc = (
_chi_xc is None
and xc_density_domain == PeriodicXCDensityDomain.PERIODIC_LATTICE
)
# AUTO can select periodic bras once cross-cell density appears. Reserve
# their exact differences without forcing AUTO away from molecular density.
needs_xc_difference_support = (
_chi_xc is None
and xc_density_domain != PeriodicXCDensityDomain.MOLECULAR_HOME
)
if _chi_xc is None:
_xc_image_radius = resolve_bipole_xc_density_reach(lat_opts_2e)
lat_opts_2e.becke_image_radius_bohr = _xc_image_radius
if exchange_split_active:
plog.info(
" Gauge: corrected (full Bloch density, no spheropole"
+ (
f"; exchange split exxdiv={exchange_exxdiv}, "
f"a_HF={alpha_hf:g}"
+ (
f", ξ_M = {_xi_madelung:.6f} Ha"
if exchange_exxdiv == "ewald"
else ""
)
if alpha_hf > 0.0
else "; pure functional -- no HF exchange"
)
+ ")"
)
# Exact-FT retirement (maintainer ruling 2026-07-13, landed
# 2026-07-18): pure semilocal RKS defaults to the same padded SR+LR
# Ewald-J composition as every other erfc route. The M4b
# interaction-resolved screening plus the M5 ``sr_image_precision``
# padding repaired the historical SR ket-image truncation that
# motivated the exact-FT default, while the exact-FT builder's
# finite-ke reciprocal core tail undercounts absolute totals on
# dense-core cells by ~0.5-1 Ha at the default ke=200 (cancels in
# differences; breaks absolute CRYSTAL/PySCF parity). The analytic-FT
# builder stays available as the explicit ``use_exact_ft_j``
# cross-check oracle and fails closed off its validated envelope.
if use_exact_ft_j:
if alpha_hf != 0.0 or exx.needs_exchange:
raise ValueError(
"run_pbc_bipole_rks: use_exact_ft_j is the pure-functional "
"exact-FT J cross-check oracle; it does not apply to hybrid "
f"or screened-hybrid functionals (a_HF = {alpha_hf:g}, "
f"screened = {bool(exx.is_screened)})."
)
if not (use_ewald_j_split and exchange_split_active):
raise ValueError(
"run_pbc_bipole_rks: use_exact_ft_j requires the corrected "
"exchange-split gauge under the Ewald J split (3D periodic "
"Monkhorst-Pack sampling); the legacy gauge has no exact-FT "
"J route."
)
if use_multipole_far_field:
raise ValueError(
"run_pbc_bipole_rks: use_exact_ft_j cannot be combined "
"with the retired multipole far-field research artifact."
)
use_exact_ewald_j_for_pure_rks = bool(use_exact_ft_j)
exact_j_ke_cutoff = 200.0
exact_j_chunk_size = 512
if use_exact_ewald_j_for_pure_rks:
import os
exact_j_ke_cutoff = float(os.environ.get("VIBEQC_J_EWALD3D_KE", "200.0"))
exact_j_chunk_size = max(
1,
int(os.environ.get("VIBEQC_J_EWALD3D_CHUNK", "512")),
)
plog.info(
" Pure-RKS Hartree J: exact analytic-FT Ewald cross-check "
f"oracle (G=0 dropped, ke_cutoff={exact_j_ke_cutoff:g} Ha)"
)
warn_bipole_exact_j_core_tail(basis, exact_j_ke_cutoff, plog)
# ---- Shared Ewald state (one a across V_ne / E_nn / J_LR) -------
ewald_options_1e: Optional[EwaldOptions] = None
omega_used: Optional[float] = None
ewald_cell_volume: Optional[float] = None
ewald_k_max: Optional[float] = None
from .pbc_bipole_common import resolve_bipole_ewald_state
ewald_options_1e, omega_used, ewald_cell_volume, ewald_k_max = (
resolve_bipole_ewald_state(
system, lat_opts_1e, lat_opts_2e, ewald_omega=ewald_omega,
ewald_precision=ewald_precision,
erfc_exchange_arm_active=exchange_split_active and (exx.c_full != 0.0 or _patom_seed_pending),
plog=plog,
)
)
_erfc_sr_build_active = (
not use_exact_ewald_j_for_pure_rks or bool(exx.is_screened) or _patom_seed_pending
)
# The shared padded ket-image ball must converge both active erfc
# kernels. HSE06's screened exchange (omega=0.11) can be longer-ranged
# than the Ewald J kernel, so the smaller omega owns the radius.
_sr_domain_omega = (
min(float(omega_used), float(exx.omega_screen))
if exx.is_screened and omega_used is not None
else omega_used
)
from .pbc_bipole_common import ewald_erfc_lattice_options
_v_ne_erfc_options = (
ewald_erfc_lattice_options(
lat_opts_1e, omega_used, ewald_precision, basis=basis, system=system,
) if ewald_options_1e is not None and v_ne_grid_options is None else None
)
_sr_image_extent = resolve_bipole_sr_image_extent(
basis,
system,
lat_opts,
lat_opts_2e,
_sr_domain_omega,
use_ewald_j_split=bool(use_ewald_j_split),
sr_image_precision=sr_image_precision,
sr_image_extent_bohr=sr_image_extent_bohr,
# The opt-in exact-FT J oracle has no erfc SR traversal to pad;
# every production route (incl. pure RKS since the exact-FT
# retirement) and screened hybrids pad the shared J/K domain.
erfc_sr_build_active=_erfc_sr_build_active,
plog=plog,
)
if farm_output_cells and (
not exchange_split_active or _sr_image_extent is None
):
raise NotImplementedError(
"run_pbc_bipole_rks: direct-ERI output-cell farming currently "
"requires the corrected 3D Ewald exchange split and a padded "
"short-range image domain"
)
# ---- DFT grid ----------------------------------------------------
# Wrapped in plog.stage so a job that stalls here (periodic Becke
# partitioning over image cells can be minutes on dense/large cells)
# shows ``[dft_grid]`` as its last live line instead of going dark --
# the pre-SCF setup that walltimed P13 MgO BIPOLE/XC left no marker
# localizing which stage was slow.
grid = None
if _chi_xc is None:
with plog.stage(
"dft_grid",
detail=(
"periodic Becke" if getattr(opts, "use_periodic_becke", False)
else "molecular grid on unit cell"
),
):
if getattr(opts, "use_periodic_becke", False):
grid = build_periodic_becke_grid(
system,
grid_options=opts.grid,
image_radius_bohr=float(
getattr(opts, "becke_image_radius_bohr", 10.0)
),
)
else:
grid = build_grid(system.unit_cell_molecule(), opts.grid)
# ---- Real-space one-electron integrals ---------------------------
if (_support_recorder is not None or _chi_xc is not None) and (
int(system.dim) != 3 or v_ne_grid_options is not None
or not use_ewald_j_split or not exchange_split_active
):
raise NotImplementedError("chi support requires analytic 3D Ewald J")
_nuclear_support = None
if _support_recorder is not None:
_support_recorder.preflight(system=system, basis=basis,
lattice=lat_opts_1e, nuclear=_v_ne_erfc_options)
with plog.stage(
"integrals_lattice",
detail=f"S/T/V at cutoff {lat_opts.cutoff_bohr:.2f} bohr",
):
S_lat, T_lat, V_lat, v_ne_lr_cache = build_bipole_one_electron_lattice(
basis,
system,
lat_opts_1e,
lat_opts_2e,
ewald_options_1e,
ewald_precision,
ewald_k_max,
v_ne_grid_options,
plog,
sym_log_style="plain",
resolved_erfc_options=_v_ne_erfc_options,
)
cells = list(S_lat.cells)
plog.info(f"n_cells in lattice sum = {len(cells)}")
# SYM3b Fock-symmetry resolution -- before the density-list decision
# (pair-resolved mode widens the density support; see
# run_pbc_bipole_rhf).
# A requested exact zone is not composable with the SYM3b reduced /
# pair-resolved output domains yet (increment 1): let an AUTO
# reduction yield to the explicit zone request; an explicit
# use_fock_symmetry_reduce=True still reaches the resolver's
# fail-closed raise below.
_reduce_request = use_fock_symmetry_reduce
if exact_zone_bohr is not None and _reduce_request is None:
_reduce_request = False
_fock_sym_map, _rep_cell_indices = resolve_bipole_fock_symmetry(
system,
basis,
lat_opts_2e,
use_fock_symmetry,
_reduce_request,
plog,
exchange_split_active=exchange_split_active,
auto_reduce_safe=(
not _erfc_sr_build_active or _sr_image_extent is not None
),
)
_pair_mode = _fock_sym_map is not None and getattr(
_fock_sym_map, "pair_resolved", False
)
from .pbc_bipole_common import resolve_bipole_exact_zone
_exact_zone = resolve_bipole_exact_zone(
system,
lat_opts_2e,
exact_zone_bohr,
exchange_split_active=exchange_split_active,
sr_image_extent=_sr_image_extent,
pair_mode=_pair_mode,
rep_cell_indices=_rep_cell_indices,
plog=plog,
)
# ---- Lattice-fold convergence guard (gauge-independent) -------------
# Shared guard (enforce_bipole_fold_support); hoisted above the gauge
# condition 2026-08-06. The wide density list below is also required by
# explicit periodic-lattice XC so every active pair difference exists.
from .pbc_bipole_common import enforce_bipole_fold_support
overlap_fold_drift = enforce_bipole_fold_support(
basis,
system,
lat_opts_2e,
method="rks",
n_k=n_k,
k_points=(k_points if n_k > 1 else None),
exchange_split_active=exchange_split_active,
plog=plog,
)
if exchange_split_active or needs_xc_difference_support or lat_opts_2e.pair_complete_1e:
if _pair_mode:
# M3: pair-resolved density support (see run_pbc_bipole_rhf).
cells_density = list(_fock_sym_map.density_domain.cells)
else:
cells_density = list(
_sr_density_cells(basis, system, lat_opts_2e, _sr_image_extent)
)
if needs_xc_difference_support:
from .periodic_external_xc import (
periodic_xc_difference_closed_cells,
)
exact_xc_cells = periodic_xc_difference_closed_cells(
system,
float(lat_opts_2e.cutoff_bohr),
_xc_image_radius,
)
by_index = {
tuple(np.asarray(cell.index, dtype=int)): cell
for cell in (*cells_density, *exact_xc_cells)
}
cells_density = sorted(
by_index.values(),
key=lambda cell: (
float(np.dot(cell.r_cart, cell.r_cart)),
tuple(np.asarray(cell.index, dtype=int)),
),
)
label = "density" if exchange_split_active else "XC density"
if _pair_mode:
detail = (
"pair-resolved Fock support plus atom-pair-complete XC "
"differences"
if needs_xc_difference_support
else "pair-resolved support at 2x cutoff"
)
plog.info(
f" {label} cell list: {len(cells_density)} cells "
f"({detail}; SR tensor has "
f"{_fock_sym_map.density_domain.n_triples} qualifying pairs)"
)
else:
plog.info(
f" {label} cell list: {len(cells_density)} cells "
"(atom-pair-complete difference support)"
)
else:
cells_density = cells
# ---- Per-k S(k), Hcore(k), X(k) ---------------------------------
from .linear_dependence import (
check_overlap_matrix,
format_linear_dependence_report,
raise_if_severe,
scf_preflight_overlap_check,
)
S_k_list: List[np.ndarray] = []
Hcore_k_list: List[np.ndarray] = []
X_k_list: List[np.ndarray] = []
overlap_reports = []
for k_idx, k in enumerate(k_points):
k_arr = np.asarray(k, dtype=float).reshape(3)
S_k, T_k, V_k, H_k, X_k, n_kept = bloch_h_core_and_orthogonalizer(
S_lat,
T_lat,
V_lat,
k_arr,
linear_dep_threshold,
canonical_orth_normalize_diag_first,
)
overlap_label = f"S(k={k_idx}, k_cart={k_arr.round(4).tolist()})"
if n_k <= 16:
report = scf_preflight_overlap_check(
S_k,
plog=plog,
label=overlap_label,
basis=basis,
)
else:
report = check_overlap_matrix(
S_k,
basis=basis,
label=overlap_label,
)
if report.severity != "ok":
prefix = {
"warn": "WARN",
"error": "ERROR",
"critical": "CRITICAL",
}[report.severity]
plog.info(
f"[{prefix}] overlap [{overlap_label}]: "
f"nbf={report.n_basis}, "
f"min eig={report.min_eigenvalue:+.2e}, "
f"cond={report.condition_number:.2e}"
)
plog.write_raw(format_linear_dependence_report(report))
raise_if_severe(report)
overlap_reports.append(report)
if n_occ > n_kept:
raise RuntimeError(
f"run_pbc_bipole_rks: canonical orth at k={k_idx} "
f"dropped too many directions (n_occ={n_occ}, "
f"n_kept={n_kept})"
)
S_k_list.append(S_k)
Hcore_k_list.append(H_k)
X_k_list.append(X_k)
# ---- Nuclear repulsion -------------------------------------------
if ewald_options_1e is not None:
if _support_recorder is None:
e_nuc = float(ewald_nuclear_repulsion(system, ewald_options_1e))
else:
from ._vibeqc_core import ewald_nuclear_repulsion_with_support
_nuclear_support = ewald_nuclear_repulsion_with_support(system, ewald_options_1e)
e_nuc = float(_nuclear_support.energy)
else:
e_nuc = float(nuclear_repulsion_per_cell(system, lat_opts_1e))
plog.info(f"E_nuc per cell = {e_nuc:+.10f} Ha")
# ---- Initial guess -----------------------------------------------
fock_guess_per_k = (
initial_density is None
and guess in (InitialGuess.SAP, InitialGuess.HUECKEL)
)
guess_fock_k = Hcore_k_list
if fock_guess_per_k:
guess_fock_k = list(
periodic_fock_guess_k(
system,
basis,
k_points,
guess,
lattice_opts=lat_opts_2e,
kinetic_lattice=T_lat,
overlap_lattice=S_lat,
)
)
C_per_k: List[np.ndarray] = []
eps_per_k: List[np.ndarray] = []
for F_guess_k, X_k in zip(guess_fock_k, X_k_list):
C_k, eps_k = _diag_in_orth_basis(F_guess_k, X_k)
C_per_k.append(C_k.astype(complex))
eps_per_k.append(eps_k)
n_occ_per_k = [n_occ] * n_k
n_electrons_per_cell = float(n_elec)
def _occupations_from_eps(
eps_per_k_local: Sequence[np.ndarray],
) -> Tuple[List[np.ndarray], float, float]:
if use_gilat:
from .bz_integration import gilat_occupations_for_kmesh
occ_gr, ef_gr = gilat_occupations_for_kmesh(
system,
kmesh,
eps_per_k_local,
n_electrons_per_cell,
spin_degeneracy=2.0,
)
return occ_gr, float(ef_gr), 0.0
# MOM permutes C(k) / eps(k) so its selected occupied states lead;
# the T = 0 fill must occupy that block positionally rather than
# re-pick by energy across the mesh (GitLab #725).
return _closed_shell_periodic_occupations(
eps_per_k_local,
weights,
n_electrons_per_cell,
n_occ,
smearing_T,
fixed_occupied_subspace=bool(use_mom),
)
occ_per_k, fermi_level, entropy = _occupations_from_eps(eps_per_k)
def _fractional_density_needed(
occ_per_k_local: Sequence[np.ndarray],
) -> bool:
"""Whether these occupations need the occupation-driven builder.
The fixed ``C[:, :n_occ]`` slice builder is exact only for the legacy
per-k integer Aufbau pattern. The T = 0 fill is one global Fermi level
over the mesh (#85), so a band-overlap mesh can occupy a different
number of bands per k (or a shared fractional group at mu), which no
fixed slice reproduces; those occupations must be built from
themselves, as the native multi-k GDF route does (#509).
"""
if _occupations_are_per_k_integer_aufbau(occ_per_k_local, n_occ):
return False
reject_bipole_fractional_legacy_gauge(
use_ewald_j_split=bool(use_ewald_j_split),
exchange_split_active=exchange_split_active,
fractional_occupations=True, # runtime-discovered band-overlap fill
driver="run_pbc_bipole_rks",
)
if use_mom or use_oda:
raise NotImplementedError(
"run_pbc_bipole_rks: the zero-temperature Brillouin-zone fill "
"is not per-k-integer-aufbau on this mesh (the bands overlap "
"the Fermi level), so there is no sharp occupied subspace for "
"use_mom/use_oda. Disable use_mom/use_oda or add finite "
"smearing."
)
return True
# Per-k -> real-space density fold over the wide (2x-cutoff) cell list
# is O(n_k x Ncells x Nbf^2) and silent; a prime multi-k cost for the
# LDA (2,2,2) row, so bracket it for localization.
with plog.stage(
"initial_density_fold",
detail=f"per-k SAD/Hcore -> real space over {len(cells_density)} cells",
):
if _fractional_density_needed(occ_per_k):
D_real = real_space_density_from_kpoints_fractional(
C_per_k,
occ_per_k,
kmesh,
cells_density,
)
else:
D_real = real_space_density_from_kpoints(
C_per_k,
n_occ_per_k,
kmesh,
cells_density,
)
if not exchange_split_active and not needs_pair_difference_xc:
_zero_cross_cell_density(D_real, basis.nbasis, n_k)
# Caller-supplied warm-start density takes precedence over the
# SAD/Hcore guess engine. Block ordering matches the canonical
# ``direct_lattice_cells(kmesh)`` ordering -- same contract as the
# RHF driver. Used by the NEB driver for within-image density
# warm-start for periodic NEB.
_constructed_density_k = None
if initial_density_k is not None:
if initial_density is not None:
raise ValueError("supply either lattice or per-k restart densities, not both")
from .guess import periodic_restart_lattice_density
_read_lattice = periodic_restart_lattice_density(
initial_density_k, S_k_list, weights, n_elec, kmesh, D_real.cells,
retained_k_system=system if _chi_xc is not None else None,
)
initial_density = _read_lattice.blocks
if initial_density is not None:
blocks_in = list(initial_density)
if len(blocks_in) != len(D_real.cells):
raise ValueError(
f"run_pbc_bipole_rks: initial_density has "
f"{len(blocks_in)} blocks; expected {len(D_real.cells)}"
)
from .guess import real_lattice_restart_block, normalize_periodic_lattice_restart
for g_idx, block in enumerate(blocks_in):
D_real.set_block(g_idx, real_lattice_restart_block(block, basis.nbasis))
plog.info("initial guess: caller-supplied density (warm-start)")
if _chi_xc is None or initial_density_k is None:
D_real = normalize_periodic_lattice_restart(D_real, S_k_list, weights, n_elec, kmesh)
initial_density_is_local = True
density_from_c_per_k = False
elif fock_guess_per_k:
plog.info(
f"initial guess: {guess.name} "
"(per-k one-particle Hamiltonian diagonalisation)"
)
initial_density_is_local = False
density_from_c_per_k = True
else:
D_engine = initial_density_closed_shell(
system.unit_cell_molecule(),
basis,
n_occ,
InitialGuess.SAD if _patom_seed_pending else guess,
is_periodic=True,
periodic_system=system,
lattice_opts=lat_opts_2e,
# READ restart (Γ-only): prior g=0 cell density (pre-resolved from
# read_from, or read + projected from read_path). Ignored unless READ.
read_density=getattr(opts, "read_density", None),
read_path=getattr(opts, "read_path", ""),
overlap=S_k_list, weights=weights,
)
if D_engine is not None:
plog.info(f"initial guess: {guess.name} (density via GuessEngine)")
if n_k > 1:
# Fold the selected D(k), including its image-cell blocks,
# with the same inverse-Bloch convention as the SCF loop.
# SAD need not be idempotent; it must be one consistent
# density in real and reciprocal space (Sun 2017, Eq. 3).
_constructed_density_k = [
np.asarray(D_engine, dtype=complex).copy() for _ in range(n_k)
]
D_real = real_space_density_from_per_k_density(
_constructed_density_k, kmesh, D_real.cells
)
else:
for g_idx, cell in enumerate(D_real.cells):
is_home = np.all(np.asarray(cell.index) == 0)
D_real.set_block(
g_idx, D_engine if is_home else np.zeros_like(D_engine)
)
else:
plog.info(f"initial guess: {guess.name} (Hcore-diag per k)")
initial_density_is_local = D_engine is not None
density_from_c_per_k = not initial_density_is_local
D_real_prev: Optional[LatticeMatrixSet] = None
# ---- SCF aids ----------------------------------------------------
damping = float(opts.damping)
if not (0.0 <= damping < 1.0):
raise ValueError(
f"run_pbc_bipole_rks: damping must be in [0,1); got {damping}"
)
damper: Optional[DynamicDamping] = None
if bool(getattr(opts, "dynamic_damping", False)):
damper = DynamicDamping(
initial_alpha=damping,
alpha_min=float(getattr(opts, "dynamic_damping_min", 0.0)),
alpha_max=float(getattr(opts, "dynamic_damping_max", 0.95)),
)
use_diis = bool(opts.use_diis)
diis_start_iter = int(opts.diis_start_iter)
accel: Optional[MultiKPeriodicSCFAccelerator] = (
MultiKPeriodicSCFAccelerator(opts) if use_diis else None
)
level_shift_static = float(getattr(opts, "level_shift", 0.0))
if level_shift_schedule is not None and not isinstance(
level_shift_schedule,
LevelShiftSchedule,
):
raise TypeError(
f"level_shift_schedule must be LevelShiftSchedule; "
f"got {type(level_shift_schedule).__name__}"
)
# CRYSTAL-style FMIXING (same convention as RHF BIPOLE driver).
# Value resolved above in the options setup; log it here near the
# SCF-loop preamble.
if fock_mixing_value != 0.0:
plog.info(
f"fock mixing: CRYSTAL FMIXING "
f"{100.0 * fock_mixing_value:.1f}% "
"(previous Fock matrix weight)"
)
F_k_prev_mixed: Optional[List[np.ndarray]] = None
if use_mom:
plog.info("MOM: ON")
C_prev_occ_per_k: Optional[List[np.ndarray]] = None
if use_oda and use_diis:
raise ValueError("use_oda and use_diis are mutually exclusive")
if use_oda:
plog.info(f"ODA: ON (trust l_max = {oda_trust_lambda_max})")
# ---- Ewald J-split cache -----------------------------------------
j_lr_cache = v_ne_lr_cache
if use_ewald_j_split and not use_exact_ewald_j_for_pure_rks:
if system.dim != 3:
raise ValueError("use_ewald_j_split requires dim=3")
if n_k > 1 and _ir_mapping.size == 0:
uniform_w = 1.0 / float(n_k)
if not np.allclose(weights, uniform_w, atol=1e-9):
raise ValueError(
"use_ewald_j_split at multi-k requires uniform "
"full-mesh weights or IBZ-reduced mesh with ir_mapping"
)
from .bipole_fock_ewald import _build_j_long_range_cache
assert omega_used is not None
if _pair_mode:
# M3: the J^LR blocks must land on the pair-resolved output
# template; dedicated cache (never the radial V_ne one).
with plog.stage(
"j_lr_cache",
detail=(
f"Ewald long-range J kernel on the pair-resolved "
f"template (K_max={ewald_k_max} bohr^-1)"
),
):
j_lr_cache = _build_j_long_range_cache(
basis,
system,
np.array(
[
np.asarray(c.r_cart, dtype=float)
for c in _fock_sym_map.cells
],
dtype=float,
),
omega_used,
ewald_precision,
K_max=ewald_k_max, lattice_opts=lat_opts_2e)
cells_r_cart_arr = np.array(
[np.asarray(c.r_cart, dtype=float) for c in cells],
dtype=float,
)
if j_lr_cache is None:
# The reciprocal-space Ewald long-range J kernel precompute is
# one of the heaviest silent pre-SCF stages (can be many
# minutes on a tight lattice / large K_max). Bracket it so a
# stalled job shows ``[j_lr_cache]`` as its last live line.
with plog.stage(
"j_lr_cache",
detail=f"Ewald long-range J kernel (K_max={ewald_k_max} bohr^-1)",
):
j_lr_cache = _build_j_long_range_cache(
basis,
system,
cells_r_cart_arr,
omega_used,
ewald_precision,
K_max=ewald_k_max, lattice_opts=lat_opts_2e)
# Multi-k split: per-(k,k′) q-channel tables for the LR exchange
# (hybrids only -- pure functionals carry no HF exchange). Shares
# the J^LR cache's Ewald w and K_max envelope.
x_lr_cache = None
if exchange_split_active and n_k > 1 and (alpha_hf > 0.0 or _patom_seed_pending):
from .bipole_fock_ewald import build_k_exchange_long_range_cache
assert j_lr_cache is not None and ewald_k_max is not None
# Per-(k,k') q-channel exchange tables: O(n_k^2) channels, the
# heaviest multi-k hybrid setup stage -- bracket for localization.
with plog.stage(
"k_lr_cache",
detail=f"{n_k} q-channels, K_max={ewald_k_max} bohr^-1",
):
x_lr_cache = build_k_exchange_long_range_cache(
basis,
system,
j_lr_cache,
K_max=ewald_k_max,
)
plog.info(
f" K^LR q-channels: {n_k} distinct q = k-k′ shifts on the "
f"shared K_max = {ewald_k_max:.2f} bohr⁻¹ envelope "
f"(a_HF = {alpha_hf:g})"
)
# Incremental/differential J_SR(+K_SR) accumulator (opt-in; see
# ``resolve_incremental_jk``). One accumulator on the total density:
# hybrids build J_SR+K_SR (build_jk), pure functionals build J_SR only
# (build_fock, K=None in the shim).
incremental_jk = resolve_incremental_jk(
use_incremental_fock=use_incremental_fock,
exchange_split_active=exchange_split_active,
use_oda=use_oda,
plog=plog,
)
def _build_fock_for_density(
density: LatticeMatrixSet,
*,
coeffs_for_rho: Optional[Sequence[np.ndarray]],
use_incremental: bool = True,
reseed_incremental: bool = False,
patom_hf_like: bool = False,
xc_density_k=None,
reused_xc=None,
) -> _PBCBipoleRKSFockBuild:
"""Build F^2e(g), add V_xc, and assemble F(k)."""
fb = build_bipole_restricted_fock(
_fock_ctx,
density,
coeffs_for_rho=coeffs_for_rho,
alpha_hf=1.0 if patom_hf_like else alpha_hf,
exchange_assembly=None if patom_hf_like else exx,
_chi_exchange=_chi_xc is not None,
_chi_density_k=xc_density_k if _chi_xc is not None else None,
use_incremental=use_incremental,
reseed_incremental=reseed_incremental,
)
f2e_real = fb.f2e_real
K_corr_per_k = fb.k_corr_per_k
# ---- Build XC potential on the DFT grid ----------------------
xc_result = None
chi_e_xc = None
chi_v_xc = None
if not patom_hf_like and _chi_xc is not None:
if xc_density_k is None:
raise RuntimeError("chi XC requires the actual per-k density")
chi_e_xc, chi_v_xc = _chi_xc.restricted(xc_density_k)
elif not patom_hf_like:
xc_result = reused_xc
if xc_result is None:
D_xc_set = _density_set_gamma_or_lattice(S_lat, density)
xc_result = build_xc_periodic_kpoints(
basis,
system,
grid,
func,
D_xc_set,
kmesh,
lat_opts_2e,
xc_density_domain,
)
# ---- Assemble per-k Fock matrices ----------------------------
f_k_list: List[np.ndarray] = []
F2e_k_all = _bloch_sum_blocks_multi_k(
f2e_real.blocks,
f2e_real.cells,
k_points,
)
xc_v_per_k = None if xc_result is None else xc_result.V_xc
for k_idx, k in enumerate(k_points):
Vxc_k = (np.asarray(chi_v_xc[k_idx]) if chi_v_xc is not None
else 0.0 if xc_result is None
else np.asarray(xc_v_per_k[k_idx]))
F_k = (
F2e_k_all[k_idx]
+ Vxc_k
+ np.asarray(Hcore_k_list[k_idx], dtype=complex)
)
if K_corr_per_k is not None:
# Ewald exchange split: a_HF.(K_LR + G=0) per k (a
# single Γ entry at n_k = 1).
F_k = F_k - 0.5 * K_corr_per_k[k_idx]
F_k = 0.5 * (F_k + F_k.conj().T)
f_k_list.append(F_k)
return _PBCBipoleRKSFockBuild(
f2e_real=f2e_real,
f_k_list=f_k_list,
e_j_short_range=fb.e_j_short_range,
e_j_long_range=fb.e_j_long_range,
e_exchange=fb.e_exchange,
e_hf_exchange_components=dict(getattr(fb, "e_hf_exchange_components", {})),
e_j_multipole=fb.e_j_multipole,
e_2e_k_correction=fb.e_2e_k_correction,
e_exchange_finite_size=fb.e_exchange_finite_size,
e_xc=(float(chi_e_xc) if chi_e_xc is not None else
None if xc_result is None else float(xc_result.e_xc)),
xc_result=xc_result,
n_electrons_grid=(None if xc_result is None else
float(xc_result.n_electrons_grid)),
periodic_density_bras=(False if xc_result is None else
bool(xc_result.periodic_density_bras)),
screened_exchange_execution=fb.screened_exchange_execution,
output_cell_farming_execution=(
fb.output_cell_farming_execution
),
)
# ---- Retired multipole far-field config -----------------------------
from .bipole_fock_multipole import ( # noqa: E402
BipoleMultipoleConfig,
resolve_multipole_config,
)
# Explicit requests fail before setup. Keep a disabled config here for
# the shared Fock context until the research-only implementation is
# removed from that internal API.
_ff_enable: Optional[bool] = False
_mp_config = resolve_multipole_config(
system,
basis,
lat_opts_2e,
user_enable=_ff_enable,
multipole_l_max=multipole_l_max,
)
if _mp_config.enabled:
plog.info(
f" BIPOLE multipole far-field: ENABLED "
f"(L_max={_mp_config.L_max}, R_bipole={_mp_config.R_bipole:.1f} bohr, "
f"n_cells={len(_mp_config.cache.cells) if _mp_config.cache else 0})"
)
else:
plog.info(
f" BIPOLE multipole far-field: off "
f"(R_bipole={_mp_config.R_bipole:.1f} bohr, "
f"cutoff={lat_opts_2e.cutoff_bohr:.1f} bohr)"
)
# SYM3b reduction is automatic for attached-symmetry crystals on the
# corrected split; explicit choices were resolved before the density list.
# ---- Disabled quartet-level far-field infrastructure ----------------
from .bipole_far_field_infrastructure import (
build_bipolar_far_field_infrastructure,
)
_spherical_buffer, _penetration_dispatch_j, _penetration_dispatch_k, _tensor_cache, _fock_kernel, _sym_recon = (
build_bipolar_far_field_infrastructure(
system,
basis,
lat_opts_2e,
use_multipole_far_field=(
_ff_enable and exchange_split_active
),
exchange_split_active=exchange_split_active,
multipole_l_max=multipole_l_max,
ewald_omega=omega_used if use_ewald_j_split else 0.0,
plog=plog,
)
)
# ---- Shared restricted Fock-build context (M2 unification) ----------
# The RKS wrapper adds V_xc after the shared two-electron builder returns.
# _build_fock_for_density (above) references this via late binding; it is
# only called from the SCF loop below, after this point.
from .bipole_domain import capture_bipole_domain
_resolved_domain = capture_bipole_domain(
lattice_1e=lat_opts_1e, lattice_2e=lat_opts_2e,
requested_lattice=opts.lattice_opts,
ewald_options=ewald_options_1e, alpha=omega_used,
reciprocal_cutoff=ewald_k_max, ewald_precision=ewald_precision,
sr_image_extent=_sr_image_extent, use_ewald_j_split=use_ewald_j_split,
exchange_ewald_split=exchange_split_active,
exchange_exxdiv=exchange_exxdiv, exact_zone=_exact_zone,
ks_options=opts,
v_ne_grid_options=v_ne_grid_options,
xc_density_domain=xc_density_domain,
exact_j_ke_cutoff=(exact_j_ke_cutoff if use_exact_ewald_j_for_pure_rks else None),
nuclear_erfc_options=_v_ne_erfc_options,
)
_fock_ctx = BipoleFockContext(
resolved_domain=_resolved_domain,
basis=basis,
system=system,
lat_opts_2e=lat_opts_2e,
use_ewald_j_split=use_ewald_j_split,
exchange_split_active=exchange_split_active,
n_k=n_k,
omega_used=omega_used,
ewald_precision=ewald_precision,
ewald_cell_volume=ewald_cell_volume,
n_elec=n_elec,
xi_madelung=_xi_madelung,
j_lr_cache=j_lr_cache,
x_lr_cache=x_lr_cache,
incremental_jk=incremental_jk,
rep_cell_indices=_rep_cell_indices,
fock_sym_map=_fock_sym_map,
mp_config=_mp_config,
spherical_moment_buffer=_spherical_buffer,
penetration_dispatch_j=_penetration_dispatch_j,
penetration_dispatch_k=_penetration_dispatch_k,
quartet_tensor_cache=_tensor_cache,
far_field_fock_kernel=_fock_kernel,
symmetry_reconstruction_map=_sym_recon,
s_lat=S_lat,
s_k_list=S_k_list,
k_points=k_points,
weights=weights,
k_points_full=k_points_full,
weights_full=weights_full,
ir_mapping=_ir_mapping,
bvk_mesh=_bvk_mesh,
n_occ=n_occ,
plog=plog,
sr_image_extent=_sr_image_extent,
exact_zone_bohr=_exact_zone,
exact_j_for_pure_rks=use_exact_ewald_j_for_pure_rks,
exact_j_ke_cutoff=exact_j_ke_cutoff,
exact_j_chunk_size=exact_j_chunk_size,
output_cell_farming_task_kind=(
output_cell_farming_task_kind if farm_output_cells else None
),
output_cell_farming_strategy=output_cell_farming_strategy,
)
def _split_k_density_list(density: LatticeMatrixSet) -> List[np.ndarray]:
return split_k_density_list(_fock_ctx, density)
def _per_k_density_list(density) -> List[np.ndarray]:
"""Per-k density for every consumer, split or not (#503).
The stored real-space density is BvK-periodic on a list wider
than the torus, so an unprojected Bloch sum over the list
overcounts by about n_cells / N_k (see run_pbc_bipole_rhf).
Fold on the torus whenever the list covers it; an ad-hoc k list and
a list smaller than the torus keep the unprojected sum, as before.
"""
if n_k == 1 or (
_bvk_mesh is not None and bvk_torus_covered(density, _bvk_mesh)
):
return _split_k_density_list(density)
return _bloch_sum_blocks_multi_k(density.blocks, density.cells, k_points)
if _patom_seed_pending:
patom_fock = _build_fock_for_density(
D_real, coeffs_for_rho=None, use_incremental=False,
patom_hf_like=True,
)
C_per_k, eps_per_k = [], []
for F_k, X_k in zip(patom_fock.f_k_list, X_k_list):
C_k, eps_k = _diag_in_orth_basis(F_k, X_k)
C_per_k.append(C_k.astype(complex))
eps_per_k.append(eps_k)
occ_per_k, fermi_level, entropy = _occupations_from_eps(eps_per_k)
if _fractional_density_needed(occ_per_k):
D_real = real_space_density_from_kpoints_fractional(
C_per_k, occ_per_k, kmesh, cells_density,
)
else:
D_real = real_space_density_from_kpoints(
C_per_k, [n_occ] * n_k, kmesh, cells_density,
)
if not exchange_split_active and not needs_pair_difference_xc:
_zero_cross_cell_density(D_real, basis.nbasis, n_k)
_constructed_density_k = [
(C * occ) @ C.conj().T for C, occ in zip(C_per_k, occ_per_k)
]
D_real_prev = None
initial_density_is_local = False
density_from_c_per_k = True
plog.banner("SCF (PBC BIPOLE RKS, direct-space)")
plog.info(" iter energy (Ha) dE ||[F,DS]|| DIIS")
scf_trace: List[SCFIteration] = []
energy_components: List[PBCBipoleEnergyComponents] = []
def _exact_free_energy(density, fb, e_dft_plus_u_value, entropy_value):
"""Free energy of ``density`` on the exact build ``fb`` (#116).
Assembled exactly as the post-loop finalisation assembles the
returned value (``fb.e_xc`` is the XC energy of the quadrature whose
V_xc entered ``fb``), so the in-loop confirmation and the terminal
refresh judge the same number.
"""
e_kin = _lattice_contract(density, T_lat, operator_name="T")
e_ne = _lattice_contract(density, V_lat, operator_name="V_ne")
e_2e = (
0.5 * _lattice_contract(density, fb.f2e_real, operator_name="F2e")
+ fb.e_2e_k_correction
)
assert fb.e_xc is not None
e_tot = (
float(e_kin + e_ne + e_2e + float(fb.e_xc))
+ e_nuc
+ float(e_dft_plus_u_value)
)
if system.dim == 3 and not exchange_split_active:
e_tot += compute_ext_el_spheropole(density, basis, system, lat_opts)
return float(e_tot) - smearing_T * float(entropy_value)
E_prev = 0.0
F_k_list: List[np.ndarray] = [np.zeros_like(H) for H in Hcore_k_list]
E_elec = 0.0
e_xc = 0.0
e_dft_plus_u = 0.0
converged = False
iter_idx = 0
def _canonical_physical_orbitals(fock_matrices):
"""Canonical bands and occupations without replacing the evaluated density."""
C_per_k = []
eps_per_k = []
for F_k, X_k in zip(fock_matrices, X_k_list):
C_k, eps_k = _diag_in_orth_basis(F_k, X_k)
C_per_k.append(np.asarray(C_k, dtype=complex))
eps_per_k.append(np.asarray(eps_k, dtype=float))
if use_mom and C_prev_occ_per_k is not None:
for idx in range(n_k):
C_k = C_per_k[idx]
eps_k = eps_per_k[idx]
sel = _mom_select(
C_k,
S_k_list[idx],
C_prev_occ_per_k[idx],
n_occ,
eps_new=eps_k,
)
virt_mask = np.ones(C_k.shape[1], dtype=bool)
virt_mask[sel] = False
virt_sel = np.where(virt_mask)[0]
virt_sel = virt_sel[np.argsort(np.real(eps_k[virt_sel]))]
order = np.concatenate([sel, virt_sel])
C_per_k[idx] = C_k[:, order]
eps_per_k[idx] = eps_k[order]
# These occupations describe the canonical F[D] band output, one update
# ahead of the committed density on a capped return. Preserve that
# density's entropy for E[D] - T*S[D] and the exact terminal check (#383).
occ_per_k, fermi_level, orbital_entropy = _occupations_from_eps(eps_per_k)
return C_per_k, eps_per_k, occ_per_k, fermi_level, orbital_entropy
#: Exact rebuild from the convergence confirmation (IID 514); reused by
#: the post-loop finalisation so healthy rows pay nothing extra.
_confirm_fb: Optional[_PBCBipoleRKSFockBuild] = None
_confirm_orbitals = None
_confirm_e_dft_plus_u: Optional[float] = None
# One-shot quadrature reuse after a refused exact confirmation.
_pending_xc = None
# ---- DFT+U setup (closed-shell BIPOLE RKS +U) ------------------------
dft_plus_u_sites_cxx: List = []
dft_plus_u_ao_groups: List[List[int]] = []
if dft_plus_u:
from ._vibeqc_core import _HubbardSiteCxx
from .dft_plus_u import ao_group_indices
ao_groups_map = ao_group_indices(basis)
for site in dft_plus_u:
key = (site.atom_index, site.l)
if key not in ao_groups_map:
raise ValueError(
f"run_pbc_bipole_rks: HubbardSite{key} has no AOs "
f"in the basis. Available channels: "
f"{sorted(ao_groups_map.keys())}"
)
dft_plus_u_sites_cxx.append(
_HubbardSiteCxx(site.atom_index, site.l, site.U_eff_hartree)
)
dft_plus_u_ao_groups.append(ao_groups_map[key])
def _apply_dft_plus_u(
density: LatticeMatrixSet,
fock_k_list: List[np.ndarray],
density_k=None,
) -> float:
"""Apply +U energy/potential to the density used for this Fock."""
if not dft_plus_u_sites_cxx:
return 0.0
from ._vibeqc_core import _compute_dft_plus_u_multi_k_per_spin_cxx
if density_k is not None:
P_k_all = density_k
else:
P_k_all = _per_k_density_list(density)
P_sigma_k_for_u = [
0.25 * (P_k + P_k.conj().T) for P_k in P_k_all
]
e_sigma, v_ao = _compute_dft_plus_u_multi_k_per_spin_cxx(
dft_plus_u_sites_cxx,
dft_plus_u_ao_groups,
S_k_list,
P_sigma_k_for_u,
list(weights),
)
v_ao = np.asarray(v_ao, dtype=complex)
for k_idx, S_k in enumerate(S_k_list):
fock = fock_k_list[k_idx] + S_k @ v_ao @ S_k
fock_k_list[k_idx] = 0.5 * (fock + fock.conj().T)
return 2.0 * float(e_sigma)
# Preserve the actual complex Bloch density through every linear mix.
# Reconstructing it from a finite real-space lattice can alias k channels.
_chi_density = _chi_previous = None
if _chi_xc is not None:
if initial_density_k is not None:
from .guess import normalize_density_k_guess
_chi_density = np.asarray(normalize_density_k_guess(
initial_density_k, S_k_list, weights, n_elec))
elif _constructed_density_k is not None:
_chi_density = np.asarray(_constructed_density_k)
elif density_from_c_per_k:
_chi_density = np.asarray([
(c * o) @ c.conj().T for c, o in zip(C_per_k, occ_per_k)])
else:
_chi_density = np.asarray(_split_k_density_list(D_real))
for iter_idx in range(1, int(opts.max_iter) + 1):
if damper is not None:
damping = damper.alpha
diis_active = use_diis and iter_idx >= diis_start_iter
D_used = D_real
if iter_idx > 1 and damping > 0.0 and not diis_active:
D_used = _damp_lattice_matrix(D_real, D_real_prev, damping)
d_used_is_damped = iter_idx > 1 and damping > 0.0 and not diis_active
_chi_used = _chi_density
if _chi_xc is not None and d_used_is_damped:
_chi_used = (1.0 - damping) * _chi_density + damping * _chi_previous
d_used_from_coeffs = (
density_from_c_per_k
and not (initial_density_is_local and iter_idx == 1)
and not d_used_is_damped
)
# Damping mutates lattice sets in place: reject it explicitly,
# in addition to checking the committed density object(s).
# Consume once; never carry a result across another trial density.
reused_xc = None
if not d_used_is_damped and _pending_xc is not None and (
D_used is _pending_xc[0]):
reused_xc = _pending_xc[-1]
_pending_xc = None
fock_build = _build_fock_for_density(
D_used,
reused_xc=reused_xc,
xc_density_k=_chi_used,
coeffs_for_rho=(C_per_k if d_used_from_coeffs else None),
)
F2e_real = fock_build.f2e_real
F_k_list = fock_build.f_k_list
# ---- DFT+U: per-spin per-k Fock contribution (closed-shell).
# Same pattern as run_pbc_bipole_rhf -- closed-shell uses
# P_s = P_total/2 and E_U_total = 2 x E_s.
constructed_density_k = _constructed_density_k if iter_idx == 1 else None
e_dft_plus_u = _apply_dft_plus_u(
D_used, F_k_list, density_k=constructed_density_k
)
# ---- XC energy -----------------------------------------------
# From the SAME quadrature that produced this iteration's V_xc
# (inside _build_fock_for_density on D_used) -- the loop used to
# run a second, redundant build_xc_periodic_kpoints here every iteration
# just to extract e_xc.
assert fock_build.e_xc is not None
e_xc = float(fock_build.e_xc)
# ---- Energy --------------------------------------------------
E_kin = _lattice_contract(D_used, T_lat, operator_name="T")
E_ne = _lattice_contract(D_used, V_lat, operator_name="V_ne")
E_2e = (
0.5
* _lattice_contract(
D_used,
F2e_real,
operator_name="F2e",
)
# k-space exchange correction (Ewald exchange split).
+ fock_build.e_2e_k_correction
)
E_elec = E_kin + E_ne + E_2e + e_xc
grad_norm_sum = 0.0
error_k_list: List[np.ndarray] = []
D_k_list: List[np.ndarray] = []
D_k_split_guess: Optional[List[np.ndarray]] = None
if exchange_split_active and initial_density_is_local and iter_idx == 1:
# BvK representative for local AND Bloch warm-start storage
# (home block at Γ; exact torus fold at multi-k -- S_g over
# the unprojected fold would overcount).
D_k_split_guess = _split_k_density_list(D_used)
D_k_guess_fold: Optional[List[np.ndarray]] = None
if (
initial_density_is_local
and iter_idx == 1
and D_k_split_guess is None
and _constructed_density_k is None
):
D_k_guess_fold = _per_k_density_list(D_used)
for idx in range(n_k):
if _chi_xc is not None:
D_k = _chi_used[idx]
elif initial_density_is_local and iter_idx == 1:
if _constructed_density_k is not None:
D_k = _constructed_density_k[idx]
elif D_k_split_guess is not None:
D_k = D_k_split_guess[idx]
else:
assert D_k_guess_fold is not None
D_k = D_k_guess_fold[idx]
D_k = 0.5 * (D_k + D_k.conj().T)
else:
C_k = C_per_k[idx]
if _occupations_are_per_k_integer_aufbau(occ_per_k, n_occ):
C_occ = C_k[:, :n_occ]
D_k = 2.0 * (C_occ @ C_occ.conj().T)
else:
# Fractional-occupation density: D(k) = S_i n_i C_i C_i+
occ = np.asarray(occ_per_k[idx], dtype=float)
C_full = np.asarray(C_k, dtype=np.complex128)
D_k = (C_full * occ[None, :]) @ C_full.conj().T
D_k_list.append(D_k)
F_k = F_k_list[idx]
w = float(weights[idx])
S_k = S_k_list[idx]
FDS = F_k @ D_k @ S_k
grad = FDS - FDS.conj().T
error_k_list.append(grad)
grad_norm_sum += w * float(np.linalg.norm(grad))
E_total = float(E_elec) + e_nuc + e_dft_plus_u
# EXT EL-SPHEROPOLE -- a 3D-Ewald-gauge correction, identically
# zero in the direct (non-Ewald) gauge used for dim<3, and
# OMITTED under the corrected gauge (double-count -- see the
# RHF driver note).
if system.dim == 3 and not exchange_split_active:
E_sphero = compute_ext_el_spheropole(D_used, basis, system, lat_opts)
E_total += E_sphero
else:
E_sphero = None
free_energy = E_total - smearing_T * entropy
dE = free_energy - E_prev if iter_idx > 1 else 0.0
check_scf_divergence(
"run_pbc_bipole_rks",
iter_idx,
free_energy,
grad_norm_sum,
dE,
)
scf_trace.append(
SCFIteration(
iter=iter_idx,
energy=float(free_energy),
delta_e=float(dE if iter_idx > 1 else 0.0),
grad_norm=float(grad_norm_sum),
diis_subspace=(accel.subspace_size if accel is not None else 0),
)
)
plog.iteration(
iter_idx,
energy=float(free_energy),
dE=float(dE if iter_idx > 1 else 0.0),
grad=float(grad_norm_sum),
diis=(accel.subspace_size if accel is not None else 0),
)
energy_components.append(
PBCBipoleEnergyComponents(
iter=int(iter_idx),
e_total=float(E_total),
e_electronic=float(E_elec),
e_kinetic=float(E_kin),
e_nuclear_attraction=float(E_ne),
e_two_electron=float(E_2e),
e_nuclear_repulsion=float(e_nuc),
e_j_short_range=fock_build.e_j_short_range,
e_j_long_range=fock_build.e_j_long_range,
e_exchange=fock_build.e_exchange,
e_exchange_finite_size=fock_build.e_exchange_finite_size,
e_ext_el_spheropole=E_sphero,
e_dft_plus_u=float(e_dft_plus_u),
)
)
plog.energy_decomposition(
iter_idx,
E_kin=float(E_kin),
E_ne=float(E_ne),
E_2e=float(E_2e),
E_elec=float(E_elec),
E_nuc=float(e_nuc),
)
physical_fock_stationary = (
grad_norm_sum < float(opts.conv_tol_grad)
)
converged = (
iter_idx > 1
and abs(dE) < float(opts.conv_tol_energy)
and physical_fock_stationary
)
# SCF-accelerator extrapolation. Full
# {DIIS, KDIIS, EDIIS, EDIIS_DIIS, ADIIS} family wired here;
# bridged modes route through the M2e stacked-real-block bridge
# (see ``per_k_to_stacked_real_blocks`` in
# ``periodic_scf_accelerators.py``).
if accel is not None:
if _chi_xc is not None:
density_k_list = _chi_used
elif constructed_density_k is not None:
density_k_list = constructed_density_k
elif exchange_split_active:
# Unprojected fold: BvK representative per k (home
# block at Γ; exact torus fold at multi-k).
density_k_list = _split_k_density_list(D_used)
else:
density_k_list = _per_k_density_list(D_used)
F_ex_list = accel.extrapolate_rhf(
F_k_list,
error_k_list=error_k_list,
density_k_list=density_k_list,
energy=free_energy,
mo_coeffs_k_list=C_per_k,
n_occ=n_occ,
weights=list(weights),
cells=cells,
kpoints=list(k_points),
)
# Once the physical Fock F(D) commutes with D, diagonalise that
# Fock -- not the extrapolated one -- even if the lagging energy
# difference still needs one confirmation iteration. Reapplying
# DIIS at a stationary density can kick the SCF away before the
# energy test catches up; a near-machine-zero error history also
# makes the Pulay B-matrix singular. Fractional occupations still
# pass through the density fixed-point check below after this
# physical-Fock update. See the RHF twin in pbc_bipole.py and
# tests/test_pbc_bipole_diis_converged_basin.py.
if diis_active and not physical_fock_stationary:
F_k_list = F_ex_list
# A small commutator does not imply density closure. Try the bare
# physical Fock first, preserving canonicalization at a fixed point.
# If that trial fails closure, use the already computed acceleration
# once and re-evaluate the new density on the next SCF iteration.
# No second Fock/XC build or accelerator-history update is needed.
_provisional_converged = converged
for _density_attempt in range(2):
# --- FMIXING (CRYSTAL-style, after DIIS, before level-shift)
# Skipped once the physical Fock is stationary (see DIIS note above).
if fock_mixing_value != 0.0 and not physical_fock_stationary:
if F_k_prev_mixed is not None:
F_mixed_list: List[np.ndarray] = []
for idx in range(n_k):
F_mixed = (1.0 - fock_mixing_value) * F_k_list[
idx
] + fock_mixing_value * F_k_prev_mixed[idx]
F_mixed = 0.5 * (F_mixed + F_mixed.conj().T)
F_mixed_list.append(F_mixed)
F_k_list = F_mixed_list
F_k_prev_mixed = [np.asarray(F, dtype=complex).copy() for F in F_k_list]
# Level shift
if level_shift_schedule is not None:
level_shift_b = level_shift_schedule.at(iter_idx)
else:
level_shift_b = level_shift_static
if level_shift_b != 0.0 and not physical_fock_stationary:
F_for_diag: List[np.ndarray] = []
for idx in range(n_k):
D_k = D_k_list[idx]
S_k = S_k_list[idx]
F_shift = (
F_k_list[idx]
+ level_shift_b * S_k
- (level_shift_b / 2.0) * (S_k @ D_k @ S_k)
)
F_shift = 0.5 * (F_shift + F_shift.conj().T)
F_for_diag.append(F_shift)
else:
F_for_diag = F_k_list
# Diagonalise
new_C_per_k = []
new_eps_per_k = []
for idx in range(n_k):
C_k, eps_k = _diag_in_orth_basis(
F_for_diag[idx],
X_k_list[idx],
)
new_C_per_k.append(C_k)
new_eps_per_k.append(eps_k)
# MOM
if use_mom and C_prev_occ_per_k is not None:
for idx in range(n_k):
C_k = new_C_per_k[idx]
eps_k = new_eps_per_k[idx]
S_k = S_k_list[idx]
sel = _mom_select(
C_k,
S_k,
C_prev_occ_per_k[idx],
n_occ,
eps_new=eps_k,
)
n_kept_idx = C_k.shape[1]
virt_mask = np.ones(n_kept_idx, dtype=bool)
virt_mask[sel] = False
virt_sel = np.where(virt_mask)[0]
virt_sel = virt_sel[np.argsort(np.real(eps_k[virt_sel]))]
order = np.concatenate([sel, virt_sel])
new_C_per_k[idx] = C_k[:, order]
new_eps_per_k[idx] = eps_k[order]
C_per_k = new_C_per_k
eps_per_k = new_eps_per_k
occ_per_k, fermi_level, entropy = _occupations_from_eps(eps_per_k)
_fractional_occupations_here = _fractional_density_needed(occ_per_k)
if _fractional_occupations_here:
D_real_new = real_space_density_from_kpoints_fractional(
C_per_k,
occ_per_k,
kmesh,
cells_density,
)
else:
D_real_new = real_space_density_from_kpoints(
C_per_k,
[n_occ] * n_k,
kmesh,
cells_density,
)
# Corrected gauge: the full Bloch fold IS the density the
# builders need (see run_pbc_bipole_rhf); legacy keeps the
# Γ-locality projection.
if not exchange_split_active and not needs_pair_difference_xc:
_zero_cross_cell_density(D_real_new, basis.nbasis, n_k)
_chi_new = None
if _chi_xc is not None:
_chi_new = np.asarray([
(c * o) @ c.conj().T for c, o in zip(C_per_k, occ_per_k)])
if converged and _chi_xc is not None:
converged = float(np.max(np.abs(_chi_new - _chi_used))) < float(opts.conv_tol_grad)
# Require the newly diagonalised density to be the same fixed point as
# D_used before accepting convergence. This also catches the
# occupation-only motion that a finite-temperature commutator cannot
# see.
if converged:
density_fixed_point_residual = max(
(
float(
np.max(
np.abs(
np.asarray(new_block)
- np.asarray(old_block)
)
)
)
for new_block, old_block in zip(
D_real_new.blocks, D_used.blocks
)
),
default=0.0,
)
converged = density_fixed_point_residual < float(
opts.conv_tol_grad
)
if (
_density_attempt == 0
and _provisional_converged
and not converged
and physical_fock_stationary
and diis_active
and accel is not None
):
plog.info(
" physical-Fock density closure failed; retrying the "
"orbital update with SCF acceleration"
)
F_k_list = F_ex_list
# Keep the commutator-based mixing/level-shift guards.
# Reintroducing a shift here would also change fractional
# occupations, rather than just retrying acceleration.
# An accelerated candidate must be evaluated as an SCF
# input before it can enter exact convergence confirmation.
converged = False
continue
break
if use_oda:
fock_naive = _build_fock_for_density(
D_real_new,
xc_density_k=_chi_new,
coeffs_for_rho=C_per_k,
use_incremental=False, # off the per-iter ΔD chain
)
oda_step = _compute_oda_lambda(
D_used,
D_real_new,
F_k_list,
fock_naive.f_k_list,
[np.asarray(k) for k in k_points],
weights,
trust_lambda_max=oda_trust_lambda_max,
)
_oda_mix(D_used, D_real_new, oda_step.lam)
D_real_prev = D_real
D_real = D_used
density_from_c_per_k = oda_step.lam == 1.0
plog.info(f" ODA: l = {oda_step.lam:.4f}")
else:
D_real_prev = D_used
D_real = D_real_new
density_from_c_per_k = True
if _chi_xc is not None:
_chi_previous = _chi_density if use_oda else _chi_used
_chi_density = ((1.0 - oda_step.lam) * _chi_used + oda_step.lam * _chi_new
if use_oda else _chi_new)
if use_mom:
C_prev_occ_per_k = [
np.asarray(C_per_k[idx][:, :n_occ]).copy() for idx in range(n_k)
]
if damper is not None:
damper.update(free_energy)
E_prev = free_energy
if converged:
# IID 514: confirm the provisional exit on the EXACT operator at
# the density just committed. The loop's gate measured F(D_prev)
# on D_prev; at a marginal fixed point the true commutator
# F(D_new) on D_new can still sit above the gate by one
# iteration's step, and the post-loop terminal check would then
# revoke the convergence with no way back. Confirming here lets
# the loop run the extra iteration(s) it needs; the rebuild is
# reused by the post-loop finalisation, so healthy rows pay
# nothing extra.
# #115: announce the cold exact rebuild and make the loop's
# provisional state durable BEFORE it runs; on a converged run
# this is the expensive post-SCF phase, and the post-loop
# finalisation reuses it at no cost.
with bipole_confirmation_phase(
plog,
iter_idx=iter_idx,
energy=float(scf_trace[-1].energy),
n_k=n_k,
nbf=basis.nbasis,
):
_confirm_fb = _build_fock_for_density(
D_real,
xc_density_k=_chi_density,
coeffs_for_rho=(C_per_k if density_from_c_per_k else None),
use_incremental=False,
# #116: re-sync the incremental chain to this exact build,
# so a failed confirmation continues on the operator it is
# judged by (a no-op when the loop exits here).
reseed_incremental=True,
)
# Dudarev et al., Phys. Rev. B 57, 1505 (1998), Eq. (6):
# the exact one-electron operator includes U_eff(1/2 - n).
_confirm_e_dft_plus_u = _apply_dft_plus_u(
D_real,
_confirm_fb.f_k_list,
)
_confirm_grad = restricted_bipole_commutator_norm(
_confirm_fb.f_k_list,
(
_chi_density if _chi_xc is not None else
_per_k_density_list(D_real)
),
S_k_list,
weights,
)
# #116: judge the exact operator's free energy at the committed
# density here too -- the delta the post-loop refresh writes into
# the terminal trace row -- so the loop cannot exit on a state
# the terminal check would refuse.
_confirm_ok, _ = bipole_terminal_check(
plog,
phase="in_loop",
iter_idx=iter_idx,
loop_objective=float(scf_trace[-1].energy),
exact_objective=_exact_free_energy(
D_real, _confirm_fb, _confirm_e_dft_plus_u, entropy
),
loop_grad_norm=float(scf_trace[-1].grad_norm),
exact_grad_norm=float(_confirm_grad),
conv_tol_energy=float(opts.conv_tol_energy),
conv_tol_grad=float(opts.conv_tol_grad),
)
converged = _confirm_ok and abs(dE) < float(opts.conv_tol_energy)
if converged and _chi_xc is None:
# The cold build can change canonical orbitals enough to fail
# fixed-point closure even when its commutator passes. Judge
# its candidate density without replacing the evaluated state.
# Finalization reuses these bands and their occupations.
_confirm_orbitals = _canonical_physical_orbitals(_confirm_fb.f_k_list)
_confirm_coeffs, _, _confirm_occ, _, _ = _confirm_orbitals
if _fractional_density_needed(_confirm_occ):
_confirm_density = real_space_density_from_kpoints_fractional(
_confirm_coeffs, _confirm_occ, kmesh, cells_density,
)
else:
_confirm_density = real_space_density_from_kpoints(
_confirm_coeffs, [n_occ] * n_k, kmesh, cells_density,
)
if not exchange_split_active and not needs_pair_difference_xc:
_zero_cross_cell_density(_confirm_density, basis.nbasis, n_k)
_confirm_density_residual = max(
(float(np.max(np.abs(np.asarray(new) - np.asarray(old))))
for new, old in zip(_confirm_density.blocks, D_real.blocks)),
default=0.0,
)
converged = _confirm_density_residual < float(opts.conv_tol_grad)
if not converged:
plog.info(
" exact-operator density closure failed: "
f"{_confirm_density_residual:.3e} >= {float(opts.conv_tol_grad):.3e}; "
"continuing from the exact Fock"
)
# At the iteration cap this is already the exact final-density
# operator, even if confirmation failed. Preserve it for reuse.
if not converged and iter_idx < int(opts.max_iter):
# Preserve provider callback semantics and the separate chi
# density path. Native XC depends only on the fixed grid,
# functional, domain and this committed density.
if not uses_external_xc and _confirm_fb.xc_result is not None:
_pending_xc = (D_real, _confirm_fb.xc_result)
_confirm_fb = None
_confirm_orbitals = None
_confirm_e_dft_plus_u = None
if converged:
break
screened_exchange_execution = fock_build.screened_exchange_execution
final_e_xc = float(e_xc)
final_e_2e = float(E_2e)
final_e_exchange = float(fock_build.e_exchange or 0.0)
# ---- Post-loop: evaluate the exact density returned to callers. The
# final diagonalisation commits a new density even on max-iteration
# exhaustion, so this rebuild is required for both success and failure.
with bipole_final_density_phase(
plog,
iter_idx=iter_idx,
energy=E_total,
converged=converged,
n_k=n_k,
nbf=basis.nbasis,
reused=(_confirm_fb is not None),
):
_fb = (
_confirm_fb
if _confirm_fb is not None
else _build_fock_for_density(
D_real,
xc_density_k=_chi_density,
coeffs_for_rho=(C_per_k if density_from_c_per_k else None),
use_incremental=False,
)
)
F_k_list = _fb.f_k_list
if _confirm_fb is not None:
assert _confirm_e_dft_plus_u is not None
e_dft_plus_u = _confirm_e_dft_plus_u
else:
e_dft_plus_u = _apply_dft_plus_u(D_real, F_k_list)
orbital_entropy = entropy
if _chi_xc is not None:
C_per_k, eps_per_k = bipole_unshifted_orbitals(F_k_list, C_per_k, occ_per_k)
else:
# Canonicalize the exact physical operator at the authoritative returned
# density. Loop-local eigenpairs may belong to the preceding density or
# to a DIIS/Fock-mixed operator, especially on a max-iteration return.
if _confirm_orbitals is not None:
C_per_k, eps_per_k, occ_per_k, fermi_level, orbital_entropy = _confirm_orbitals
else:
C_per_k, eps_per_k, occ_per_k, fermi_level, orbital_entropy = (
_canonical_physical_orbitals(F_k_list)
)
screened_exchange_execution = _fb.screened_exchange_execution
# The exact final-density Fock already carries this quadrature energy.
assert _fb.e_xc is not None
final_e_xc = float(_fb.e_xc)
E_kin_f = _lattice_contract(D_real, T_lat, operator_name="T")
E_ne_f = _lattice_contract(D_real, V_lat, operator_name="V_ne")
E_2e_f = (
0.5 * _lattice_contract(D_real, _fb.f2e_real, operator_name="F2e")
+ _fb.e_2e_k_correction
)
E_elec = E_kin_f + E_ne_f + E_2e_f + final_e_xc
final_e_2e = float(E_2e_f)
final_e_exchange = float(_fb.e_exchange or 0.0)
E_total = float(E_elec) + e_nuc + e_dft_plus_u
# Fresh E_total doesn't include spheropole -- add it (3D only; the
# term is zero in the direct gauge used for dim<3, and omitted
# under the corrected gauge).
if system.dim == 3 and not exchange_split_active:
E_sphero_final = compute_ext_el_spheropole(D_real, basis, system, lat_opts)
E_total += E_sphero_final
else:
E_sphero_final = None
energy_components[-1] = PBCBipoleEnergyComponents(
iter=int(iter_idx),
e_total=float(E_total),
e_electronic=float(E_elec),
e_kinetic=float(E_kin_f),
e_nuclear_attraction=float(E_ne_f),
e_two_electron=float(E_2e_f),
e_nuclear_repulsion=float(e_nuc),
e_bielet_zone_ee=(
None if use_ewald_j_split else float(E_2e_f)
),
e_ext_el_spheropole=E_sphero_final,
e_j_short_range=_fb.e_j_short_range,
e_j_long_range=_fb.e_j_long_range,
e_exchange=_fb.e_exchange,
e_exchange_finite_size=_fb.e_exchange_finite_size,
e_j_multipole=_fb.e_j_multipole,
e_dft_plus_u=float(e_dft_plus_u),
)
# ---- Ionic-Γ basin-health diagnostic (smearing straddle) -----------
# Evaluate the diagnostic from the same final canonical eigenpairs and
# occupations that are returned to callers.
basin_warning = smearing_basin_warning(
smearing_T,
[(eps_per_k, occ_per_k, n_occ, 2.0)],
orbital_entropy,
"run_pbc_bipole_rks",
)
if basin_warning is not None:
plog.info(" WARNING: " + basin_warning)
warnings.warn(basin_warning, UserWarning, stacklevel=2)
free_energy_final = E_total - smearing_T * entropy
final_grad_norm = restricted_bipole_commutator_norm(
F_k_list,
(
_chi_density if _chi_xc is not None else
_per_k_density_list(D_real)
),
S_k_list,
weights,
)
_loop_objective = (
float(scf_trace[-1].energy) if scf_trace else float(free_energy_final)
)
_loop_grad = (
float(scf_trace[-1].grad_norm) if scf_trace else float(final_grad_norm)
)
refresh_bipole_terminal_trace(
scf_trace,
float(free_energy_final),
grad_norm=final_grad_norm,
)
_terminal_ok, _ = bipole_terminal_check(
plog,
phase="post_loop",
iter_idx=iter_idx,
loop_objective=_loop_objective,
exact_objective=float(free_energy_final),
loop_grad_norm=_loop_grad,
exact_grad_norm=float(final_grad_norm),
conv_tol_energy=float(opts.conv_tol_energy),
conv_tol_grad=float(opts.conv_tol_grad),
)
if converged:
converged = _terminal_ok
acceptance = None
if _chi_xc is not None:
final_count = bipole_electron_count(_chi_density, S_k_list, weights)
acceptance = bipole_hf_acceptance(
converged=converged, iterations=iter_idx, max_iter=int(opts.max_iter),
energy_residual=max(abs(float(dE)), abs(free_energy_final - _loop_objective)),
density_residual=float(np.max(np.abs(_chi_density - _chi_used))),
orbital_residual=final_grad_norm,
electron_count_errors=[0.5 * (final_count - n_elec)] * 2,
conv_tol_energy=float(opts.conv_tol_energy), conv_tol_grad=float(opts.conv_tol_grad))
converged = acceptance["stationary"]
plog.converged(
n_iter=iter_idx,
energy=free_energy_final,
converged=converged,
)
_executed_support = None
if _support_recorder is not None:
_executed_support = _support_recorder(
system=system, basis=basis, domain=_resolved_domain,
one_electron={"S": S_lat, "T": T_lat, "V_ne": V_lat},
density=D_real, fock=_fb.f2e_real, j_cache=j_lr_cache,
x_cache=x_lr_cache, nuclear_support=_nuclear_support,
kpoints=k_points, weights=weights, v_ne_cache=v_ne_lr_cache,
)
# _fb is the exact rebuild at the authoritative returned density, also
# on an iteration-capped return. Trial/PATOM callbacks never set this count.
_xc_diagnostics = (None if _fb.n_electrons_grid is None else {
"n_electrons_grid": _fb.n_electrons_grid,
"periodic_density_bras": _fb.periodic_density_bras,
})
_xc_grid_error = check_xc_grid_electron_count(
_xc_diagnostics, n_elec,
context=("run_pbc_bipole_rks (Becke partition radius="
f"{float(opts.becke_image_radius_bohr):g} bohr)"),
lattice_options=lat_opts_2e,
)
return PBCBipoleRKSResult(
xc_grid_electrons=_fb.n_electrons_grid,
xc_grid_electron_error=_xc_grid_error,
xc_periodic_density_bras=_fb.periodic_density_bras,
chi_scf_acceptance=acceptance,
executed_support=_executed_support,
chi_xc_metadata=(dict(_chi_xc.metadata) if _chi_xc is not None else None),
accepted_density_k=(np.asarray(_chi_density).copy() if _chi_xc is not None else None),
restart_mesh=tuple(kmesh.mesh),
restart_kpoints=np.asarray(kmesh.kpoints).copy(), restart_weights=np.asarray(kmesh.weights).copy(),
guess_selection=periodic_result_selection(system, opts.initial_guess, restarted=initial_density is not None or initial_density_k is not None),
restart_basis=basis, restart_lattice=np.asarray(system.lattice).copy(),
energy=float(E_total),
e_electronic=float(E_elec),
e_nuclear=e_nuc,
e_ext_el_spheropole=E_sphero_final,
e_xc=final_e_xc,
e_coulomb=final_e_2e - final_e_exchange,
e_hf_exchange=final_e_exchange,
e_hf_exchange_components=dict(_fb.e_hf_exchange_components),
n_iter=iter_idx,
converged=converged,
mo_energies=eps_per_k,
mo_coeffs=C_per_k,
fock=F_k_list,
overlap=S_k_list,
hcore=Hcore_k_list,
density=D_real,
scf_trace=scf_trace,
ewald_alpha_bohr_inv=omega_used,
ewald_precision=float(ewald_precision),
resolved_domain=_resolved_domain,
sr_image_extent_bohr=_sr_image_extent,
pair_resolved_fock_domain=bool(_fock_sym_map is not None),
e_dft_plus_u=float(e_dft_plus_u),
energy_components=energy_components,
functional=str(opts.functional),
smearing_temperature=smearing_T,
fermi_level=float(fermi_level),
entropy=float(entropy),
free_energy=float(free_energy_final),
occupations=[np.asarray(o, dtype=float) for o in occ_per_k],
exchange_ewald_split=bool(exchange_split_active),
exchange_exxdiv=(exchange_exxdiv if exchange_split_active else None),
overlap_fold_drift=overlap_fold_drift,
exact_zone_bohr=_exact_zone,
output_cell_farming_execution=(
_fb.output_cell_farming_execution
),
screened_exchange_execution=screened_exchange_execution,
basin_warning=basin_warning,
kpoints_cart=np.asarray(k_points, dtype=float).reshape(-1, 3),
kpoint_weights=np.asarray(weights, dtype=float).reshape(-1),
fock_mixing=fock_mixing_value,
)