API
QuantumGaussianDynamics.Dynamics — TypeSettings for the dynamics.
Dynamics(dt :: T = 1.0 #femtosecodns, totaltime :: T # In femtosecodns, algorithm :: String = "generalized-verlet", kongliuratio :: T = 1.0, verbose :: Bool = true, evolvecorrelators :: Bool = true, correlated :: Bool = true, seed :: Int64, N :: Int64,
# Save the data each
save_each :: Int64 = 1,
save_filename :: String,
save_correlators :: Bool)The settings for the simulation. dt and total_time are in femtoseconds
dtis the time step [in femtoseconds]total_timeis the total simulation time [in femtoseconds]algorithmis the integration algorithm to use. Either "generalized-verlet" or "semi-implicit-verlet"kong_liu_ratiois the ratio exploits the importance sampling.verboseis a flag to print out information during the simulationevolve_correlatorsis a flag to evolve the correlators <RR>, <PP> and <RP>correlatedif true, the correlated approach for computing ensemble averages is usedseedis the seed for the random number generatorNis the number of stochastic configurationssave_filenameis the name of the file where to save the datasave_correlatorsis a flag to save the correlators informationsave_eachis the number of steps between each save of the data
QuantumGaussianDynamics.ElectricField — TypeElectric Field.
Base.@kwdef mutable struct ElectricField{T <: AbstractFloat}
fun :: Function #Time in fs, unit
Zeff :: Matrix{T}
edir :: Vector{T} #Must have unit norm
eps :: Matrix{T}
endThis structure contains the information about the external IR electric field.
funis the function that describes the electric field as a function of timeZeffis the effective charge matrixediris the direction of the electric fieldepsis the dielectric constant matrix
QuantumGaussianDynamics.Ensemble — TypeEnsemble average information stocastic displacements and forces have index i, j, coordinate i, configuration j
Ensemble(rho0 :: WignerDistribution{T}, positions :: Matrix{T}, # Positions are multiplied by the square root of the masses energies :: Vector{T}, forces :: Matrix{T}, # Forces are divided by the squareroot of masses stress :: Matrix{T}, # Stress in eV/A^3 sschaenergies :: Vector{T}, sschaforces :: Matrix{T}, nconfigs :: Int32, weights :: Vector{T}, temperature :: T, correlated :: Bool, y0 :: Matrix{T}, #unitcell :: Matrix{T})
QuantumGaussianDynamics.calculate_ensemble! — Methodcalculate_ensemble!(ensemble::Ensemble{T}, crystal) where {T<:AbstractFloat}
Evaluate energies, forces, and stresses for each configuration in an Ensemble using a given calculator ( assigned to crystal). Results are written back into the ensemble in place.
Arguments
ensemble::Ensemble{T}: ensemble of stochastic configurations. Updated in place.crystal: external calculator object (e.g. ASE/PyCall wrapper) providing:crystal.positions(settable Cartesian coordinates)get_potential_energy()get_forces()(returnsN_atoms × 3array)get_stress()(returns stress tensor
Units
- Energies are converted to Rydberg (
* CONV_RY). - Forces are flattened, divided by √masses, and converted with
CONV_RY/CONV_BOHR. - Stress is assumed to be in eV/ų and stored directly.
Returns
- The updated
ensemble(modification is in place, return value is mainly for chaining).
QuantumGaussianDynamics.classic_evolution! — Methodclassicevolution!(Rs::Vector{T}, Ps::Vector{T}, dt::T, clfor, part) where {T <: AbstractFloat}
Classic dynamics.
QuantumGaussianDynamics.euler_step! — Methodeulerstep!(wigner:: WignerDistribution{T}, dt :: T, avgfor :: Vector{T}, d2V_dr2 :: Matrix{T}) where {T <: AbstractFloat}
Explicit Euler method. Implemented for testing and didactic use. MUST BE AVOIDED, it is unconditionally unstable.
QuantumGaussianDynamics.fixed_step! — Methodfunction fixedstep!(wigner :: WignerDistribution{T}, dt :: T, avgfor :: Vector{T}, d2V_dr2 :: Matrix{T},part ) where {T <: AbstractFloat}
Evolves the quantum state freezing the nuclear degrees of freedom. This corresponds to a classic evolution in which however the forces are computed as ensemble averages using the initial quantum distribution.
QuantumGaussianDynamics.full_generalized_verlet_step! — Methodsemiimpliciteulerstep!(wigner:: WignerDistribution{T}, dt :: T, avgfor :: Vector{T}, d2V_dr2 :: Matrix{T}) where {T <: AbstractFloat}
Experimental.
QuantumGaussianDynamics.generalized_verlet_step! — Methodgeneralized_verlet_step!(rho :: WignerDistribution{T}, dt :: T, avg_for :: Vector{T}, d2V_dr2 :: Matrix{T}, bc0 :: Vector{T} ,part ) where {T <: AbstractFloat}Evolves the means and second–order correlators of a WignerDistribution by one generalized-Verlet step. This is algorithm has O(dt^3) error in time step. The algorithnm is illustrated in the work from Libbi et al., npj Computat. Mater. 11, 102 (2025), and consists of the explicit evolution of the variables ⟨R⟩, ⟨P⟩ and ⟨RR⟩, and of an implicit evolution for the variables ⟨PP⟩ and ⟨RP⟩. The latter is performed iteratively.
Arguments
rho::WignerDistribution{T}: state (means & correlators), updated in place.dt::T: time step.avg_for::Vector{T}: current average force ⟨f⟩.d2V_dr2::Matrix{T}: Hessian (∂²V). Assumed symmetric in practice.part::Int: stage selector (1= predictor,2= corrector).
Given the following variables name ⟨R⟩ = Rav, ⟨P⟩ = Pav, ⟨f⟩ = avgfor, ⟨RR⟩ = RRcorr, ⟨PP⟩ = PPcorr, ⟨RP⟩ = RPcorr, ⟨d²V⟩ = d2V_dr2
part == 1(predictor):- Means (half-kick)
- ⟨R⟩ ← ⟨R⟩ + dt⟨P⟩ + ½dt²*⟨f⟩
- ⟨P⟩ ← ⟨P⟩ + ½dt⟨f⟩
- Correlators
- let K = ⟨RR⟩ * ⟨d²V⟩
- ⟨RR⟩ ← ⟨RR⟩ + dt(⟨RP⟩ + ⟨RP⟩ᵀ) − ½dt²(K + Kᵀ) + dt²⟨PP⟩
- save ⟨RP⟩₀ = ⟨RP⟩
- ⟨RP⟩ ← ⟨RP⟩ + ½dt(⟨PP⟩ − K)
- ⟨PP⟩ ← ⟨PP⟩ − ½dt(⟨d²V⟩⟨RP⟩₀ + (⟨d²V⟩⟨RP⟩₀)ᵀ)
- Means (half-kick)
part == 2(corrector / finishing kick):- Means
- ⟨P⟩ ← ⟨P⟩ + ½dt⟨f⟩
- Correlators (fixed-point refinement of ⟨PP⟩ and ⟨RP⟩)
- pre-update: ⟨RP⟩ ← ⟨RP⟩ − ½dt(⟨RR⟩*⟨d²V⟩)
- iterate a few times (typically 4):
- ⟨RP⟩₁ ← ⟨RP⟩ + ½dt⟨PP⟩₁
- ⟨PP⟩₁ ← ⟨PP⟩ − ½dt(⟨d²V⟩⟨RP⟩₁ + (⟨d²V⟩⟨RP⟩₁)ᵀ)
- write back final ⟨PP⟩, ⟨RP⟩
- Means
QuantumGaussianDynamics.generate_ensemble! — Methodgenerate_ensemble!(N, ensemble:: Ensemble{T}, wigner_distribution :: WignerDistribution{T}) where {T <: AbstractFloat}Generate or update an ensemble of stochastic configurations for nuclear dynamics, based on a Wigner distribution.
Arguments
N::Int: Number of configurations to generate (must be even).ensemble::Ensemble{T}: Target ensemble structure. Arrays are either updated in place if their size already matchesN, or reallocated otherwise.wigner_distribution::WignerDistribution{T}: Wigner distribution that encodes the covariance matrices, eigenvalues, and eigenvectors used to generate the ensemble.
Behavior
- If
ensemble.correlated == true, correlated configurations are generated fromensemble.y0using the square root of the correlator matrix. - Otherwise, random Gaussian vectors are drawn (using
randn) and scaled by√λor1/√λdepending on whetherρ.evolve_correlatorsis set. - In MPI runs, random numbers are generated on rank 0 and broadcast to all ranks so that every processor works with the same ensemble.
Returns
- The updated
ensemble(modifications are in place).
QuantumGaussianDynamics.get_average_energy — Methodget_average_energy(ensemble :: Ensemble{T}) where {T <: AbstractFloat}Computes the ensemble average of the potential energy.
QuantumGaussianDynamics.get_average_forces — Methodget_average_forces(ensemble :: Ensemble{T}) where {T <: AbstractFloat}Computes the ensemble average of the forces.
QuantumGaussianDynamics.get_average_stress — Methodget_average_stress(ensemble :: Ensemble{T}, wigner :: WignerDistribution{T}) where {T <: AbstractFloat}Computes the ensemble average of the stress. The result is returned in Ry/Bohr^3
QuantumGaussianDynamics.get_averages! — Methodfunction getaverages!(avgfor :: Vector{T}, d2vdr2 :: Matrix{T}, ensemble :: Ensemble{T}, wignerdistribution :: WignerDistribution{T}) where {T <: AbstractFloat}
Evaluate the average force and d2v_dr2 from the ensemble and the wigner distribution. The weights on the ensemble should have been already updated.
QuantumGaussianDynamics.get_classic_forces — Methodget_classic_forces(wigner_distribution:: WignerDistribution{T}, crystal) where {T <: AbstractFloat}Compute the forces at the centroids, i.e., the classical forces acting on the crystal.
QuantumGaussianDynamics.get_kong_liu — Methodget_kong_liu(ensemble :: Ensemble{T}) where {T <: AbstractFloat}Compute the Kong-Liu ration
QuantumGaussianDynamics.get_random_y — Methodget_random_y(N, N_modes, settings :: Dynamics{T}) where {T <: AbstractFloat}Generates a random matrix of numbers sampled from a normal distribution. Each row corresponds to a phonon mode, and contains N random configurations (or N/2 if evenodd = true). For parallel execution, the matrix ymui is broadcast so that all processors use the same random variables.
QuantumGaussianDynamics.get_total_energy — Methodget_total_energy(ensemble :: Ensemble{T}, wigner_distribution :: WignerDistribution{T}) where {T <: AbstractFloat}Computes the total quantum energy, as a sum of the quantum potential energy (avgene), quantum kynetic energy (Krho), and classic kynetic energy (K_point)
QuantumGaussianDynamics.get_λs — Methodfunction get_λs(RR_corr :: Matrix{T}) where {T <: AbstractFloat}Get the eigenvalues of <RR>.
QuantumGaussianDynamics.init_ensemble_from_python — Methodinit_ensemble_from_python(py_ensemble, settings::Dynamics{T}) where {T<:AbstractFloat}Construct a Julia Ensemble object from a Python-side ensemble exported via PyCall.
This routine converts positions, forces, stresses, and energies from the Python py_ensemble (typically created by CellConstructor / SSCHA Python codes) into the internal representation used by QuantumGaussianDynamics.
Arguments
py_ensemble: A Python object with fields:current_dyn– Python dynamical matrix objectT0– temperature (in K)N– number of configurationsxats– atomic random displacements, shape (N, Nat, 3)energies– raw energies for each configurationsscha_energies– SSCHA energiesforces– atomic forces, shape (N, Nat, 3)sscha_forces– SSCHA forces, shape (N, Nat, 3)stresses– stress tensors, shape (N, 3, 3)
Conversions performed
- Positions:
(Nat, 3, N)→(3*Nat, N). Converted to Bohr and multiplied by √masses. - Energies: copied directly from
py_ensemble.energiesandsscha_energies. - Forces:
(Nat, 3, N)→(3*Nat, N). Converted to Ry/Bohr and divided by √masses. - Stresses:
(3,3)tensors packed into 6×N Voigt form: 1=xx, 2=yy, 3=zz, 4=yz, 5=xz, 6=xy. - Weights: initialized to 1.0 for all configurations.
- Random seed: if
settings.seed != 0, reseeds RNG for reproducibility. - y0: initial random Gaussian vectors, generated with
get_random_y. Ifsettings.correlated==false, filled with zeros of the correct shape.
Returns
Ensemble{T}: A new ensemble populated with data from Python, ready for use in QuantumGaussianDynamics simulations.
QuantumGaussianDynamics.init_from_dyn — Methodinit_from_dyn(dyn, TEMPERATURE::T, settings::Dynamics{T}) where {T<:AbstractFloat}Initialize a WignerDistribution object starting from a dynamical matrix.
This routine constructs the equilibrium nuclear quantum state (in terms of positions, momenta, and correlation matrices) associated with the phonon modes of the provided dynamical matrix at a given temperature.
Arguments
dyn: A dynamical matrix object from SSCHA calculationsTEMPERATURE::T: The target temperature.settings::Dynamics{T}: Simulation settings.
Details
- Builds a supercell from
dynand computes the number of modes and atoms. - Diagonalizes the supercell dynamical matrix to obtain phonon frequencies and eigenvectors (polarizations).
- Computes Gaussian width parameters (
alpha,beta) and correlators (RR_corr,PP_corr,RP_corr) at the specified temperature. - Initializes average positions (
R_av) and momenta (P_av), rescaled by atomic masses. - Removes translational acoustic modes from the correlators/eigenvalues.
Returns
rho::WignerDistribution: An initialized Wigner distribution representing the quantum nuclear state corresponding to the given dynamical matrix and temperature.
Notes
- Frequencies are assumed to be in Rydberg units.
- Cell vectors and coordinates are converted to Bohr units.
- If
settings.evolve_correlators == false, eigen-decomposition ofalphais used; otherwise, theRR_corrmatrix is diagonalized (deprecated).
QuantumGaussianDynamics.integrate! — Methodintegrate!(wigner::WignerDistribution{T},
ensemble::Ensemble{T},
settings::Dynamics{T},
crystal,
efield::ElectricField{T}) where {T<:AbstractFloat}Time-integrate the TDSCHA equations of motion. This mutates wigner (⟨R⟩, ⟨P⟩, correlators) and ensemble (weights, possibly regenerated configurations), and writes diagnostics to disk at the cadence settings.save_each.
Integration scheme
- Uses the algorithm specified by
settings.algorithm:"euler" | "semi-implicit-euler" | "semi-implicit-verlet" | "fixed" | "generalized-verlet" | "none". - At each step:
- Build average force and Hessian‐like term from the current ensemble:
get_averages!. - Add external field forces
get_external_forces(t_fs, efield, wigner). - Advance quantum variables with the selected integrator.
- Advance classical centroids
(Rs, Ps)viaclassic_evolution!. - Update eigenstructure of
RRand reweight the ensembleupdate_weights!. - If the Kong–Liu ratio falls below
settings.kong_liu_ratio, regenerate the ensemble and recompute forces.
- Build average force and Hessian‐like term from the current ensemble:
Files written (rank 0 only)
All prefixed by settings.save_filename * "settings.dt-settings.total_time-settings.N": All in Rydberg units, NOT scaled by masses.
.pos: ⟨R⟩ and ⟨P⟩ plus total energy..rho: RR correlator..for: average force and “classical” force, andd²V/dR²..cl: classical centroids and classical energy..ext: external field force..str: average Voigt stress..bet: PP correlator..gam: RP correlator.
Returns
nothing. The function mutates wigner, ensemble, and writes output files.
Errors
Throws ArgumentError if settings.algorithm is unknown.
Example
```julia w = WignerDistribution{Float64}(; ... ) # prepared from a phonon dyn ens = Ensemble{Float64}(; ... ) # configurations & weights ef = ElectricField{Float64}(; fun=t->exp(-(t-50)^2/200), Zeff=Z, edir=û, eps=ϵ) integrate!(w, ens, Dynamics(dt=1.0, totaltime=200.0, algorithm="generalized-verlet", kongliuratio=0.8, verbose=true, correlated=true, seed=1, N=size(ens.positions,2), saveeach=50, save_filename="run"), crystal, ef)
QuantumGaussianDynamics.pulse — Methodpulse(t, A, w, t0, sig)Gaussian wavepacket pulse
QuantumGaussianDynamics.remove_translations — MethodRemove acoustic sum rule from eigenvalue and eigenvectors
QuantumGaussianDynamics.semi_implicit_euler_step! — Methodsemiimpliciteulerstep!(wigner:: WignerDistribution{T}, dt :: T, avgfor :: Vector{T}, d2V_dr2 :: Matrix{T}) where {T <: AbstractFloat}
Semi-implicit Euler method. Implemented for testing and didactic use. Not efficient, use semi-implicit Verlet instead.
QuantumGaussianDynamics.semi_implicit_verlet_step! — Methodfunction semi_implicit_verlet_step!(rho:: WignerDistribution{T}, dt :: T, avg_for :: Vector{T}, d2V_dr2 :: Matrix{T}, part ) where {T <: AbstractFloat}Evolves the quantum means and correlators of a WignerDistribution by one semi-implicit Verlet integration step. This algorithms has error O(dt^2) on the integration of the correlators.
This is a two-stage scheme:
part == 1updates the positions (R_av) and momenta (P_av) using the current forceavg_for.part == 2completes the step with the updated force, and evolves the second-order correlators (RR_corr,RP_corr,PP_corr).
Arguments
ρ::WignerDistribution{T}: system state (means and correlators), mutated in place.dt::T: time step.avg_for::Vector{T}: average force vector ⟨F⟩.d2V_dr2::Matrix{T}: Hessian (second derivatives of the potential).part::Int: integration stage:
Method
Given the following variables name ⟨R⟩ = Rav, ⟨P⟩ = Pav, ⟨f⟩ = avgfor, ⟨RR⟩ = RRcorr, ⟨PP⟩ = PPcorr, ⟨RP⟩ = RPcorr, ⟨d²V⟩ = d2V_dr2
- In part == 1:
- ⟨R⟩ ← ⟨R⟩ + dt·⟨P⟩ + ½·dt²·⟨f⟩
- ⟨P⟩ ← ⟨P⟩ + ½·dt·⟨f⟩
- In part == 2:
- ⟨P⟩ ← ⟨P⟩ + ½·dt·⟨f⟩
- ⟨RP⟩ ← ⟨RP⟩ + dt·⟨PP⟩ − dt·⟨RR⟩·⟨d²V⟩
- ⟨PP⟩ ← ⟨PP⟩ − dt·(⟨d²V⟩·⟨RP⟩ + (⟨d²V⟩·⟨RP⟩)ᵀ)
- ⟨RR⟩ ← ⟨RR⟩ + dt·(⟨RP⟩ + ⟨RP⟩ᵀ)
QuantumGaussianDynamics.sin_field — Methodsin_field(t, A, w)Sinusoidal external field.
QuantumGaussianDynamics.step! — MethodPerform one integration step using the selected algorithm.
Arguments
algorithm: Dynamics structure containing algorithm name.wigner: WignerDistribution state.my_dt: timestep, in Ry units.tot_for: total force vector.d2v_dr2: force constant (Hessian-like) matrix.part: part of the algorithm (1,2)
It automatically calls the correct *_step! function.
QuantumGaussianDynamics.update_weights! — MethodUpdate the weights of the ensemble according with the new wigner distribution