Skip to content

Commit 90bb7b1

Browse files
authored
Correct SCF-step convergence residuals and split delta_density_rms (#452, #453, #454)
Fix `delta_energies_total` to derive from the per-SCF `scf_steps.energies_total` series (not the labeled `Outputs.total_energies`), and stop synthesizing `delta_force_abs` from the final `total_forces` (now parser-only). Replace `delta_density_rms` with definition-specific quantities -- `delta_charge_abs`, `delta_charge_density_rms`, `delta_charge_relative`, `delta_density_matrix_rms`, `delta_density_matrix_max` -- plus `energy_error_estimate`, and replace `ChargeConvergenceTarget` with a `DensityConvergenceTarget` whose `type` selector derives the path and unit from the referenced `scf_steps` field. Breaking: removes `delta_density_rms`; renames `ChargeConvergenceTarget`.
1 parent 88a56ef commit 90bb7b1

11 files changed

Lines changed: 330 additions & 225 deletions

File tree

docs/schema/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Thermodynamics workflow for free-energy and thermodynamic property calculations
170170

171171
Convergence target classes and workflow-level convergence result structures
172172

173-
**Key sections:** WorkflowConvergenceTarget, EnergyConvergenceTarget, ForceConvergenceTarget, PotentialConvergenceTarget, ChargeConvergenceTarget, WavefunctionConvergenceTarget, WorkflowConvergenceResults, SimulationWorkflowModel, SimulationWorkflowResults, GeometryOptimizationModel, GeometryOptimizationResults
173+
**Key sections:** WorkflowConvergenceTarget, EnergyConvergenceTarget, ForceConvergenceTarget, PotentialConvergenceTarget, DensityConvergenceTarget, WavefunctionConvergenceTarget, WorkflowConvergenceResults, SimulationWorkflowModel, SimulationWorkflowResults, GeometryOptimizationModel, GeometryOptimizationResults
174174

175175
## [Workflow Trajectory Properties](workflow_trajectory.md)
176176

docs/schema/outputs.diagram.md

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,44 @@ This diagram shows the relationships between schema classes:
1515
classDiagram
1616
class AbsorptionSpectrum {
1717
}
18-
class ChemicalPotential {
19-
}
20-
class CrystalFieldSplitting {
18+
class ElectronicBandGap {
2119
}
22-
class ElectronicBandStructure {
20+
class ElectronicEigenvalues {
2321
}
24-
class ElectronicDensityOfStates {
22+
class ElectronicGreensFunction {
2523
}
2624
class ElectronicSelfEnergy {
2725
}
28-
class FermiSurface {
26+
class HoppingMatrix {
27+
}
28+
class KineticEnergy {
2929
}
3030
class Occupancy {
3131
}
3232
class Outputs {
3333
}
3434
class PhysicalProperty {
3535
}
36-
class PotentialEnergy {
36+
class SCFSteps {
3737
}
38-
class QuasiparticleWeight {
38+
class Temperature {
3939
}
40-
class TotalForce {
40+
class TotalEnergy {
4141
}
42-
class XASSpectrum {
42+
class TotalForce {
4343
}
4444
Outputs *-- AbsorptionSpectrum : absorption_spectra
45-
Outputs *-- ChemicalPotential
46-
Outputs *-- CrystalFieldSplitting
47-
Outputs *-- ElectronicBandStructure
48-
Outputs *-- ElectronicDensityOfStates : electronic_dos
45+
Outputs *-- ElectronicBandGap
46+
Outputs *-- ElectronicEigenvalues
47+
Outputs *-- ElectronicGreensFunction
4948
Outputs *-- ElectronicSelfEnergy : electronic_self_energies
50-
Outputs *-- FermiSurface
49+
Outputs *-- HoppingMatrix : hopping_matrices
50+
Outputs *-- KineticEnergy : kinetic_energies
5151
Outputs *-- Occupancy : occupancies
52-
Outputs *-- PotentialEnergy : potential_energies
53-
Outputs *-- QuasiparticleWeight
52+
Outputs *-- SCFSteps
53+
Outputs *-- Temperature
54+
Outputs *-- TotalEnergy : total_energies
5455
Outputs *-- TotalForce
55-
Outputs *-- XASSpectrum : xas_spectra
5656
```
5757

5858
</div>
@@ -67,43 +67,43 @@ _Diagram 2 of 2 (split due to large number of children)_
6767

6868
```mermaid
6969
classDiagram
70-
class ElectronicBandGap {
70+
class ChemicalPotential {
7171
}
72-
class ElectronicEigenvalues {
72+
class CrystalFieldSplitting {
7373
}
74-
class ElectronicGreensFunction {
74+
class ElectronicBandStructure {
7575
}
76-
class HoppingMatrix {
76+
class ElectronicDensityOfStates {
7777
}
78-
class HybridizationFunction {
78+
class FermiSurface {
7979
}
80-
class KineticEnergy {
80+
class HybridizationFunction {
8181
}
8282
class Outputs {
8383
}
8484
class Permittivity {
8585
}
8686
class PhysicalProperty {
8787
}
88-
class RadiusOfGyration {
88+
class PotentialEnergy {
8989
}
90-
class SCFSteps {
90+
class QuasiparticleWeight {
9191
}
92-
class Temperature {
92+
class RadiusOfGyration {
9393
}
94-
class TotalEnergy {
94+
class XASSpectrum {
9595
}
96-
Outputs *-- ElectronicBandGap
97-
Outputs *-- ElectronicEigenvalues
98-
Outputs *-- ElectronicGreensFunction
99-
Outputs *-- HoppingMatrix : hopping_matrices
96+
Outputs *-- ChemicalPotential
97+
Outputs *-- CrystalFieldSplitting
98+
Outputs *-- ElectronicBandStructure
99+
Outputs *-- ElectronicDensityOfStates : electronic_dos
100+
Outputs *-- FermiSurface
100101
Outputs *-- HybridizationFunction
101-
Outputs *-- KineticEnergy : kinetic_energies
102102
Outputs *-- Permittivity : permittivities
103+
Outputs *-- PotentialEnergy : potential_energies
104+
Outputs *-- QuasiparticleWeight
103105
Outputs *-- RadiusOfGyration : radii_of_gyration
104-
Outputs *-- SCFSteps
105-
Outputs *-- Temperature
106-
Outputs *-- TotalEnergy : total_energies
106+
Outputs *-- XASSpectrum : xas_spectra
107107
```
108108

109109
<p class="uml-legend__title">Legend</p>

docs/schema/outputs.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,13 @@ classDiagram
9191
|---|---|---|
9292
| `energies_total` | m_float64(float) (shape: ['*']) | Total energy at each SCF step. |
9393
| `delta_energies_total` | m_float64(float) (shape: ['*']) | Absolute change of total energy at each SCF step. |
94+
| `energy_error_estimate` | m_float64(float) (shape: ['*']) | <details><summary>Estimate of the remaining error in the total energy at each SCF step,</summary>Estimate of the remaining error in the total energy at each SCF step,<br>derived from the density residual rather than from the change of the<br>total energy itself. For example, Quantum ESPRESSO's "estimated scf<br>accuracy" is the Hartree self-energy of the density residual. Distinct<br>from `delta_energies_total`, which is the change of the total energy<br>between consecutive steps.</details> |
9495
| `delta_potential_rms` | m_float64(float) (shape: ['*']) | Root mean square of change of potential energy at each SCF step. |
95-
| `delta_density_rms` | m_float64(float) (shape: ['*']) | Root mean square of change of potential energy at each SCF step. |
96+
| `delta_charge_abs` | m_float64(float) (shape: ['*']) | <details><summary>Volume-integrated absolute change of the electron density between</summary>Volume-integrated absolute change of the electron density between<br>consecutive SCF steps, `integral \|rho_n(r) - rho_(n-1)(r)\| d^3r`,<br>expressed as a charge (equivalently a number of electrons). Reported by<br>all-electron codes such as WIEN2k (`:DIS`). The exact norm and any<br>normalization are a code-reported convention that the schema does not<br>enforce.</details> |
97+
| `delta_charge_density_rms` | m_float64(float) (shape: ['*']) | <details><summary>Root mean square, over real-space grid points, of the change of the</summary>Root mean square, over real-space grid points, of the change of the<br>electron density between consecutive SCF steps. Unlike `delta_charge_abs`<br>the volume is retained, so this is a charge density. Reported by<br>plane-wave codes such as VASP (`rms(c)`).</details> |
98+
| `delta_charge_relative` | m_float64(float) (shape: ['*']) | Integrated absolute density change normalized by the electron count, `integral \|rho_n - rho_(n-1)\| d^3r / N`, hence dimensionless. Reported by exciting ("charge distance") and GPAW (per valence electron). |
99+
| `delta_density_matrix_rms` | m_float64(float) (shape: ['*']) | <details><summary>Root mean square of the change of the density-matrix elements `P_munu`</summary>Root mean square of the change of the density-matrix elements `P_munu`<br>(in the non-orthonormal atomic-orbital basis) between consecutive SCF<br>steps. The elements are dimensionless, so is this residual. Reported by<br>Gaussian-basis codes such as CRYSTAL (`tst`) and ORCA (`RMS-DP`).</details> |
100+
| `delta_density_matrix_max` | m_float64(float) (shape: ['*']) | <details><summary>Maximum absolute change of the density-matrix elements `P_munu` between</summary>Maximum absolute change of the density-matrix elements `P_munu` between<br>consecutive SCF steps; the max-norm counterpart of<br>`delta_density_matrix_rms`. Reported by Gaussian-basis codes such as<br>CP2K, CRYSTAL (`PX`), and ORCA (`Max-DP`).</details> |
96101
| `delta_wavefunction_rms` | m_float64(float) (shape: ['*']) | Root mean square of change of wavefunction coefficients at each SCF step. Dimensionless quantity representing convergence of orbital coefficients. |
97102
| `delta_force_abs` | m_float64(float) (shape: ['*']) | Absolute change of forces at each SCF step. |
98103
| `durations` | m_float64(float) (shape: ['*']) | Time spent at each SCF step. |

docs/schema/workflow_convergence.diagram.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This diagram shows the relationships between schema classes:
1313

1414
```mermaid
1515
classDiagram
16-
class ChargeConvergenceTarget {
16+
class DensityConvergenceTarget {
1717
}
1818
class EnergyConvergenceTarget {
1919
}
@@ -35,7 +35,7 @@ classDiagram
3535
}
3636
class WorkflowConvergenceTarget {
3737
}
38-
WorkflowConvergenceTarget <|-- ChargeConvergenceTarget
38+
WorkflowConvergenceTarget <|-- DensityConvergenceTarget
3939
WorkflowConvergenceTarget <|-- EnergyConvergenceTarget
4040
WorkflowConvergenceTarget <|-- ForceConvergenceTarget
4141
SimulationWorkflowResults <|-- GeometryOptimizationResults

docs/schema/workflow_convergence.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
```mermaid
1212
classDiagram
13-
class ChargeConvergenceTarget
13+
class DensityConvergenceTarget
1414
class EnergyConvergenceTarget
1515
class ForceConvergenceTarget
1616
class GeometryOptimizationModel
@@ -21,7 +21,7 @@ classDiagram
2121
class WavefunctionConvergenceTarget
2222
class WorkflowConvergenceResults
2323
class WorkflowConvergenceTarget
24-
WorkflowConvergenceTarget <|-- ChargeConvergenceTarget
24+
WorkflowConvergenceTarget <|-- DensityConvergenceTarget
2525
WorkflowConvergenceTarget <|-- EnergyConvergenceTarget
2626
WorkflowConvergenceTarget <|-- ForceConvergenceTarget
2727
SimulationWorkflowResults <|-- GeometryOptimizationResults
@@ -84,15 +84,16 @@ classDiagram
8484
|---|---|---|
8585
| `threshold` | m_float_bounded(float) | <details><summary>Convergence threshold.</summary>Convergence threshold. Must be non-negative.<br>When threshold_type is 'relative', must be dimensionless.<br>When threshold_type is 'absolute', 'maximum', or 'rms', must have physical units.<br>Child classes override this to add convergence path annotations.</details> |
8686

87-
### `ChargeConvergenceTarget`
87+
### `DensityConvergenceTarget`
8888

8989
| Section | Description | MetaInfo |
9090
|---|---|---|
91-
| `ChargeConvergenceTarget` | Convergence target for electron density/charge differences. | [Open in MetaInfo browser](https://nomad-lab.eu/prod/v1/develop/gui/analyze/metainfo/nomad_simulations/section_definitions@nomad_simulations.schema_packages.workflow.general.ChargeConvergenceTarget){:target="_blank"} |
91+
| `DensityConvergenceTarget` | Convergence target for the SCF electron-density residual. | [Open in MetaInfo browser](https://nomad-lab.eu/prod/v1/develop/gui/analyze/metainfo/nomad_simulations/section_definitions@nomad_simulations.schema_packages.workflow.general.DensityConvergenceTarget){:target="_blank"} |
9292

9393
| Quantity | Type | Description |
9494
|---|---|---|
9595
| `threshold` | m_float_bounded(float) | <details><summary>Convergence threshold.</summary>Convergence threshold. Must be non-negative.<br>When threshold_type is 'relative', must be dimensionless.<br>When threshold_type is 'absolute', 'maximum', or 'rms', must have physical units.<br>Child classes override this to add convergence path annotations.</details> |
96+
| `type` | Enum | Which density-convergence residual this target tracks. Selects the `scf_steps.delta_<type>` quantity read for the check (and thereby its expected unit). |
9697

9798
### `WavefunctionConvergenceTarget`
9899

scripts/verticals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
'EnergyConvergenceTarget',
7171
'ForceConvergenceTarget',
7272
'PotentialConvergenceTarget',
73-
'ChargeConvergenceTarget',
73+
'DensityConvergenceTarget',
7474
'WavefunctionConvergenceTarget',
7575
'WorkflowConvergenceResults',
7676
'SimulationWorkflowModel',

src/nomad_simulations/schema_packages/outputs.py

Lines changed: 91 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ class SCFSteps(ArchiveSection):
6464
""",
6565
)
6666

67+
energy_error_estimate = Quantity(
68+
shape=['*'],
69+
type=float,
70+
unit='joule',
71+
description="""
72+
Estimate of the remaining error in the total energy at each SCF step,
73+
derived from the density residual rather than from the change of the
74+
total energy itself. For example, Quantum ESPRESSO's "estimated scf
75+
accuracy" is the Hartree self-energy of the density residual. Distinct
76+
from `delta_energies_total`, which is the change of the total energy
77+
between consecutive steps.
78+
""",
79+
)
80+
6781
delta_potential_rms = Quantity(
6882
shape=['*'],
6983
type=float,
@@ -73,12 +87,64 @@ class SCFSteps(ArchiveSection):
7387
""",
7488
)
7589

76-
delta_density_rms = Quantity(
90+
delta_charge_abs = Quantity(
7791
shape=['*'],
7892
type=float,
7993
unit='coulomb',
8094
description="""
81-
Root mean square of change of potential energy at each SCF step.
95+
Volume-integrated absolute change of the electron density between
96+
consecutive SCF steps, `integral |rho_n(r) - rho_(n-1)(r)| d^3r`,
97+
expressed as a charge (equivalently a number of electrons). Reported by
98+
all-electron codes such as WIEN2k (`:DIS`). The exact norm and any
99+
normalization are a code-reported convention that the schema does not
100+
enforce.
101+
""",
102+
)
103+
104+
delta_charge_density_rms = Quantity(
105+
shape=['*'],
106+
type=float,
107+
unit='coulomb / meter ** 3',
108+
description="""
109+
Root mean square, over real-space grid points, of the change of the
110+
electron density between consecutive SCF steps. Unlike `delta_charge_abs`
111+
the volume is retained, so this is a charge density. Reported by
112+
plane-wave codes such as VASP (`rms(c)`).
113+
""",
114+
)
115+
116+
delta_charge_relative = Quantity(
117+
shape=['*'],
118+
type=float,
119+
unit='dimensionless',
120+
description="""
121+
Integrated absolute density change normalized by the electron count,
122+
`integral |rho_n - rho_(n-1)| d^3r / N`, hence dimensionless. Reported by
123+
exciting ("charge distance") and GPAW (per valence electron).
124+
""",
125+
)
126+
127+
delta_density_matrix_rms = Quantity(
128+
shape=['*'],
129+
type=float,
130+
unit='dimensionless',
131+
description="""
132+
Root mean square of the change of the density-matrix elements `P_munu`
133+
(in the non-orthonormal atomic-orbital basis) between consecutive SCF
134+
steps. The elements are dimensionless, so is this residual. Reported by
135+
Gaussian-basis codes such as CRYSTAL (`tst`) and ORCA (`RMS-DP`).
136+
""",
137+
)
138+
139+
delta_density_matrix_max = Quantity(
140+
shape=['*'],
141+
type=float,
142+
unit='dimensionless',
143+
description="""
144+
Maximum absolute change of the density-matrix elements `P_munu` between
145+
consecutive SCF steps; the max-norm counterpart of
146+
`delta_density_matrix_rms`. Reported by Gaussian-basis codes such as
147+
CP2K, CRYSTAL (`PX`), and ORCA (`Max-DP`).
82148
""",
83149
)
84150

@@ -280,55 +346,18 @@ def set_model_method_ref(self) -> ModelMethod | None:
280346

281347
def _compute_energy_deltas(self, logger: 'BoundLogger'):
282348
"""
283-
Compute delta_energies_total from consecutive total_energies values.
284-
285-
Returns array of absolute energy differences between consecutive steps.
349+
Compute `delta_energies_total` as the absolute change between consecutive
350+
`scf_steps.energies_total` values, i.e. the per-SCF-iteration total-energy
351+
series. This is the correct source for an SCF convergence measure; the
352+
repeating `Outputs.total_energies` holds labeled generic total energies
353+
(e.g. "DFT" vs "DFT + dispersion"), whose differences are not SCF deltas.
286354
"""
287-
try:
288-
if self.total_energies is None or len(self.total_energies) < 2:
289-
return None
290-
291-
# Extract energy values from each step
292-
energy_values = [e.value for e in self.total_energies]
293-
294-
# Compute differences manually to preserve Pint units
295-
deltas = []
296-
for i in range(1, len(energy_values)):
297-
delta = np.abs(energy_values[i] - energy_values[i - 1])
298-
deltas.append(delta)
299-
300-
# Convert list to array-like structure
301-
if len(deltas) > 0 and hasattr(deltas[0], 'magnitude'):
302-
# Pint quantities - stack magnitudes and add units
303-
magnitudes = [d.magnitude for d in deltas]
304-
return np.array(magnitudes) * deltas[0].units
305-
else:
306-
return np.array(deltas)
307-
308-
except (AttributeError, IndexError, TypeError, ValueError) as e:
309-
logger.debug(f'Could not compute delta_energies_total: {e}')
355+
if self.scf_steps is None or self.scf_steps.energies_total is None:
310356
return None
311-
312-
def _compute_force_norms(self, logger: 'BoundLogger'):
313-
"""
314-
Compute delta_force_abs from total_forces as force norms.
315-
316-
Returns array of force norms (L2 norm of 3D force vectors).
317-
"""
318-
try:
319-
if self.total_forces is None or len(self.total_forces) == 0:
320-
return None
321-
322-
# Get force values (Pint Quantity with shape [n_atoms, 3])
323-
force_values = self.total_forces[-1].value
324-
325-
# Compute force norms (preserves Pint units)
326-
force_norms = ((force_values**2).sum(axis=1)) ** 0.5
327-
328-
return force_norms
329-
except (AttributeError, IndexError, TypeError) as e:
330-
logger.debug(f'Could not compute delta_force_abs: {e}')
357+
energies = self.scf_steps.energies_total
358+
if len(energies) < 2:
331359
return None
360+
return np.abs(np.diff(energies.magnitude)) * energies.units
332361

333362
def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None:
334363
super().normalize(archive, logger)
@@ -349,19 +378,20 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None:
349378
except Exception as e:
350379
logger.debug(f'Could not set model_method_ref: {e}')
351380

352-
# Populate missing SCF delta quantities from available data
353-
if self.scf_steps is not None:
354-
# Define delta computations: (delta_field, compute_function)
355-
delta_computations = [
356-
('delta_energies_total', self._compute_energy_deltas),
357-
('delta_force_abs', self._compute_force_norms),
358-
]
359-
360-
for delta_field, compute_func in delta_computations:
361-
if getattr(self.scf_steps, delta_field, None) is None:
362-
computed_value = compute_func(logger)
363-
if computed_value is not None:
364-
setattr(self.scf_steps, delta_field, computed_value)
381+
# Derive `delta_energies_total` from the per-SCF `energies_total` series when the
382+
# parser did not provide it directly (#454).
383+
#
384+
# The other SCF residuals are deliberately NOT synthesized here. In particular
385+
# `delta_force_abs` (the change of forces between successive SCF iterations) cannot
386+
# be derived from the archive: `Outputs.total_forces` holds the final/labeled forces,
387+
# not a per-SCF-iteration series, so norming them would fill an SCF-convergence
388+
# field with the final force magnitudes -- a physically different quantity (#453).
389+
# `delta_force_abs` and the density/potential residuals are therefore set only by
390+
# parsers that genuinely report them per SCF step.
391+
if self.scf_steps is not None and self.scf_steps.delta_energies_total is None:
392+
deltas = self._compute_energy_deltas(logger)
393+
if deltas is not None:
394+
self.scf_steps.delta_energies_total = deltas
365395

366396

367397
class WorkflowOutputs(Outputs):

0 commit comments

Comments
 (0)