Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/cuda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ jobs:
if grep -qE '^[[:space:]]*device[[:space:]]+' "$f"; then
sed -i -E 's/^[[:space:]]*device[[:space:]]+.*/device gpu/' "$f"
else
echo "device gpu" >> "$f"
printf '\ndevice gpu\n' >> "$f"
fi
done
bash ../integrate/Autotest.sh -n 2 -j "$GPU_TEST_JOBS" -f CASES_GPU.txt
Expand All @@ -141,7 +141,7 @@ jobs:
if grep -qE '^[[:space:]]*device[[:space:]]+' "$f"; then
sed -i -E 's/^[[:space:]]*device[[:space:]]+.*/device gpu/' "$f"
else
echo "device gpu" >> "$f"
printf '\ndevice gpu\n' >> "$f"
fi
done
bash ../integrate/Autotest.sh -n 2 -j "$GPU_TEST_JOBS" -f CASES_GPU.txt
Expand All @@ -153,7 +153,7 @@ jobs:
if grep -qE '^[[:space:]]*device[[:space:]]+' "$f"; then
sed -i -E 's/^[[:space:]]*device[[:space:]]+.*/device gpu/' "$f"
else
echo "device gpu" >> "$f"
printf '\ndevice gpu\n' >> "$f"
fi
done
bash ../integrate/Autotest.sh -n 2 -j "$GPU_TEST_JOBS" -f CASES_GPU.txt
Expand All @@ -170,7 +170,7 @@ jobs:
if grep -q "^device" "$f"; then
sed -i 's/^device.*/device gpu/' "$f"
else
echo "device gpu" >> "$f"
printf '\ndevice gpu\n' >> "$f"
fi
done
bash ../integrate/Autotest.sh -n 1 -j "$GPU_TEST_JOBS" -a abacus -f CASES_GPU.txt
1 change: 1 addition & 0 deletions docs/developers_guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ This section provides guidelines and resources for developers working on the ABA

basic_types_class.md
input_availability.md
input_validation.md
agent_governance.md
72 changes: 72 additions & 0 deletions docs/developers_guide/input_validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Shared INPUT validation

INPUT contracts are declared on `Input_Item::domain`. The common engine checks
scalar integer, real, Boolean and string values, finite real numbers, choices,
numerical bounds, and conditional requirements. It never branches on parameter
names. For example:

```cpp
item.domain.set_choices({"1", "2"});
item.domain.set_rule("value>0 and value<=1");
item.domain.add_requirement("noncolin==true or lspinorb==true", "nspin==4");
```

These are independent examples of declarations. Scalar readers register their
actual numeric/Boolean type automatically. The existing `sync_*` registrations
supply typed, read-only value access, including nested MD fields and vectors.
The checker does not infer token cardinality from a prose documentation type,
or format final values and parse them back. Custom input grammars remain in
their readers until their contracts are declared; legal empty strings and
vectors must remain accepted.

Conditions reuse the parsed availability grammar (`and`, `or`, comparisons,
`in`, `contains`). Runtime requirements are explicit declarations: descriptive
availability alone does not imply that a disabled switch is an error.
References and operand types are checked when registrations are assembled.

## Preparation order

1. Check all explicit domains before readers or reset hooks can hide invalid
input. Complete tokens and scalar cardinality are checked; real numbers
must be finite. Independent errors are collected.
2. Read typed values, retain snapshots of supplied values, and run existing
default/reset hooks and canonicalization.
3. Evaluate conditional requirements with supplied values taking precedence
over prepared defaults, so reset hooks cannot hide a conflicting request.
4. Check resolved values and requirements using the same engine.

Errors identify the file, parameter, offending value, legal choices/rules, and
its manual URL. All ranks receive the error before parameter broadcasts.
Normal execution and `--check-input` share this preparation boundary. Existing
checks needing structure, build capabilities or calculation context continue
at their existing boundary.

A private unset initializer does not belong to the public domain. For example,
a `{1, 2}` domain rejects explicit `-1` before defaults; omission skips the
first check and the derived value must satisfy the same domain. A documented
negative disabled mode or negative physical value has different semantics.
An effective domain can have an activation condition for unused defaults;
explicit supplied values are still checked.

## Documentation and defaults

The normal numeric/Boolean reader registrations enforce scalar type contracts
automatically. Explicit choice/range/requirement declarations cover system,
electronic, relaxation, MD, vdW, output, DeePKS, DFPT and TDDFT inputs, including
`scf_thr_type`, `symmetry`, `mem_saver` and the spin dependency.

When writing reusable INPUT, an inactive value outside its public domain is
omitted: internal unset markers must not be presented as legal explicit input.
The value in memory is not changed.

Choices, value predicates and conditional requirements are exported to built-in
help, generated YAML and the INPUT manual from these declarations. Generated
YAML and Markdown are build artifacts and must not be committed. Recommended
values and bounds in a default formula are not automatically legal limits.

Validation does not itself resolve defaults or make every legacy reset safe.
Default resolution should distinguish presence from numerical value, and fill
only omitted inputs. In particular `0`, `false` and a legal negative value are
explicit values. Defaults requiring structure/orbitals must be resolved when
those dependencies exist. Runtime numeric fields need not become nullable:
a separate input-preparation state can represent missing and pending values.
9 changes: 9 additions & 0 deletions docs/generate_input_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ def generate_parameter_markdown(param: Dict[str, str],
type_text = escape_md_text(normalize_type(str(param['type'])))
lines.append(f"- **Type**: {type_text}")

# Legal values from the same domain used by runtime validation.
if param.get('allowed_values'):
choices = ', '.join(escape_md_text(value) for value in param['allowed_values'])
lines.append(f"- **Allowed values**: {choices}")

for key, title in [('value_rule', 'Legal range'), ('requirements', 'Requirements')]:
if param.get(key):
lines.append(f"- **{title}**: " + link_availability(str(param[key]), name_anchors))

# Availability (before description, as in original format)
if param.get('availability', '') != '':
lines.append("- **Availability**: *"
Expand Down
31 changes: 31 additions & 0 deletions docs/test_generate_input_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Checks for legal-value metadata in the generated INPUT reference."""

import unittest

from generate_input_main import generate_parameter_markdown


class InputDomainDocumentationTest(unittest.TestCase):
def test_integer_choices_include_zero(self):
rendered = generate_parameter_markdown(
{"name": "gga_grad", "type": "Integer", "allowed_values": [0, 1, 2]}, {}
)
self.assertIn("- **Allowed values**: 0, 1, 2", rendered)

def test_ranges_and_requirements_share_reference_links(self):
rendered = generate_parameter_markdown(
{"name": "mixing_beta", "type": "Real", "value_rule": "value>=0",
"requirements": "When noncolin==true: require nspin==4"},
{"noncolin": "#noncolin", "nspin": "#nspin"},
)
self.assertIn("**Legal range**", rendered)
self.assertIn("**Requirements**", rendered)
self.assertIn("[`nspin`](#nspin)", rendered)

def test_undeclared_domain_is_not_an_empty_restriction(self):
rendered = generate_parameter_markdown({"name": "legacy", "type": "Real"}, {})
self.assertNotIn("Allowed values", rendered)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions source/Makefile.Objects
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@ OBJS_IO=module_parameter/input_conv.o\
parse_args.o\
module_parameter/availability.o\
module_parameter/availability_validator.o\
module_parameter/input_domain.o\
module_parameter/read_input.o\
module_parameter/read_inp_sys.o\
module_parameter/read_inp_estruc.o\
Expand Down
1 change: 1 addition & 0 deletions source/source_io/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ add_library(
module_parameter/read_inp_out.cpp
module_parameter/availability.cpp
module_parameter/availability_validator.cpp
module_parameter/input_domain.cpp
module_parameter/read_input.cpp
module_parameter/read_set_globalv.cpp
)
Expand Down
40 changes: 40 additions & 0 deletions source/source_io/input_help.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ void ParameterHelp::build_registry() {
meta.default_value = item.default_value;
meta.unit = item.unit;
meta.availability = item.get_availability();
meta.allowed_values = item.domain.format_choices();
meta.value_rule = item.domain.format_rule();
meta.requirements = item.domain.format_requirements();

// Pre-compute lowercase name for fast fuzzy matching
meta.name_lowercase = to_lowercase(item.label);
Expand Down Expand Up @@ -399,6 +402,21 @@ bool ParameterHelp::show_parameter_help(const std::string& key, std::ostream& os
os << "Parameter: " << meta.name << "\n";
os << "Type: " << meta.type << "\n";

if (!meta.allowed_values.empty()) {
const std::string allowed_values = "Allowed values: " + meta.allowed_values;
os << word_wrap(allowed_values, MAX_WIDTH, INDENT_SIZE, 0) << "\n";
}

if (!meta.value_rule.empty()) {
const std::string value_rule = "Legal range: " + meta.value_rule;
os << word_wrap(value_rule, MAX_WIDTH, INDENT_SIZE, 0) << "\n";
}

if (!meta.requirements.empty()) {
const std::string requirements = "Requirements: " + meta.requirements;
os << word_wrap(requirements, MAX_WIDTH, INDENT_SIZE, 0) << "\n";
}

if (!meta.default_value.empty()) {
os << "Default: " << meta.default_value << "\n";
}
Expand Down Expand Up @@ -495,6 +513,28 @@ void ParameterHelp::generate_yaml(std::ostream& os) {
os << " category: " << yaml_quote_if_needed(item.category) << "\n";
os << " type: " << yaml_quote_if_needed(item.type) << "\n";

if (!item.domain.choices().empty()) {
os << " allowed_values: [";
const auto& choices = item.domain.choices();
for (size_t i = 0; i < choices.size(); ++i)
{
if (i != 0) { os << ", "; }
if (item.domain.kind() == AvailabilityValueKind::String)
{
os << yaml_quote_if_needed(choices[i]);
}
else { os << choices[i]; }
}
os << "]\n";
}

if (!item.domain.format_rule().empty()) {
os << " value_rule: " << yaml_quote_if_needed(item.domain.format_rule()) << "\n";
}
if (!item.domain.format_requirements().empty()) {
os << " requirements: " << yaml_quote_if_needed(item.domain.format_requirements()) << "\n";
}

// Description uses block scalar
os << " description: ";
if (item.description.empty()) {
Expand Down
3 changes: 3 additions & 0 deletions source/source_io/input_help.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ struct ParameterMetadata {
std::string category;
std::string unit; // Empty string if no unit
std::string availability; // Empty string if always available
std::string value_rule;
std::string requirements;
std::string allowed_values; // Rendered from the runtime domain; empty if not declared
std::string name_lowercase; // Pre-computed lowercase for fast fuzzy matching
};

Expand Down
Loading
Loading