Skip to content

Refactor SCF quantites and convergence#260

Open
kubanmar wants to merge 23 commits intodevelopfrom
warnings_and_errors
Open

Refactor SCF quantites and convergence#260
kubanmar wants to merge 23 commits intodevelopfrom
warnings_and_errors

Conversation

@kubanmar
Copy link
Collaborator

@kubanmar kubanmar commented Sep 4, 2025

In this PR I want to refactor how to deal with not converged or otherwise not finished calculations.

The first step is to refactor and unify how we represent the convergence of the simulation workflow. This PR introduces a new subsection WorkflowConvergenceTarget to all cases where a workflow converges, e.g., SCF, or geometry optimization. It is intended to replace the convergence parameters in GeometryOptimizationModel as well as the convergence settings in SCFOutputs and SelfConsistency.

SCFOutputs is very minimal now and I don't know what it is used for. The information about the SCF should be included in the workflow section. Should SCFOutputs be a reference then, to not duplicate information? Is SCFOutputs used anywhere?

This is a draft PR, I left the failing tests in there on purpose to keep track of the functionality that I broke by removing SelfConsistency.

Note: This PR breaks the abinit parser, because it tries to populate workflow.geometry_optimization.GeometryOptimizationModel.convergence_tolerance_energy_difference, which moved to WorkflowConvergenceTarget

Summary of changes:

  1. add more quantities to Program
  2. add WorkflowConvergenceTarget
  3. add WorkflowConvergenceTarget to SimulationWorkflowModel
  4. remove SelfConsistency
  5. remove self_consistency_ref from PhysicalProperty
  6. remove convergence from SCFOutputs
  7. remove convergence from GeometryOptimizationModel

@kubanmar kubanmar self-assigned this Sep 4, 2025
Copy link
Collaborator

@JFRudzinski JFRudzinski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me.

TODOs (ideally in the next 2 weeks, i.e., by 27.10):

  • Add SCF as a workflow class, like GeomOpt
  • Determine a standard for populating SCF and SCF+GeomOpt, ideally using a representative parser
  • Align with @ndaelman in the context of #191 and decide what indicators will be present in data.outputs
  • Consider backcompatibility / integration procedure in the parsers

@coveralls
Copy link

coveralls commented Oct 17, 2025

Pull Request Test Coverage Report for Build 18594497000

Details

  • 21 of 21 (100.0%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.01%) to 90.259%

Totals Coverage Status
Change from base Build 18342061192: -0.01%
Covered Lines: 5958
Relevant Lines: 6601

💛 - Coveralls

@kubanmar
Copy link
Collaborator Author

kubanmar commented Nov 14, 2025

The current status of the implementation is:

@JFRudzinski
Copy link
Collaborator

That's great @kubanmar , thanks for pushing it forward 🙌 let's see if there is anytime next week, but if not it can be wrapped up the following week

Comment on lines 67 to 72
convergence_threshold_unit = Quantity(
type=str,
description="""
Unit using the pint UnitRegistry() notation for the `convergence_threshold`.
"""
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this won't be compatible with the NOMAD framework for handling units. It won't convert when the user asks so. Moreover, to fit your description, you should alter the type. This still won't power the unit conversion on request.

The modular handling of units is very hard. Many ppl have given it a shot. The closest we have right now are these dataframes.

@kubanmar
Copy link
Collaborator Author

Newest updates:

  • correct behavior for SinglePoint workflows
  • new challenge when populating the workflow section of the archive directly: mapping of tasks becomes nested

TODO:

  • find a way to pass information of the convergence of the single points in a geometry optimization to the top level workflow

I have left some comments in the code where I could need some help.

@JFRudzinski
Copy link
Collaborator

@kubanmar can you rename this PR so the scope is more clear, include SCF

@JFRudzinski JFRudzinski mentioned this pull request Dec 16, 2025
@kubanmar kubanmar changed the title Warnings and errors Refactor SCF quantites and convergence Dec 16, 2025
@kubanmar
Copy link
Collaborator Author

kubanmar commented Dec 19, 2025

The latest commit provides a working example:

  • convergence targets are correctly recognized and during normalization the corresponding results are created
  • correct assignment of single point vs. geometry optimization convergence targets
  • independently reporting of geometry optimization and SCF convergence

TODO:

  • resolve conflicts
  • tests

@EBB2675 if you want to review/contribute I would be happy, otherwise I will continue working on it after the break

@coveralls
Copy link

coveralls commented Jan 28, 2026

Pull Request Test Coverage Report for Build 22540757767

Details

  • 487 of 547 (89.03%) changed or added relevant lines in 12 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.2%) to 83.383%

Changes Missing Coverage Covered Lines Changed/Added Lines %
tests/conftest.py 5 9 55.56%
src/nomad_simulations/schema_packages/workflow/geometry_optimization.py 43 62 69.35%
src/nomad_simulations/schema_packages/workflow/general.py 135 172 78.49%
Totals Coverage Status
Change from base Build 22018524490: 0.2%
Covered Lines: 7843
Relevant Lines: 9406

💛 - Coveralls

@kubanmar
Copy link
Collaborator Author

I have now merged develop and fixed formatting and type checks. There are still plenty of TODOs for which I would appreciate help.

Most severe is that this is only compatible with NOMAD v1.3, as the newer version generates too many nested subsections in for the workflow tasks. If I understood @EBB2675 correctly this is not a schema/parser level issue, but can only be addressed on the mapping parser level.

Using https://github.com/FAIRmat-NFDI/nomad-parser-plugins-simulation/tree/schema_update_convergence_targets for the parsers, the code now generates the intended data for: exciting single point calculations and geometry optimizations.

From my side, this is therefore ready for review.

@kubanmar kubanmar marked this pull request as ready for review January 28, 2026 16:39
@kubanmar
Copy link
Collaborator Author

TODOs:

  • should one replace the repeating subsection of ConvergenceTargets with a section definition that explicitly defines all targets as quantities (see top of workflow/general) - it would be nice if everyone could comment on this
  • there are not tests, at least for the normalization they would be useful
  • TODOs in the code

kubanmar and others added 2 commits February 17, 2026 13:47
* refactor convergence target schema

* add convergence test module

* generalize get_convergence_value

* revert Results removal

* rename to threshold_type

* fix convergence test module

* clean and ruff

* ruff format

* fix bool treatment in tests

* fix reference assignment

* doc string format fix

* docstring fixes

* update with parsing refactor

* ruff
@ndaelman-hu
Copy link
Collaborator

ndaelman-hu commented Feb 26, 2026

residuum is semantically inconsistent with other threshold_type values. The other four threshold_type values (absolute, relative, maximum, rms) are mathematical comparison methods that describe how to perform the convergence check - pure mathematical operations independent of the physical quantity being checked.

residuum is different: it specifies what to compare - the difference between the current value and the value estimated from the wavefunction, rather than the difference between successive iterations (general.py:54). This makes residuum quantity-specific (only applicable to density/wavefunction), while the other four are quantity-agnostic.

Additionally, residuum currently has no supporting implementation logic.

I recommend removing residuum from threshold_type. When codes report wavefunction/density residuals, create specialized convergence target classes:

class WavefunctionResidualConvergenceTarget(WorkflowConvergenceTarget):
    threshold = Quantity(type=np.float64, unit='coulomb')
    threshold_type = Quantity(...)  # Can still use absolute/rms/etc for the residual
    _convergence_property_path = 'scf_steps.wavefunction_residuals'

This maintains the clean semantic separation: class = physical quantity, threshold_type = mathematical comparison method.

Comment on lines +207 to +231
if isinstance(value, int | float | np.floating):
# Scalar value - use absolute or relative
if conv_type == 'absolute':
return self._check_absolute(value)
elif conv_type == 'relative':
# For relative, child class should provide reference
logger.warning(
f'Relative convergence requires reference value in '
f'{self.__class__.__name__}'
)
return None
else:
return self._check_absolute(value)

elif isinstance(value, np.ndarray):
# Array value - can use maximum or rms
if conv_type == 'maximum':
return self._check_maximum(value)
elif conv_type == 'rms':
return self._check_rms(value)
elif conv_type == 'absolute':
# For array, treat as maximum
return self._check_maximum(value)
else:
return self._check_maximum(value)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The `threshold_type` dispatch is coupled to data shape, creating artificial constraints where data shape dictates which comparison methods are valid rather than user intent or physical meaning. Why can't you check `absolute` on each array element, or `maximum` of a scalar?

Additionally, there's silent fallback behavior with no warnings when `threshold_type` doesn't match data shape:

  • Parser sets `threshold_type='maximum'` but data is scalar → silently uses `absolute` instead (line 219)
  • Parser sets `threshold_type='absolute'` but data is array → silently uses `maximum` instead (line 229)

The `threshold_type` should describe the mathematical operation independent of data shape.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reflecting more on it: The threshold_type enum conflates three orthogonal concerns:

  1. Data shape: scalar vs array
  2. Aggregation method: How to reduce vectors to comparable scalars (maximum, rms)
  3. Comparison type: How to compare values (absolute, relative)

These should be independent. For example:

  • You might want maximum aggregation of force components with relative convergence (current max force compared to previous max force)
  • Or rms aggregation of energies across a trajectory with absolute convergence

The current design forces specific pairings that may not match the physical quantity or convergence criteria desired.

A cleaner design might separate:

aggregation_method = MEnum('scalar', 'maximum', 'rms')  # How to reduce arrays
comparison_type = MEnum('absolute', 'relative')  # How to compare

@ndaelman-hu
Copy link
Collaborator

Question about workflow2.results structure

I noticed that archive.workflow2.results follows a nested subsection-with-quantities structure rather than the repeating section pattern used in archive.data.

Section-based structure (archive.data):

archive.data
  ├── model_system[] (repeating sections)
  ├── model_method[] (repeating sections)
  └── outputs[] (repeating sections)

Workflow structure (archive.workflow2):

archive.workflow2
  ├── method (subsection)
  ├── results (subsection with quantities)
  │     ├── is_converged (quantity)
  │     ├── final_energy_difference (quantity)
  │     ├── final_force_maximum (quantity)
  │     └── convergence[] (repeating subsection)
  └── tasks[] (repeating sections)

Question: Is the quantity structure `workflow2.results` meant to keep shadowing `archive.results`?

Understanding this would help clarify the design intent and whether there are legacy patterns that should be considered for future schema evolution.

@JFRudzinski

Change threshold type in all convergence target classes from `np.float64` to
`positive_float()` to enforce schema-level validation. This ensures thresholds
are non-negative (x ≥ 0), allowing zero thresholds but rejecting semantically
invalid negative values.

Updated classes:
- `EnergyConvergenceTarget`
- `ForceConvergenceTarget`
- `PotentialConvergenceTarget`
- `ChargeConvergenceTarget`

Add test to verify zero thresholds are accepted. Remove test for negative
thresholds as these are now prevented at the schema level.
@ladinesa
Copy link
Collaborator

Question about workflow2.results structure

I noticed that archive.workflow2.results follows a nested subsection-with-quantities structure rather than the repeating section pattern used in archive.data.

Section-based structure (archive.data):

archive.data
  ├── model_system[] (repeating sections)
  ├── model_method[] (repeating sections)
  └── outputs[] (repeating sections)

Workflow structure (archive.workflow2):

archive.workflow2
  ├── method (subsection)
  ├── results (subsection with quantities)
  │     ├── is_converged (quantity)
  │     ├── final_energy_difference (quantity)
  │     ├── final_force_maximum (quantity)
  │     └── convergence[] (repeating subsection)
  └── tasks[] (repeating sections)

Question: Is the quantity structure workflow2.results meant to keep shadowing archive.results?

Understanding this would help clarify the design intent and whether there are legacy patterns that should be considered for future schema evolution.

@JFRudzinski

It do not mean it to shadow archive.results. In the same spirit that archive.results contain a 'summary' of data, workflow.results is a summary of the workflow. I would like to think that there are quantities that are specific to workflow.results. .I concede that there are quantities are seemed to be duplicated e.g. in for geometry_optimization workflow, tolerances appear in both. But my opinion is this should not be the case. For me this is limited by the requirement that search indices and the gui are built from archive.results. This already came up from the new results normalizer discussion if we indeed rely on archive.results for everything or we can directly work with the data/ workfow sections.

@ndaelman-hu
Copy link
Collaborator

Question about workflow2.results structure
I noticed that archive.workflow2.results follows a nested subsection-with-quantities structure rather than the repeating section pattern used in archive.data.
Section-based structure (archive.data):

archive.data
  ├── model_system[] (repeating sections)
  ├── model_method[] (repeating sections)
  └── outputs[] (repeating sections)

Workflow structure (archive.workflow2):

archive.workflow2
  ├── method (subsection)
  ├── results (subsection with quantities)
  │     ├── is_converged (quantity)
  │     ├── final_energy_difference (quantity)
  │     ├── final_force_maximum (quantity)
  │     └── convergence[] (repeating subsection)
  └── tasks[] (repeating sections)

Question: Is the quantity structure workflow2.results meant to keep shadowing archive.results?
Understanding this would help clarify the design intent and whether there are legacy patterns that should be considered for future schema evolution.
@JFRudzinski

It do not mean it to shadow archive.results. In the same spirit that archive.results contain a 'summary' of data, workflow.results is a summary of the workflow. I would like to think that there are quantities that are specific to workflow.results.

I concede that there are quantities are seemed to be duplicated e.g. in for geometry_optimization workflow, tolerances appear in both. But my opinion is this should not be the case.

Thx for clarifying! These overlapping quantities is why I asked about the shadowing in the first place.

For me this is limited by the requirement that search indices and the gui are built from archive.results. This already came up from the new results normalizer discussion if we indeed rely on archive.results for everything or we can directly work with the data/ workfow sections.

I see. I raised this question since all other SCF data is now handled by sections. This is the only exception, and it is relevant to plotting. I was thinking off turning it also into sections for consistency and polymorphism. I had not yet considered how it would impact query performance (or possibility).

@JFRudzinski
Copy link
Collaborator

yes, 100% agree with @ladinesa ... there are some other developments of the results section and normalization that was met at the end of the day yesterday, but I will report at our next meeting

- Add `WavefunctionConvergenceTarget` class to `general.py` for tracking wavefunction coefficient convergence in SCF workflows
- Add `delta_wavefunction_rms` quantity to `SCFSteps` schema in `outputs.py` to store RMS changes of wavefunction coefficients
- Add comprehensive test suite covering edge cases: zero convergence, missing data, single iteration, NaN/Inf handling, negative values, boundary conditions, and array vs scalar data
- Use `positive_float()` for threshold validation (x ≥ 0)
- Set `_convergence_property_path` to `scf_steps.delta_wavefunction_rms` for automatic resolution

All 51 convergence target tests pass.
- Remove `residuum` from `threshold_type` enum (semantically inconsistent - describes WHAT not HOW)
- Remove unused imports (`Iterable`, `jmespath`, `SimulationTime`, `Outputs`)
- Simplify `WorkflowConvergenceTarget` docstring
@ndaelman-hu
Copy link
Collaborator

Resolved in commit 705b7d9.

Removed residuum from the threshold_type enum and created WavefunctionConvergenceTarget class for wavefunction convergence tracking (when parsers extract this data).

@ndaelman-hu
Copy link
Collaborator

Created follow-up issues from review discussion:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants