Skip to content

implement NonlocalCorrelation#328

Open
EBB2675 wants to merge 16 commits intodevelopfrom
nonlocal-correlation
Open

implement NonlocalCorrelation#328
EBB2675 wants to merge 16 commits intodevelopfrom
nonlocal-correlation

Conversation

@EBB2675
Copy link
Collaborator

@EBB2675 EBB2675 commented Feb 17, 2026

Purpose

This PR introduces a dedicated NonlocalCorrelation schema section and integrates it into DFT normalization for nonlocal-correlation add-ons (+VV10 / +rVV10). It also standardizes XC partner linking through typed references.

Scope

Included
1. Added NonlocalCorrelation class

  • Introduced a schema section to represent nonlocal correlation terms explicitly.
  • Supports variants such as VV10, rVV10, vdW-DF, vdW-DF2, vdW-DF-cx, optB88-vdW, and optB86b-vdW.
  • Includes:
    • type: normalized nonlocal-correlation identity
    • xc_partner_ref: reference to the paired XCFunctional

2. Integrated nonlocal add-on handling into DFT.normalize

  • Parses xc.functional_key for trailing +VV10 / +rVV10.
  • Strips the add-on from the base XC key (e.g. SCAN + rVV10 -> SCAN).
  • Reuses an existing matching NonlocalCorrelation contribution when possible.
  • Creates a new NonlocalCorrelation(type=...) contribution when needed.
  • Sets NonlocalCorrelation.xc_partner_ref to the active dft.xc.

3. Standardized XC partner linkage for empirical dispersion

  • EmpiricalDispersionModel now uses xc_partner_ref as well.
  • DFT.normalize sets EmpiricalDispersionModel.xc_partner_ref (when missing) to the active dft.xc.

4. Removed legacy partner string field

  • Removed the old xc_partner string-based partner field/usage.
  • Partner linkage is now reference-based only (xc_partner_ref).

Why no libvdwxc canonicalization layer in this PR

I could not add a libvdwxc canonical-label mapping because, unlike LibXC, there is no stable and broadly adopted canonical label/ID registry equivalent to LibXC’s taxonomy. This PR therefore keeps nonlocal normalization schema-level (NonlocalCorrelation.type) and parser-driven.

Context & Links

  • For inputs like SCAN + rVV10, normalization now results in:

xc.functional_key == 'SCAN'
one NonlocalCorrelation(type='rVV10')
partner linkage to the base XC via xc_partner_ref

Reviewer Notes

We currently infer nonlocal correlation only from functional_key patterns +VV10 / +rVV10 (via split_xc_and_addons()), but NonlocalCorrelation.type also includes vdW-DF variants (vdW-DF, vdW-DF2, vdW-DF-cx, optB88-vdW, optB86b-vdW). Should we (a) extend inference to recognize these variants in functional_key, (b) restrict the enum to what we infer automatically (VV10/rVV10) and expect vdW-DF variants to be set explicitly by parsers, or (c) keep both but document that only VV10/rVV10 are inferred today? @ndaelman-hu

If an existing NonlocalCorrelation contribution conflicts with what we infer from functional_key, we currently create a second entry, which is not ideal. What would you suggest? @ndaelman-hu

Status

  • Ready for review

Breaking Changes

  • None

Dependencies / Blockers

  • None

Testing / Validation

How was this change validated?

  • Unit tests

  • Integration tests

  • Added/updated tests in tests/test_model_method.py to cover:

  • creation of NonlocalCorrelation from SCAN + rVV10

  • reuse of existing matching NonlocalCorrelation(type='VV10')

  • filling missing NonlocalCorrelation.type

  • linking NonlocalCorrelation.xc_partner_ref to dft.xc

  • linking EmpiricalDispersionModel.xc_partner_ref to dft.xc

@EBB2675 EBB2675 changed the title implement NonlocalCorrelation` implement NonlocalCorrelation Feb 17, 2026
@coveralls
Copy link

coveralls commented Feb 17, 2026

Pull Request Test Coverage Report for Build 22352739452

Details

  • 90 of 92 (97.83%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.1%) to 83.421%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/nomad_simulations/schema_packages/model_method.py 24 25 96.0%
src/nomad_simulations/schema_packages/utils/xc_addons.py 15 16 93.75%
Totals Coverage Status
Change from base Build 22222032692: 0.1%
Covered Lines: 7623
Relevant Lines: 9138

💛 - Coveralls

@EBB2675 EBB2675 linked an issue Feb 17, 2026 that may be closed by this pull request
@EBB2675 EBB2675 marked this pull request as ready for review February 17, 2026 15:05
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a dedicated NonlocalCorrelation schema section to represent nonlocal correlation functionals (VV10/rVV10, vdW-DF families) and integrates it into DFT normalization. The implementation automatically parses and strips nonlocal addons (e.g., +VV10, +rVV10) from XC functional keys, creates or reuses corresponding NonlocalCorrelation contributions, and establishes typed references between contributions and their paired XC functionals.

Changes:

  • Added NonlocalCorrelation class with typed xc_partner_ref for linking to base XC functional
  • Implemented split_xc_and_addons utility to parse nonlocal addons from functional keys
  • Modified DFT.normalize to handle addon parsing, contribution creation/reuse, and partner reference linking
  • Replaced string-based xc_partner with typed xc_partner_ref in ExplicitDispersionModel
  • Removed obsolete fields (is_embedded_in_xc, nonlocal_kernel) and enum values from ExplicitDispersionModel

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
src/nomad_simulations/schema_packages/utils/xc_addons.py New utility module for parsing nonlocal correlation addons from XC keys
src/nomad_simulations/schema_packages/model_method.py Added NonlocalCorrelation class, updated ExplicitDispersionModel, modified DFT.normalize for addon handling
tests/test_model_method.py Added tests for addon parsing, contribution creation/reuse, and partner reference linking

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +639 to +647
existing_nonlocal = None
for contribution in self.contributions or []:
if not isinstance(contribution, NonlocalCorrelation):
continue
if contribution.type in (None, nonlocal_corr_addon):
existing_nonlocal = contribution
break

if existing_nonlocal is None:
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The reuse logic creates a new NonlocalCorrelation contribution when an existing one has a different type (e.g., existing type='VV10' but addon='rVV10'). This results in multiple nonlocal correlation contributions with different types, which may indicate a parser error or conflict between explicitly created contributions and the functional_key. Consider logging a warning when this occurs to help identify configuration issues.

Suggested change
existing_nonlocal = None
for contribution in self.contributions or []:
if not isinstance(contribution, NonlocalCorrelation):
continue
if contribution.type in (None, nonlocal_corr_addon):
existing_nonlocal = contribution
break
if existing_nonlocal is None:
existing_nonlocal = None
conflicting_nonlocal_types = set()
for contribution in self.contributions or []:
if not isinstance(contribution, NonlocalCorrelation):
continue
if contribution.type in (None, nonlocal_corr_addon):
existing_nonlocal = contribution
break
if contribution.type is not None:
conflicting_nonlocal_types.add(contribution.type)
if existing_nonlocal is None:
if conflicting_nonlocal_types:
logger.warning(
"DFT.xc nonlocal correlation type conflict detected; "
"creating additional NonlocalCorrelation contribution.",
existing_nonlocal_types=sorted(conflicting_nonlocal_types),
requested_nonlocal_type=nonlocal_corr_addon,
functional_key=getattr(self.xc, "functional_key", None),
)

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

thx for the suggestion, i will wait for nathan's comment

Comment on lines +5 to +8
_NONLOCAL_SUFFIX_RE = re.compile(
r'^(?P<base>.+?)\s*\+\s*(?P<addon>RVV10|VV10)\s*$',
re.IGNORECASE,
)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The regex pattern uses .+? for the base functional which requires at least one character, but it also strips the result. This means inputs like "+ VV10" (with leading +) will not match. While this is likely intentional, consider documenting this behavior or adding validation to handle malformed inputs more explicitly.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +16
def split_xc_and_addons(raw: str) -> tuple[str | None, str | None, str | None]:
"""
Split an XC key into baseline XC and optional add-ons.

Returns:
(base_xc_key, explicit_dispersion_suffix, nonlocal_corr_addon)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The function signature indicates it should return (base_xc_key, explicit_dispersion_suffix, nonlocal_corr_addon), but the explicit_dispersion_suffix is always None in the implementation. This suggests the function is designed for future expansion to handle explicit dispersion addons like '+D3', '+D4', etc., but this is not currently implemented. Consider either implementing this functionality or removing the unused return value to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +8
_NONLOCAL_SUFFIX_RE = re.compile(
r'^(?P<base>.+?)\s*\+\s*(?P<addon>RVV10|VV10)\s*$',
re.IGNORECASE,
)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The regex pattern only captures 'VV10' and 'RVV10' (case-insensitive). However, the NonlocalCorrelation.type enum includes many more variants: 'vdW-DF', 'vdW-DF2', 'vdW-DF-cx', 'optB88-vdW', 'optB86b-vdW'. If these are meant to be parsed from functional keys like 'PBE+vdW-DF', the regex needs to be extended. If they are only set explicitly by parsers, this should be documented in the function's docstring.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +37
def split_xc_and_addons(raw: str) -> tuple[str | None, str | None, str | None]:
"""
Split an XC key into baseline XC and optional add-ons.

Returns:
(base_xc_key, explicit_dispersion_suffix, nonlocal_corr_addon)

Notes:
- For now, only nonlocal-correlation add-ons are recognized: +VV10 / +rVV10.
- Unknown forms are left untouched as the base XC key.
"""
if raw is None:
return None, None, None

text = raw.strip()
if not text:
return None, None, None

match = _NONLOCAL_SUFFIX_RE.match(text)
if not match:
return text, None, None

base_xc_key = match.group('base').strip() or None
addon_raw = match.group('addon').upper()
nonlocal_corr_addon = 'rVV10' if addon_raw == 'RVV10' else 'VV10'

return base_xc_key, None, nonlocal_corr_addon
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The function returns (base_xc_key, None, nonlocal_corr_addon) for valid nonlocal addons, or (text, None, None) for inputs without recognized addons. However, there's no validation or logging for malformed inputs like "PBE++VV10" (double plus), "VV10+PBE" (reversed order), or "PBE+VV10+rVV10" (multiple addons). Consider adding validation or documenting expected input format constraints.

Copilot uses AI. Check for mistakes.
EBB2675 and others added 2 commits February 24, 2026 14:23
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@EBB2675 EBB2675 mentioned this pull request Feb 26, 2026
11 tasks
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +11 to +23
def split_xc_and_addons(raw: str) -> tuple[str | None, str | None, str | None]:
"""
Split an XC key into baseline XC and optional add-ons.

Returns:
(base_xc_key, explicit_dispersion_suffix, nonlocal_corr_addon)

Notes:
- For now, only nonlocal-correlation add-ons are recognized: +VV10 / +rVV10.
- Unknown forms are left untouched as the base XC key.
"""
if raw is None:
return None, None, None
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

split_xc_and_addons is annotated as raw: str, but the implementation explicitly handles raw is None and callers (e.g. DFT.normalize) can pass XCFunctional.functional_key which may be None. This will trip mypy; please change the signature to accept str | None (and adjust the return annotation/docstring accordingly).

Copilot uses AI. Check for mistakes.
type=str,
xc_partner_ref = Quantity(
type=Reference(SectionProxy('XCFunctional')),
description="Base XC functional used/tuned for (e.g. 'PBE', 'SCAN', 'B3LYP').",
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

xc_partner_ref is now a Reference(XCFunctional), but the description still reads like this is a string value (e.g. 'PBE', 'SCAN'). Please update the description to clearly state it stores a reference to the partner XCFunctional section (and, if needed, where to find its name/key).

Suggested change
description="Base XC functional used/tuned for (e.g. 'PBE', 'SCAN', 'B3LYP').",
description=(
"Reference to the partner XCFunctional section used or tuned for this "
"dispersion model. The corresponding functional label (e.g. 'PBE', "
"'SCAN', 'B3LYP') is stored in the referenced XCFunctional entry."
),

Copilot uses AI. Check for mistakes.
@EBB2675 EBB2675 requested a review from ndaelman-hu February 26, 2026 12:31
@ndaelman-hu
Copy link
Collaborator

This PR (among other things) semantizes NonlocalCorrelation in our schema, giving it clearer visiblity and standardization.

We currently infer nonlocal correlation only from functional_key patterns +VV10 / +rVV10 (via split_xc_and_addons()), but NonlocalCorrelation.type also includes vdW-DF variants (vdW-DF, vdW-DF2, vdW-DF-cx, optB88-vdW, optB86b-vdW). Should we (a) extend inference to recognize these variants in functional_key, (b) restrict the enum to what we infer automatically (VV10/rVV10) and expect vdW-DF variants to be set explicitly by parsers, or (c) keep both but document that only VV10/rVV10 are inferred today? @ndaelman-hu

Good questions — I did some (admittedly cursory) research into how codes report these functionals. I may well be missing cases, so take this with a grain of salt.

From what I could find, no more than one or two programs or libraries actually attach a nonlocal correlation tag to the functional name. It's more common in the literature, but most codes — VASP, QE, SIESTA, ABINIT — set it through separate input fields or flag combinations. So in practice it may be better to just have the parser set NonlocalCorrelation explicitly?

I understand the impulse to automate this and make life easier for parser developers, but the automation may be somewhat premature here. That said, I don't have a strong opinion on this question by itself — it's the second one that gives me a clearer preference.

If an existing NonlocalCorrelation contribution conflicts with what we infer from functional_key, we currently create a second entry, which is not ideal. What would you suggest? @ndaelman-hu

I'm not entirely sure what you mean by creating multiple entries (more on that below), but it sounds like it could be a deeper-lying bug that would take significant time to trace. It may just be easier to remove the normalization logic and defer to the parser — in that case there should only ever be one NonlocalCorrelation section.

We could of course retain the inference if it's much more commonly needed in the QC community than my quick search suggests. But then we'd need to investigate the duplication or design cleanup logic to merge entries, which is more involved.

Question to @EBB2675

With "a second entry" do you mean two separate archives or two subsections within one? And did you notice this when running a parsing example? I ask because the behavior sounds in line with issues I've seen with the mapping annotations parser — I could see that being the underlying cause.

@EBB2675
Copy link
Collaborator Author

EBB2675 commented Feb 26, 2026

From what I could find, no more than one or two programs or libraries actually attach a nonlocal correlation tag to the functional name. It's more common in the literature, but most codes — VASP, QE, SIESTA, ABINIT — set it through separate input fields or flag combinations. So in practice it may be better to just have the parser set NonlocalCorrelation explicitly?

that sounds nicer, i will remove these normalizations and re-tag you for final look 👍

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.

Extend DFT classification with libvdwxc

4 participants