Skip to content

feat(input): validate integer choices from shared parameter metadata - #8192

Draft
Stardust0831 wants to merge 2 commits into
deepmodeling:developfrom
Stardust0831:feat/input-domain-validation
Draft

Stardust0831 wants to merge 2 commits into
deepmodeling:developfrom
Stardust0831:feat/input-domain-validation

Conversation

@Stardust0831

@Stardust0831 Stardust0831 commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Linked Issue

Related to #7719. That issue tracks software silently changing parameters explicitly supplied by the user. The intended policy is to reject invalid explicit input with actionable diagnostics and legal values, while allowing contextual defaults for omitted parameters. Valid explicit input must take precedence over defaults.

What's changed?

Introduce a general, reusable INPUT validation mechanism, starting with scalar integer choices. Each Input_Item declares its legal values and a typed, read-only field binding. The shared engine handles complete-token validation, checking effective values, and consistent diagnostics with legal choices and parameter-specific manual links. The same declaration also drives abacus -h, YAML export and generated Allowed values documentation.

Other scalar integer-choice parameters can reuse this mechanism by declaring their field binding and allowed choices. The validator has no parameter-name branches or per-parameter validation callbacks. nspin and gga_grad are the first two registrations of this shared mechanism; more parameters can migrate incrementally, and additional rule types can extend the common operators.

Migrate nspin (1, 2, 4) and gga_grad (0, 1, 2) to this shared path. For example, nspin 2abc previously became 2; it now fails with the supplied value, legal choices and a link to the relevant manual section. Extra tokens, overflow and missing values also produce errors. A final migrated keyword without a value is no longer overlooked. Unmigrated parameters retain their existing handling of empty strings, empty vectors and EOF; missing-value rules must come from the declared domain.

One prepare_input call validates explicit domains before readers/default hooks, runs existing input preparation, then validates effective domains including defaults. Independent domain errors are collected and broadcast before all MPI ranks stop. Normal startup and --check-input share this path. Legal explicit values, including gga_grad 0, and existing contextual defaults are preserved for the migrated parameters.

This is the first infrastructure slice, not a claim that all reset behavior in #7719 is fixed. Existing dependency checks remain, including the nspin=4 requirement with noncolin/lspinorb. Replacing arbitrary reset callbacks with defaults that only fill omitted fields, conditional rules, ranges and other domain types are later migrations. A legacy reset from one legal value to another is not detected by this first domain-only implementation. The existing gamma_only convenience behavior is unchanged.

Validation flow

Previously, readers and reset hooks ran before each parameter's check_value callback, where legal-value and dependency checks were mixed together. The two diagrams below form one execution sequence. Green = new shared domain checks; red = stop. Domain checks apply only to declared domains, currently nspin and gga_grad.

1. Check explicit input, then prepare parameter values

flowchart LR
    A["INPUT<br/>raw tokens"] --> B{"Explicit<br/>domain valid?"}
    B -- "No" --> E["Domain error<br/>stop"]
    B -- "Yes / omitted" --> C["Read values<br/>defaults / reset<br/>aliases"]
    C --> D["Effective<br/>parameters"]
    classDef added fill:#e6f4ea,stroke:#237a3b,stroke-width:2px,color:#111;
    classDef error fill:#fff0f0,stroke:#b42318,color:#111;
    class B added;
    class E error;
Loading

For a supplied domain-backed parameter, require exactly one complete integer from allowed_values. Omitted keywords skip this token check. The preparation box uses the existing readers, default/reset hooks and alias normalization.

2. Check effective values, then dependencies

flowchart LR
    A{"Effective<br/>domain valid?"} -- "Yes" --> B["Resolve context<br/>broadcast"]
    A -- "No" --> E["Domain error<br/>stop"]
    B --> C{"Dependencies<br/>OK?"}
    C -- "Yes" --> D["Run / finish<br/>check-input"]
    C -- "No" --> F["Existing error<br/>stop"]
    classDef added fill:#e6f4ea,stroke:#237a3b,stroke-width:2px,color:#111;
    classDef error fill:#fff0f0,stroke:#b42318,color:#111;
    class A added;
    class E,F error;
Loading

The effective-value check also covers defaults for omitted parameters. Both domain checks are coordinated by prepare_input; domain failures collect independent errors, report supplied/resolved values, legal choices and manual links, then broadcast diagnostics so all MPI ranks stop. After STRU/system context is available, the existing check_value callbacks check dependencies, conflicts and remaining legacy constraints. Their errors retain the existing reporting path.

Legal values and dependencies answer different questions:

INPUT example Legal-value check Dependency check Result
nspin 3 Fails: allowed values are 1, 2, 4 Not reached Shared diagnostic before any reset hook
gga_grad -1 Fails: allowed values are 0, 1, 2 Not reached Same diagnostic format, with the gga_grad manual link
nspin 2abc, nspin 2.0, or an overflowing integer Fails: a complete representable integer is required Not reached Report the original input and legal choices
nspin 2 4 Fails: a scalar requires exactly one value Not reached Reject the extra token
Bare nspin, including at EOF without a newline Fails: the required value is missing Not reached Report the missing value and legal choices
nspin +2 or nspin 02 Passes: both represent integer 2 Continue existing checks Accept these integer spellings
nspin 2 with noncolin 1 or lspinorb 1 Passes: 2 is a legal spin mode Fails: either feature requires nspin=4 Existing dependency error; neither explicit value is rewritten
nspin 4 with noncolin 1 or lspinorb 1 Passes Passes this spin dependency Continue the other checks
Omitted nspin with noncolin 1 No explicit-token check; the current default 1 passes its domain Fails the same dependency Existing behavior is retained; this PR does not introduce automatic nspin=4 selection
gga_grad 0 Passes: explicit zero is a legal value No new dependency rule is added Preserve 0
nspin 1 with gga_grad 2 Both values pass Applicability is not converted into a new hard dependency Preserve the supplied values and existing computational behavior
Omitted nspin and gga_grad, with spin flags off No explicit-token checks; effective defaults 1 and 0 pass Passes this spin dependency Use the existing defaults

Omitted keywords do not participate in the explicit-token check; their effective defaults are checked later. The default/reset box deliberately denotes the existing implementation: full isolation of explicit values from all legacy reset hooks is still pending. The documentation's availability expressions are not automatically enforced as hard dependency errors in this PR.

Diagnostic format and rule review

Actual output when the same INPUT contains nspin 3 and gga_grad -1:

Invalid parameters in INPUT:
Invalid INPUT parameter 'nspin': supplied '3'. Expected one Integer from {1, 2, 4}.
See https://abacus.deepmodeling.com/en/latest/advanced/input_files/input-main.html#nspin
Invalid INPUT parameter 'gga_grad': supplied '-1'. Expected one Integer from {0, 1, 2}.
See https://abacus.deepmodeling.com/en/latest/advanced/input_files/input-main.html#gga-grad

The format identifies the input file, parameter, supplied value, legal choices and the corresponding manual section. Independent domain errors are reported together. This standard format currently covers declared-domain failures; legacy dependency errors have not been migrated to it.

The two choice sets exactly match the pre-existing guards and the published manual. The new token rules require one complete scalar integer, as documented; +2 and 02 remain accepted integer spellings. gga_grad applicability is not turned into a hard prohibition: for example, nspin 1 with gga_grad 2 remains accepted, with its existing computational behavior. The existing implication noncolin || lspinorb => nspin == 4 remains unchanged.

Compatibility risks and automatic values

Both new domain checks run during INPUT preparation, before calculation starts. Built-in initial values already exist when the parameter object is constructed. The sequence is: validate supplied raw tokens, read explicit values and run existing default/reset hooks, then validate the values produced by that preparation. Structure/system context and the legacy dependency checks follow. The second check is not a check on every solver iteration, and some derived values are only available later.

The main compatibility change is stricter parsing for migrated parameters: an input such as nspin 2.0, nspin 2abc or nspin 2 4, previously accepted through partial parsing, now fails. Existing scripts that emit these forms must emit a scalar integer. Optional empty values for unmigrated parameters remain valid. The shared preparation code is exercised by the serial writer/reader round trip, MPI parameter tests and the full CPU/GPU CI suites. No numerical kernels are changed; the added checks run at startup.

This slice uses the same integer-choice set for raw and prepared values. That is suitable for nspin and gga_grad, which have no automatic-value sentinel. Parameters using automatic/default sentinels must not be migrated mechanically to this operator. In particular:

Existing behavior Implication for later migration
scf_thr_type=-1 and scf_thr=-1 are resolved by the current reader to contextual defaults Review and document whether the sentinel is a public automatic choice; distinguish accepted input modes from the required resolved value
scf_ene_thr=-1 disables the extra energy-convergence criterion; nbspline=-1 disables B-spline interpolation A negative disabled-mode value can remain valid after preparation; it is not necessarily an unresolved default
nbands=0 requests automatic band selection after the electron count is known Validate the concrete derived result at the stage where the required context is available, using the shared rule engine
A negative initializer is only an internal placeholder Do not infer that the same number is a public legal INPUT value merely from the initializer

A later shared extension should represent omitted input, an explicit automatic request and an explicit concrete value separately, and distinguish input-domain rules from resolved-value rules. Resolve defaults for omissions or a declared automatic request; preserve valid explicit concrete values, including zero/false. A documented automatic token is an instruction to derive a value. Unknown negative values must not silently become automatic requests. Defaults that require later context need an appropriate shared validation stage. These capabilities are not implemented in this first PR.

The existing reset callbacks are still a migration limitation: checking membership alone cannot detect one legal explicit value being replaced by another legal value. Likewise, the applicability descriptions are not sufficient on their own to introduce new hard rejection rules.

Unit Tests and/or Case Tests for my changes

Local builds used GCC 13.4, CPU/OpenMP, OMP_NUM_THREADS=1, and the existing Conda numerical libraries. Built both serial and MPI executables with LCAO/LIBXC/JSON disabled.

Passed:

cmake --build /tmp/abacus-input-domain-build --target MODULE_IO_input_domain MODULE_IO_input_domain_reader MODULE_IO_read_item_serial MODULE_IO_read_input_serial MODULE_IO_availability abacus_pw_omp -j 12
ctest --test-dir /tmp/abacus-input-domain-build --output-on-failure -R '^MODULE_IO_(input_domain(_reader)?|read_item_serial|read_input_serial|availability)$'
python3 -m unittest discover -s docs -p test_generate_input_main.py -v
x86_64-conda-linux-gnu-g++ -std=c++11 -Isource -fsyntax-only source/source_io/module_parameter/input_domain.cpp

All five CTest targets (55 individual tests) and both documentation tests passed. Full GoogleTest completion was verified. A subsequent rule audit reran the 13 domain/reader unit tests and verified 41 CLI input cases, including all nine legal spin/gradient combinations, malformed and missing tokens, default/explicit values and spin dependencies. The published manual page returned HTTP 200, and its nspin, gga-grad and symmetry anchors were confirmed. Tests cover complete tokens, cardinality, overflow, enum membership, typed bindings, copied metadata, errors before reset hooks, contextual defaults, existing spin dependencies, valid empty optional values and reading writer-generated INPUT files. The check-input death test now enables check_mode only in its child process, so later tests finish and failures remain visible to CTest.

CLI verification used /tmp/abacus-input-domain-build/abacus_pw_omp:

  • --version: v3.11.0-beta11.
  • -h nspin: displays Allowed values: 1, 2, 4.
  • --generate-parameters-yaml, followed by python3 docs/generate_input_main.py /tmp/abacus-input-domain-cli/parameters.yaml --output /tmp/abacus-input-domain-cli/input-main.md: both domains verified in parsed YAML and generated Markdown.
  • --check-input from an isolated copy of tests/01_PW/004_PW_UPF201_Si: omitted defaults and explicit nspin 2 / gga_grad 0 exit 0. Out-of-domain, malformed and missing values exit 1 with the expected diagnostics; normal startup also rejects invalid input.
  • Sentinel compatibility smoke check: a Si case with scf_thr_type=-1, scf_thr=-1, scf_ene_thr=-1, nbspline=-1, search_radius=-1 and nbands=0 passed both --check-input and normal SCF. INPUT.info records prepared values 1, 1e-9, -1, -1, -1, 0; the running log records automatic NBANDS=14 and completed SCF. This PW check confirms that the input reader accepts the legacy values; it does not exercise LCAO search-radius resolution.

MPI verification:

cmake --build /tmp/abacus-input-domain-mpi-build --target abacus_pw_para MODULE_IO_input_test_para -j 12
mpirun -np 2 /tmp/abacus-input-domain-mpi-build/source/source_io/test/MODULE_IO_input_test_para --gtest_filter=InputParaTest.GgaGradAcceptedRange
mpirun -np 2 /tmp/abacus-input-domain-mpi-build/abacus_pw_para

The focused test passed on both ranks. The isolated Si case completed SCF and exited 0. A follow-up two-rank Si SCF also completed with empty out_pchg and dfpt_qfile. Combined nspin 3 / gga_grad -1 and bare nspin at EOF exited 1 with diagnostics, within a 30-second timeout. The existing MPI --check-input path prints success but calls exit(0) without finalization, so OpenMPI reports improper termination; this pre-existing behavior was not changed.

git diff --check and python3 tools/03_code_analysis/agent_governance_check.py --staged passed. The initial infrastructure commit had three header-include review warnings, explained below; the compatibility follow-up had no governance findings.

Governance Notes

  • INPUT/docs changes: legal choices live in the C++ registrations and generate help/YAML/Markdown; generated artifacts are not committed. Added a developer guide describing the preparation order and migration limits.
  • Core module impact: input preparation and help metadata only; no solver, Hamiltonian or numerical-kernel changes. CMake, serial tests and the legacy Makefile object list include the new source; pyabacus source lists do not compile this reader.
  • Header dependencies: InputDomain needs standard string/vector declarations and owns its vector; Input_Item owns a domain by value and therefore needs the complete type. Input_para is forward-declared in the domain header.
  • Code-quality scoring: new domain implementation/header score 100/98. Three existing large implementation files remain below 60; the baseline comparison confirms their pre-existing size/complexity/global-dependency debt.
  • Exceptions requested: none.

This branch has not been deployed

No deployments
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