Skip to content

Handle nscf outputs that lack the scf-Fermi stdout marker - #92

Open
elinscott wants to merge 7 commits into
aiidateam:mainfrom
elinscott:nscf-fermi-fallback
Open

Handle nscf outputs that lack the scf-Fermi stdout marker#92
elinscott wants to merge 7 commits into
aiidateam:mainfrom
elinscott:nscf-fermi-fallback

Conversation

@elinscott

@elinscott elinscott commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

get_fermi_energy_from_nscf extracts the scf Fermi energy from an nscf run by matching the stdout line (compare with: X eV, computed in scf). That companion line is not always present, so the helper silently returned None. The None then flowed through prepare_wannier90_pp_inputs into the wannier90 input parameters, where the .win writer finally failed with an opaque Invalid value passed, accepts only bools, ints, floats and strings.

As per the QE source (PW/src/print_ks_energies.f90, print_ks_ef_homolumo), the single-value marker the regex matches is emitted only for a metallic (smearing/tetrahedra), single-Fermi-energy nscf run: insulators print HOMO/LUMO instead, and constrained-magnetization (tot_magnetization) runs print a two-value variant that the regex does not match. This is governed by the occupation/spin type of the nscf run, not by the QE version — the print_ef_scf guard is unconditionally true for nscf runs, identically in the 7.4.1, 7.5 and 7.6-dev sources I checked.

Changes

  • get_fermi_energy_from_nscf falls back to the Fermi energy the parser extracted into the nscf's output_parameters when the stdout marker is absent, and its return type annotation is corrected to Optional[float] (it could already return None on a failed match). The fallback also handles constrained-magnetization runs, which report one Fermi level per channel, by taking max(fermi_up, fermi_down) as a conservative reference for the frozen windows.
  • The fallback guards on fermi_energy_units == "eV", mirroring get_fermi_energy, so a value in the wrong unit cannot be returned silently (the aiida-quantumespresso parser always stores eV, so this is a latent-trap guard rather than a behavioural change).
  • prepare_wannier90_pp_inputs raises a descriptive ValueError if the Fermi energy still resolves to None, instead of passing it through to the .win writer. The plotting helper get_workchain_fermi_energy (utils/workflows/plot/bands.py), whose nscf branches consume the same value, gains the same guard.
  • Unit tests for get_fermi_energy_from_nscf cover the marker-present path, the fallback to the parsed nscf Fermi energy, the spin-polarised max(up, down) branch, the None-when-nothing-available case, and the non-eV units guard.

Notes

The stdout marker remains the first choice where present, preserving current behaviour where the current machinery works (the marker reports the scf Fermi energy, which can differ from the nscf one).

Design question

Shouldn't this Fermi energy come from the nscf in the first place — always, not just as a fallback? The value feeds shift_energy_windows applied against the nscf eigenvalues, so surely its fermi level should be our reference? It will also typically be a more accurate reference, evaluated with better Brillouin-zone sampling cf. the scf.

However, using the nscf fermi level by default would be a breaking change, shifting the reference for every user rather than just skip-scf runs (cf. this PR, which keeps behaviour unchanged where the current machinery works.)

Testing

Exercised end-to-end against pw.x 7.6-dev nscf output: previously the wannier90 calculation excepted in presubmit; with the fallback the pp run receives the parsed Fermi energy and completes.

elinscott and others added 5 commits July 8, 2026 11:33
QE >= 7.6-dev no longer prints the "(compare with: ... computed in
scf)" line in nscf runs, so get_fermi_energy_from_nscf silently
returned None, which then crashed deep inside the .win writer with an
opaque "Invalid value" error. Fall back to the Fermi energy the parser
extracted from the nscf output itself, and raise loudly in
prepare_wannier90_pp_inputs if the value still resolves to None.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spin-polarised runs with a constrained total magnetization report
fermi_energy_up / fermi_energy_down instead of a single fermi_energy;
take the highest of the two (the electron chemical potential across
both channels).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The nscf fallback read output_parameters["fermi_energy"] with no units
check, unlike get_fermi_energy which returns None unless the units are
eV; add the same units guard so a non-eV value cannot be returned
silently. Reword the fallback's docstring/comments: the stdout marker is
governed by the nscf occupation/spin type (QE prints the single-value
"compare with" line only for a metallic, single-Fermi-energy nscf), not
by QE version, and max(up, down) is a conservative frozen-window
reference, not a chemical potential.

Also guard get_workchain_fermi_energy in plot/bands.py: its nscf
branches consume the same helper and could return None into the
band-plotting arithmetic, mirroring the guard already added in
prepare_wannier90_pp_inputs.
Cover marker-present (scf value used), marker-absent fallback to the
parsed nscf Fermi energy, the spin-polarised max(up, down) branch, the
None-when-unavailable case, and the non-eV units guard. Pure-function
tests with stubbed nodes; not executed under pytest in this environment
because the suite's autouse fixture requires a database profile.
test_scdm mocked the scf output_parameters with fermi_energy but no
fermi_energy_units, so get_fermi_energy returned None and the new guard
raised "Fermi energy resolved to None". Add the units key, and assert
that the Fermi energy reaches the wannier90 pp parameters, which the
test previously left unchecked.

Also reword both guards: they named the nscf workchain and the stdout
marker even when the scf branch produced the None, and said nothing
about what to change. They now name the workchain actually consulted and
the output_parameters keys it must carry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@elinscott

Copy link
Copy Markdown
Collaborator Author

test_scdm was failing here with ValueError: Fermi energy resolved to None. The mock is at fault, not the guard: its scf output_parameters set fermi_energy without fermi_energy_units, so get_fermi_energy returned None. Real data never looks like that — aiida-quantumespresso writes the value and its units together, and all 23 of its pw parser fixtures carry both. Before this PR that None went into the wannier90 parameters unchecked, so the test was asserting the behavior the PR removes.

Pushed: the mock gains its units, and the test now asserts the Fermi energy actually reaches wannier90.parameters.

Resolve the conflict in `prepare_wannier90_pp_inputs`: keep both main's
"a caller-supplied `fermi_energy` wins over reading the nscf" shortcut and
this branch's `fermi_source` record of which workchain supplied the energy.

- Fold main's shortcut together with the pre-existing "no workchain at all"
  branch, which does the same thing under a different guard.
- Set `fermi_source` to None when the energy came from the caller's
  parameters rather than from a workchain, and give that case its own
  guard message.
`prepare_wannier90_pp_inputs` picks the Fermi energy from the nscf run in
the context or from the caller's wannier90 parameters, and no test drove
either path: the existing `test_scdm` always has an scf run in context.

- Read from the nscf run when the parameters carry no `fermi_energy`.
- Keep the caller's `fermi_energy` when the parameters carry one, even
  though an nscf run is in the context.
- Reject a `fermi_energy` of None in the parameters, with a message that
  names the parameters rather than the nscf run.
@elinscott elinscott added this to the v2.9.0 milestone Aug 14, 2026
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.

1 participant