From 11ea119405bfa61b6aeb6926ab5399f52bc493e5 Mon Sep 17 00:00:00 2001 From: SY Wang Date: Sun, 20 Sep 2026 01:56:56 +0800 Subject: [PATCH] Refactor: replace the JSON path walker with schema operations --- docs/advanced/json/json_add.md | 188 +++++-------- docs/advanced/json/json_para.md | 17 +- source/source_io/module_json/abacusjson.cpp | 76 +----- source/source_io/module_json/abacusjson.h | 23 +- source/source_io/module_json/general_info.cpp | 24 +- source/source_io/module_json/init_info.cpp | 23 +- source/source_io/module_json/json_node.h | 23 -- source/source_io/module_json/output_info.cpp | 95 ++++--- .../module_json/test/para_json_test.cpp | 253 ++++++------------ 9 files changed, 251 insertions(+), 471 deletions(-) delete mode 100644 source/source_io/module_json/json_node.h diff --git a/docs/advanced/json/json_add.md b/docs/advanced/json/json_add.md index 6040328843..2ec0ee9d2c 100644 --- a/docs/advanced/json/json_add.md +++ b/docs/advanced/json/json_add.md @@ -2,181 +2,133 @@ ## Overview -ABACUS uses [nlohmann-json](https://github.com/nlohmann/json) as the backend for its optional JSON output. The JSON implementation is kept under `source/source_io/module_json`, with `AbacusJson` providing the small interface used to build and write `abacus.json`. +ABACUS uses [nlohmann-json](https://github.com/nlohmann/json) for its optional JSON output. The implementation lives in `source/source_io/module_json` and uses `Json::jsonValue`, an alias for `nlohmann::ordered_json`, to retain object-key insertion order. -The public alias and mutation interfaces are: +`AbacusJson` provides access to the shared document and writes it to a file. Its declarations are in namespace `Json`: ```cpp using jsonValue = nlohmann::ordered_json; -// Public static members of Json::AbacusJson: -static void set_json(const std::vector& keys, jsonValue value); -static void append_json(const std::vector& keys, jsonValue value); -``` - -`jsonValue` uses `nlohmann::ordered_json` so that object keys are written in insertion order. `jsonKeyNode` accepts either a string key or an integer array index, so paths can mix JSON objects and arrays. - -`abacusjson.h` includes only `nlohmann/json_fwd.hpp`. A source file that constructs or operates on `jsonValue` must include `` itself, inside the `__JSON` guard. Callers of the higher-level functions in `init_info.h` and `output_info.h` do not need the backend header. - -## Adding values - -### Add or replace an object member - -Use `set_json()` to assign a value at a path: +class AbacusJson +{ + public: + static jsonValue& document(); + static void write_to_json(const std::string& filename); -```cpp -Json::AbacusJson::set_json({"general_info", "version"}, version); + private: + static jsonValue doc; +}; ``` -Missing intermediate named nodes are created as objects. The final value is replaced regardless of its previous type, including when it is an array or an object. For example, setting a complete coordinate array replaces the old coordinates rather than adding another nested array: +Keep the document root an object. Its state remains shared within each process; this change does not introduce independent output contexts or make concurrent writes safe. The mutable accessor is for the schema generators and tests in `module_json`. Other modules should continue to pass data to functions such as `add_output_energy()` instead of directly editing the document. -```cpp -Json::AbacusJson::set_json({"init", "coordinate"}, coordinates); -``` +The old path-component type and generic set/append interface have been removed. Use native object assignment, shallow `update()`, and array `push_back()` inside the schema generators; do not introduce another generic path wrapper. -Replacing a complete object also replaces all of its members; this is not a merge operation. +`abacusjson.h` includes only `nlohmann/json_fwd.hpp`. Source files that construct or manipulate JSON values must include `nlohmann/json.hpp` under `__JSON`. The existing CMake option `ENABLE_JSON` controls this feature. Callers using only the higher-level declarations in `init_info.h` or `output_info.h` do not need the backend header. -### Append to an array +## Constructing metadata -Use `append_json()` to append one value to an array: +`gen_general_info()` owns the whole `general_info` section and assigns it as a complete object: ```cpp -Json::AbacusJson::append_json({"init", "label"}, label); +AbacusJson::document()["general_info"] = { + {"version", version}, + {"commit", commit}, + {"device", param.inp.device}, + {"mpi_num", mpi_num}, + {"omp_num", omp_num}, + {"pseudo_dir", param.inp.pseudo_dir}, + {"orbital_dir", param.inp.orbital_dir}, + {"stru_file", param.globalv.global_in_stru}, + {"kpt_file", param.inp.kpoint_file}, + {"start_time", start_time_str}, + {"end_time", end_time_str}}; ``` -A missing final named member is created as an array. An existing destination must already be an array: appending to a scalar, an object, or `null` is an error rather than an implicit conversion. - -For nested arrays, construct the value with `jsonValue::array()`: +The `init` section is shared by `gen_stru()`, `gen_init()`, and `add_nkstot()`. The first two construct the fields they own in a local object, then apply a **shallow** update: ```cpp -Json::jsonValue coordinate = Json::jsonValue::array({x, y, z}); -Json::AbacusJson::append_json({"init", "coordinate"}, coordinate); +// Inside init_info.cpp; init_section() is local to this source file. +init_section().update(info); ``` -The coordinate is appended as **one row**; its elements are not flattened into the destination array. An empty path is a no-op for both `set_json()` and `append_json()`. - -### Construct objects and arrays - -Use the nlohmann-json initializer syntax through the `Json::jsonValue` alias. There is no need for backend-specific helper macros. - -Object example: - -```cpp -Json::jsonValue scf = { - {"energy", energy}, - {"ediff", ediff}, - {"drho", drho}, - {"time", time}, -}; -``` +The local helper creates a missing `init` object but rejects an existing non-object, including `null`. The update preserves fields supplied by the other generators and replaces each supplied value as a whole. In particular, per-species maps and coordinate arrays must not retain stale entries or accumulate on repeated generation. Do not assign a newly generated object to the entire `init` section, and do not enable recursive object merging here. -Array example: +`add_nkstot()` only sets its own field: ```cpp -Json::jsonValue row = Json::jsonValue::array({x, y, z}); +init_section()["nkstot"] = nkstot; ``` -Append a completed SCF record with: +## Output-record lifecycle -```cpp -Json::AbacusJson::append_json({"output", -1, "scf"}, scf); -``` +The workflow starts each record with `init_output_array_obj()` **before** the corresponding solver writes SCF or other result data. That function alone creates the `output` array and appends the initial record. It rejects an existing `output` value that is not an array; an explicit `null` is not treated as a missing field. -Construct complete sections or arrays locally before storing them where practical. `gen_general_info()` assigns its complete section once. `gen_stru()` constructs each structure field locally, and `gen_init()` does the same for calculation metadata. These two generators share `init` with `add_nkstot()`, so they replace only their own fields through a file-local helper; they must not replace the entire `init` object and discard fields written by another generator. +The existing workflow entry points own this initialization: -For a current output record, coordinates, magnetic moments, the cell, forces, and stress are replaced as complete arrays. Repeating the geometry update for the same record therefore does not accumulate extra rows. Only genuinely sequential data, such as `output` records and `scf` iteration records, use `append_json()`. +| Workflow | Record initialization | +| --- | --- | +| SCF/relaxation | `Relax_Driver::iter_info()` starts the record, except for the first `ks-lr` step described below. | +| `ks-lr` | `ESolver_LR::before_all_runners()` starts the record before its embedded KS calculation; the first relaxation-driver step reuses it. | +| UnitCell-backed MD | `Run_MD::md_line()` starts a record at the beginning of each MD iteration when `mdcell.has_backing_unitcell()` is true. | +| Socket/i-PI | `SocketHandlers::handle_posdata()` starts a record before running the solver for the received `POSDATA` frame. | -## Addressing array elements +Do not move record creation into individual field writers, create a second record for the same step, or reset the whole document to start a new step. -Integer path components address existing array elements. Non-negative indices count from the beginning, while negative indices count from the end (`-1` is the last element). Indexed traversal never grows an array. +The result writers use `current_output()`, a helper local to `output_info.cpp`. It rejects a missing or non-array `output`, an empty array, or a final element that is not an object. It never creates a record as a side effect of writing a result. -For example, given: +For example, inside namespace `Json` in `output_info.cpp`: -```json +```cpp +void add_output_energy(const double energy) { - "Json": { - "key6": { - "key7": [ - {"a": 1, "new": 2}, - "vasp", - "abacus" - ] - } - } + current_output()["energy"] = energy; } ``` -replace `"vasp"` with `"cp2k"` using either its forward index: +Coordinate, force, stress, magnetic-moment, and cell arrays are built locally and assigned as complete arrays. Repeatedly updating the same record must replace these arrays rather than append rows. -```cpp -Json::AbacusJson::set_json({"Json", "key6", "key7", 1}, "cp2k"); -``` - -or the corresponding negative index: +SCF iterations are different: they form a history and must be appended. `add_output_scf_mag()` creates a missing `scf` array, rejects an existing non-array history, and appends one iteration object. Its implementation uses: ```cpp -Json::AbacusJson::set_json({"Json", "key6", "key7", -2}, "cp2k"); +jsonValue& output = current_output(); +output["total_mag"] = total_mag; +output["absolute_mag"] = absolute_mag; +jsonValue& scf = *output.emplace("scf", jsonValue::array()).first; +if (!scf.is_array()) +{ + throw std::invalid_argument("JSON SCF history must be an array"); +} +scf.push_back({{"energy", energy}, {"ediff", ediff}, + {"drho", drho}, {"time", time}}); ``` -When the destination selected by an integer is itself an array, `append_json()` appends to that nested array; it does not replace the selected element. Out-of-range indices and mismatched object/array path components are errors. +`ordered_json` may invalidate references to child values when new members are inserted into their parent object. Acquire the `scf` reference after inserting `total_mag` and `absolute_mag`, and do not retain a record reference across appending another `output` record. The same caution applies to references to root sections when new root keys are inserted. -The workflow must call `init_output_array_obj()` before filling the corresponding calculation/ionic-step record. `set_json()` and `append_json()` do not create an implicit current output record when traversing `{"output", -1, ...}`. Record initialization remains the responsibility of the existing driver/solver entry points, not the generic path interface. +## Serialization and tests -## Migrating older JSON call sites +`document()` and `write_to_json()` do not perform MPI rank filtering. The existing `json_output()` wrapper writes `abacus.json` only on rank 0 in MPI builds; callers outside `module_json` should retain the existing integration wrappers. -The former `add_json(keys, value, is_array)` interface has been removed. Choose the new operation by intent, not just by the old boolean: +`write_to_json()` preserves the existing four-space formatting and reports file-open and write/close failures. It serializes the document before opening the destination, so a serialization error does not first truncate the file. Non-finite numbers serialize as JSON `null`; decimal versus scientific float notation is not part of the schema contract. -- Use `set_json()` for scalar assignments, whole-container replacement, and replacement of an indexed element. -- Use `append_json()` for adding one element to a named or indexed array. - -The old interface appended to an existing named array even when `is_array` was `false`, and it replaced an indexed element even when the flag was `true`. Neither implicit behavior is retained by the new operation names. +The tests reset the shared document through `document()` in their fixture; no access-control macro or friend accessor is needed. Focus coverage on ABACUS behavior: generated fields and units, repeated metadata updates, record initialization and SCF accumulation, invalid section types, insertion order, escaping and non-finite values through the real writer, and file errors. Do not replace removed path-walker tests with tests of nlohmann-json's generic container API. ## Code structure -The JSON implementation is organized as follows: - ```text source/source_io/module_json/ -├── abacusjson.cpp/.h # set/append path handling and file output -├── json_node.h # object-key / array-index path component +├── abacusjson.cpp/.h # shared document and file output ├── general_info.cpp/.h # general_info section ├── init_info.cpp/.h # comment and init sections -├── output_info.cpp/.h # output section +├── output_info.cpp/.h # output records and lifecycle checks ├── para_json.cpp/.h # integration-facing wrappers └── test/ # focused unit tests ``` -JSON support is compiled under `__JSON`, which is enabled by the CMake option `ENABLE_JSON`. +`init_section()` and `current_output()` are file-local helpers, not public interfaces for workflow callers. ## Guidelines for extending JSON output -When adding JSON output: - -1. Keep JSON construction in `source/source_io/module_json` whenever practical, rather than spreading nlohmann-json details into unrelated modules. -2. Pass the data required for output explicitly through function parameters. Do not add new `GlobalV`, `GlobalC`, or `PARAM` accesses merely to obtain a value for JSON output. -3. Prefer existing domain objects or small scalar/reference parameters over introducing new cross-module dependencies. -4. Use `Json::jsonValue` for compound JSON values, `set_json()` for assignment, and `append_json()` for sequence growth. -5. Preserve the existing JSON schema unless the change intentionally modifies the public output format. -6. Add or update focused tests under `source/source_io/module_json/test` for new fields and for array/object behavior. - -For example, `output_info` receives the required values as function arguments and adds them to the current output record: - -```cpp -void add_output_scf_mag(const double total_mag, - const double absolute_mag, - const double energy, - const double ediff, - const double drho, - const double time) -{ - AbacusJson::set_json({"output", -1, "total_mag"}, total_mag); - AbacusJson::set_json({"output", -1, "absolute_mag"}, absolute_mag); - AbacusJson::append_json({"output", -1, "scf"}, - {{"energy", energy}, - {"ediff", ediff}, - {"drho", drho}, - {"time", time}}); -} -``` +Keep construction in the existing schema generator, pass its required data explicitly, and avoid adding `GlobalV`, `GlobalC`, or `PARAM` access. Preserve field names, value types, units, and order unless a schema change is intentional. Add focused tests for new fields and lifecycle behavior, and update the [JSON output reference](json_para.md) when the public schema changes. -This keeps the JSON layer explicit and avoids introducing additional global dependencies into the output path. +Keep examples and new implementation code compatible with the C++11 baseline. Include complete domain-type definitions in the source or test file that needs them, keep public header dependencies minimal, and do not reintroduce access-control macros for testing. diff --git a/docs/advanced/json/json_para.md b/docs/advanced/json/json_para.md index 745e0a5f88..eba7f35022 100644 --- a/docs/advanced/json/json_para.md +++ b/docs/advanced/json/json_para.md @@ -5,12 +5,13 @@ - [General Information](#general-information) - [Initialization Information](#initialization-information) - [Output](#output) + - [Serialization](#serialization) ## Overview -When JSON support is enabled, ABACUS writes calculation metadata and results to `abacus.json` for post-processing. +When JSON support is enabled with the CMake option `ENABLE_JSON`, ABACUS writes calculation metadata and results to `abacus.json` for post-processing using nlohmann-json. In MPI builds, the output wrapper writes this file only on rank 0. -The current top-level JSON members are `comment`, `init`, `output`, and `general_info`. Some fields are populated only when the corresponding calculation data are available. +The current top-level JSON members are `comment`, `init`, `output`, and `general_info`. Some fields are populated only when the corresponding calculation data are available. The native-schema refactor changes the internal construction API, not these field names or their units. See the [JSON development guide](json_add.md) for implementation details. ## General Information @@ -33,7 +34,7 @@ The `general_info` object records basic build and runtime metadata: The top-level `comment` describes the default units used by the JSON output. The `init` object records the initial structure and calculation settings. Depending on the calculation path, it can contain: - `element` - [object(string:string)] Element/pseudopotential element information keyed by atom label. -- `orb` - [object(string:string/null)] Numerical orbital file for each atom type; `null` when no orbital file is used. +- `orb` - [object(string:string/null)] Numerical orbital path for each atom type, formed by concatenating the configured orbital-directory string and the per-type filename; `null` when that combined string is empty. - `pp` - [object(string:string)] Pseudopotential file for each atom type. - `coordinate` - [array(array(double))] Initial Cartesian coordinates in Angstrom. - `mag` - [array(double)] Initial magnetic moment for each atom. @@ -58,7 +59,9 @@ The top-level `comment` describes the default units used by the JSON output. The ## Output -`output` is an array. Each element represents one calculation/ionic-step output record. Fields are filled as the corresponding results become available: +`output` is an array. Each element represents one calculation/ionic-step output record, initialized before its results are written. A newly initialized record has `null` values for `e_fermi`, `energy`, `scf_converge`, `force`, and `stress`, and empty arrays for `coordinate`, `mag`, and `cell`. The `total_mag`, `absolute_mag`, and `scf` members are added by the SCF writer. + +Fields are filled as the corresponding results become available; not every workflow populates all of them: - `energy` - [double/null] Total energy in eV. - `e_fermi` - [double/null] Fermi energy in eV. @@ -76,4 +79,10 @@ The top-level `comment` describes the default units used by the JSON output. The - `drho` - [double] Charge-density difference. - `time` - [double] Time used by the SCF step in seconds. +Updating geometry data for an existing record replaces its coordinate, magnetic-moment, and cell arrays, together with force and stress arrays when requested; it does not append duplicate rows. SCF iterations are appended to that record's `scf` history, while starting a new calculation/ionic step appends a new `output` record. + +## Serialization + +The writer uses four-space indentation and retains object-key insertion order. Non-finite floating-point values (NaN and positive or negative infinity) are serialized as `null`, not as nonstandard JSON numeric tokens. A `null` numeric field can therefore mean either that no value has been written or that the stored value was non-finite; it should not be interpreted as zero. + JSON numbers are intended to be consumed as numeric values. Their textual representation (for example, decimal versus scientific notation) is not part of the output schema. diff --git a/source/source_io/module_json/abacusjson.cpp b/source/source_io/module_json/abacusjson.cpp index a37ba19a94..e00049097d 100644 --- a/source/source_io/module_json/abacusjson.cpp +++ b/source/source_io/module_json/abacusjson.cpp @@ -2,89 +2,17 @@ #ifdef __JSON #include -#include #include #include -#include namespace Json { -namespace -{ -// Only missing named nodes are created. Indexed access never grows an array. -jsonValue* resolve_path(jsonValue& root, - const std::vector& keys, - jsonValue initial_value) -{ - if (keys.empty()) - { - return nullptr; - } - - jsonValue* parent = &root; - for (std::size_t i = 0; i < keys.size(); ++i) - { - const jsonKeyNode& key = keys[i]; - if (key.is_index) - { - if (!parent->is_array()) - { - throw std::invalid_argument("JSON output: an integer path component requires an array"); - } - const std::ptrdiff_t size = static_cast(parent->size()); - std::ptrdiff_t index = static_cast(key.i); - if (index < 0) - { - index += size; - } - if (index < 0 || index >= size) - { - throw std::out_of_range("JSON output: array index out of range"); - } - parent = &parent->at(static_cast(index)); - } - else - { - if (!parent->is_object()) - { - throw std::invalid_argument("JSON output: a named path component requires an object"); - } - jsonValue::iterator child = parent->find(key.key); - if (child == parent->end()) - { - jsonValue initial = i + 1 == keys.size() ? std::move(initial_value) : jsonValue::object(); - child = parent->emplace(key.key, std::move(initial)).first; - } - parent = &child.value(); - } - } - return parent; -} -} // namespace jsonValue AbacusJson::doc = jsonValue::object(); -void AbacusJson::set_json(const std::vector& keys, jsonValue value) +jsonValue& AbacusJson::document() { - jsonValue* target = resolve_path(doc, keys, nullptr); - if (target != nullptr) - { - *target = std::move(value); - } -} - -void AbacusJson::append_json(const std::vector& keys, jsonValue value) -{ - jsonValue* target = resolve_path(doc, keys, jsonValue::array()); - if (target == nullptr) - { - return; - } - if (!target->is_array()) - { - throw std::invalid_argument("JSON output: append requires an array"); - } - target->push_back(std::move(value)); + return doc; } void AbacusJson::write_to_json(const std::string& filename) diff --git a/source/source_io/module_json/abacusjson.h b/source/source_io/module_json/abacusjson.h index 382cc460ce..66c5e54720 100644 --- a/source/source_io/module_json/abacusjson.h +++ b/source/source_io/module_json/abacusjson.h @@ -2,8 +2,6 @@ #define ABACUS_JSON_H #include -#include -#include "json_node.h" #ifdef __JSON // Keep the implementation-heavy json.hpp out of this header. @@ -14,31 +12,14 @@ namespace Json using jsonValue = nlohmann::ordered_json; -class AbacusJsonTestAccess; - class AbacusJson { public: + // Shared document for the schema generators in module_json; keep its root an object. + static jsonValue& document(); static void write_to_json(const std::string& filename); - /** - * Replace a value at a named or indexed path, including whole containers. - * Missing named parents are created as objects. Integer indices must refer - * to existing array elements; negative indices count from the end. - * An empty path leaves the document unchanged. - */ - static void set_json(const std::vector& keys, jsonValue value); - - /** - * Append one value to an array at the path, without flattening that value. - * A missing named destination is created as an array. An existing - * destination must be an array, including when selected by an integer - * index; nulls, objects and scalars are rejected. Path rules match set_json. - */ - static void append_json(const std::vector& keys, jsonValue value); - private: - friend class AbacusJsonTestAccess; static jsonValue doc; }; diff --git a/source/source_io/module_json/general_info.cpp b/source/source_io/module_json/general_info.cpp index 0e3b7c01c1..60c0fcbe5c 100644 --- a/source/source_io/module_json/general_info.cpp +++ b/source/source_io/module_json/general_info.cpp @@ -47,18 +47,18 @@ void gen_general_info(const Parameter& param) int omp_num = 1; #endif - AbacusJson::set_json({"general_info"}, - {{"version", version}, - {"commit", commit}, - {"device", param.inp.device}, - {"mpi_num", mpi_num}, - {"omp_num", omp_num}, - {"pseudo_dir", param.inp.pseudo_dir}, - {"orbital_dir", param.inp.orbital_dir}, - {"stru_file", param.globalv.global_in_stru}, - {"kpt_file", param.inp.kpoint_file}, - {"start_time", start_time_str}, - {"end_time", end_time_str}}); + AbacusJson::document()["general_info"] = { + {"version", version}, + {"commit", commit}, + {"device", param.inp.device}, + {"mpi_num", mpi_num}, + {"omp_num", omp_num}, + {"pseudo_dir", param.inp.pseudo_dir}, + {"orbital_dir", param.inp.orbital_dir}, + {"stru_file", param.globalv.global_in_stru}, + {"kpt_file", param.inp.kpoint_file}, + {"start_time", start_time_str}, + {"end_time", end_time_str}}; } #endif } // namespace Json diff --git a/source/source_io/module_json/init_info.cpp b/source/source_io/module_json/init_info.cpp index 1d750a2092..0dc9dc580f 100644 --- a/source/source_io/module_json/init_info.cpp +++ b/source/source_io/module_json/init_info.cpp @@ -7,20 +7,20 @@ #ifdef __JSON #include -#include +#include namespace Json { namespace { -// Structure, k-point metadata and calculation metadata share the init section. -// Replace only the fields built by this generator, not the entire section. -void set_init_fields(jsonValue fields) +jsonValue& init_section() { - for (jsonValue::iterator field = fields.begin(); field != fields.end(); ++field) + jsonValue& init = *AbacusJson::document().emplace("init", jsonValue::object()).first; + if (!init.is_object()) { - AbacusJson::set_json({"init", field.key()}, std::move(field.value())); + throw std::invalid_argument("JSON init section must be an object"); } + return init; } } // namespace @@ -49,18 +49,19 @@ void gen_init(UnitCell* ucell, const Input_para& inp) info["kmesh_type"] = inp.kmesh_type; info["kspacing"] = jsonValue::array({inp.kspacing[0], inp.kspacing[1], inp.kspacing[2]}); info["koffset"] = jsonValue::array({inp.koffset[0], inp.koffset[1], inp.koffset[2]}); - set_init_fields(std::move(info)); + // Shallow update: preserve other generators' fields, replace this generator's containers. + init_section().update(info); } void add_nkstot(int nkstot) { - AbacusJson::set_json({"init", "nkstot"}, nkstot); + init_section()["nkstot"] = nkstot; } void gen_stru(UnitCell* ucell, const Input_para& inp) { - AbacusJson::set_json({"comment"}, - "Unless otherwise specified, the unit of energy is eV and the unit of length is Angstrom"); + AbacusJson::document()["comment"] = + "Unless otherwise specified, the unit of energy is eV and the unit of length is Angstrom"; jsonValue info = jsonValue::object(); for (int it = 0; it < ucell->ntype; ++it) @@ -95,7 +96,7 @@ void gen_stru(UnitCell* ucell, const Input_para& inp) {ucell->latvec.e31 * lat0_angstrom, ucell->latvec.e32 * lat0_angstrom, ucell->latvec.e33 * lat0_angstrom}}; - set_init_fields(std::move(info)); + init_section().update(info); } } // namespace Json diff --git a/source/source_io/module_json/json_node.h b/source/source_io/module_json/json_node.h deleted file mode 100644 index 1b91d75444..0000000000 --- a/source/source_io/module_json/json_node.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef JSON_NODE_H -#define JSON_NODE_H - -#include - -namespace Json -{ - -class jsonKeyNode -{ - public: - jsonKeyNode(int index) : i(index), is_index(true) {} - jsonKeyNode(const std::string& name) : key(name) {} - jsonKeyNode(const char* name) : key(name) {} - - int i = 0; - std::string key; - bool is_index = false; -}; - -} // namespace Json - -#endif diff --git a/source/source_io/module_json/output_info.cpp b/source/source_io/module_json/output_info.cpp index b893ef9c45..463adcee31 100644 --- a/source/source_io/module_json/output_info.cpp +++ b/source/source_io/module_json/output_info.cpp @@ -6,6 +6,7 @@ #ifdef __JSON #include +#include #endif #include @@ -16,17 +17,44 @@ namespace Json #ifdef __JSON +namespace +{ +jsonValue& current_output() +{ + jsonValue& root = AbacusJson::document(); + const jsonValue::iterator output = root.find("output"); + if (output == root.end() || !output->is_array()) + { + throw std::invalid_argument("JSON output records must be initialized as an array"); + } + if (output->empty()) + { + throw std::out_of_range("JSON output record is not initialized"); + } + jsonValue& record = output->back(); + if (!record.is_object()) + { + throw std::invalid_argument("JSON output record must be an object"); + } + return record; +} +} // namespace + void init_output_array_obj() { - AbacusJson::append_json({"output"}, - {{"e_fermi", nullptr}, - {"energy", nullptr}, - {"scf_converge", nullptr}, - {"force", nullptr}, - {"stress", nullptr}, - {"coordinate", jsonValue::array()}, - {"mag", jsonValue::array()}, - {"cell", jsonValue::array()}}); + jsonValue& output = *AbacusJson::document().emplace("output", jsonValue::array()).first; + if (!output.is_array()) + { + throw std::invalid_argument("JSON output must be an array"); + } + output.push_back({{"e_fermi", nullptr}, + {"energy", nullptr}, + {"scf_converge", nullptr}, + {"force", nullptr}, + {"stress", nullptr}, + {"coordinate", jsonValue::array()}, + {"mag", jsonValue::array()}, + {"cell", jsonValue::array()}}); } void add_output_cell_coo_stress_force(const UnitCell& ucell, @@ -37,6 +65,7 @@ void add_output_cell_coo_stress_force(const UnitCell& ucell, const bool cal_force, const bool cal_stress) { + jsonValue& output = current_output(); const double output_acc = 1.0e-8; if (cal_force) { @@ -53,7 +82,7 @@ void add_output_cell_coo_stress_force(const UnitCell& ucell, ++iat; } } - AbacusJson::set_json({"output", -1, "force"}, std::move(force_array)); + output["force"] = std::move(force_array); } if (cal_stress) @@ -65,7 +94,7 @@ void add_output_cell_coo_stress_force(const UnitCell& ucell, stress(i, 1) * unit_transform, stress(i, 2) * unit_transform})); } - AbacusJson::set_json({"output", -1, "stress"}, std::move(stress_array)); + output["stress"] = std::move(stress_array); } const double lat0_angstrom = ucell.lat0_angstrom; @@ -82,29 +111,29 @@ void add_output_cell_coo_stress_force(const UnitCell& ucell, mag.push_back(ucell.atoms[it].mag[ia]); } } - AbacusJson::set_json({"output", -1, "coordinate"}, std::move(coordinates)); - AbacusJson::set_json({"output", -1, "mag"}, std::move(mag)); - AbacusJson::set_json({"output", -1, "cell"}, - {{ucell.latvec.e11 * lat0_angstrom, - ucell.latvec.e12 * lat0_angstrom, - ucell.latvec.e13 * lat0_angstrom}, - {ucell.latvec.e21 * lat0_angstrom, - ucell.latvec.e22 * lat0_angstrom, - ucell.latvec.e23 * lat0_angstrom}, - {ucell.latvec.e31 * lat0_angstrom, - ucell.latvec.e32 * lat0_angstrom, - ucell.latvec.e33 * lat0_angstrom}}); + output["coordinate"] = std::move(coordinates); + output["mag"] = std::move(mag); + output["cell"] = {{ucell.latvec.e11 * lat0_angstrom, + ucell.latvec.e12 * lat0_angstrom, + ucell.latvec.e13 * lat0_angstrom}, + {ucell.latvec.e21 * lat0_angstrom, + ucell.latvec.e22 * lat0_angstrom, + ucell.latvec.e23 * lat0_angstrom}, + {ucell.latvec.e31 * lat0_angstrom, + ucell.latvec.e32 * lat0_angstrom, + ucell.latvec.e33 * lat0_angstrom}}; } void add_output_efermi_converge(const double efermi, const bool scf_converge) { - AbacusJson::set_json({"output", -1, "e_fermi"}, efermi); - AbacusJson::set_json({"output", -1, "scf_converge"}, scf_converge); + jsonValue& output = current_output(); + output["e_fermi"] = efermi; + output["scf_converge"] = scf_converge; } void add_output_energy(const double energy) { - AbacusJson::set_json({"output", -1, "energy"}, energy); + current_output()["energy"] = energy; } void add_output_scf_mag(const double total_mag, @@ -114,10 +143,16 @@ void add_output_scf_mag(const double total_mag, const double drho, const double time) { - AbacusJson::set_json({"output", -1, "total_mag"}, total_mag); - AbacusJson::set_json({"output", -1, "absolute_mag"}, absolute_mag); - AbacusJson::append_json({"output", -1, "scf"}, - {{"energy", energy}, {"ediff", ediff}, {"drho", drho}, {"time", time}}); + jsonValue& output = current_output(); + output["total_mag"] = total_mag; + output["absolute_mag"] = absolute_mag; + // Acquire the history only after inserting other fields: ordered_json may reallocate them. + jsonValue& scf = *output.emplace("scf", jsonValue::array()).first; + if (!scf.is_array()) + { + throw std::invalid_argument("JSON SCF history must be an array"); + } + scf.push_back({{"energy", energy}, {"ediff", ediff}, {"drho", drho}, {"time", time}}); } #endif // __JSON diff --git a/source/source_io/module_json/test/para_json_test.cpp b/source/source_io/module_json/test/para_json_test.cpp index a26ba69d1f..3ddee9e4d1 100644 --- a/source/source_io/module_json/test/para_json_test.cpp +++ b/source/source_io/module_json/test/para_json_test.cpp @@ -24,70 +24,40 @@ #include "source_io/module_parameter/parameter.h" #include "source_main/version.h" -namespace Json -{ -class AbacusJsonTestAccess -{ - public: - static void reset() - { - AbacusJson::doc = jsonValue::object(); - } - - static const jsonValue& document() - { - return AbacusJson::doc; - } -}; -} // namespace Json - class AbacusJsonTest : public testing::Test { protected: void SetUp() override { - Json::AbacusJsonTestAccess::reset(); + Json::AbacusJson::document() = Json::jsonValue::object(); + } + + void TearDown() override + { + std::remove("test.json"); + std::remove("json-output-not-a-directory"); } const Json::jsonValue& document() const { - return Json::AbacusJsonTestAccess::document(); + return Json::AbacusJson::document(); } }; -TEST_F(AbacusJsonTest, SetAndAppendJson) -{ - Json::AbacusJson::set_json({"key"}, "value"); - Json::AbacusJson::set_json({"nested", "value"}, 1); - Json::AbacusJson::set_json({"nested", "value"}, 2); - Json::AbacusJson::append_json({"array"}, Json::jsonValue{{"index", 0}}); - Json::AbacusJson::append_json({"array"}, Json::jsonValue{{"index", 1}}); - Json::AbacusJson::set_json({"array", -1, "label"}, "last"); - - const Json::jsonValue& root = document(); - EXPECT_EQ(root["key"], "value"); - EXPECT_EQ(root["nested"]["value"], 2); - ASSERT_EQ(root["array"].size(), 2u); - EXPECT_EQ(root["array"][0]["index"], 0); - EXPECT_EQ(root["array"][1]["index"], 1); - EXPECT_EQ(root["array"][1]["label"], "last"); -} - TEST_F(AbacusJsonTest, OutputJson) { - Json::AbacusJson::set_json({"key"}, "value"); - Json::AbacusJson::set_json( - {"nested"}, Json::jsonValue{{"value", 1}, {"array", Json::jsonValue::array({1, 2, 3})}}); - - const std::string filename = "test.json"; - Json::AbacusJson::write_to_json(filename); - - std::ifstream file(filename); + // Exercise our writer, including escaping, number types and insertion order. + Json::AbacusJson::document() = { + {"z", "quote: \"; slash: \\; newline: \n; UTF-8: \xCE\xB1"}, + {"a", std::string("a\0b", 3)}, + {"nested", {{"int", 1}, {"float", 0.1}, {"bool", true}, {"null", nullptr}, + {"array", Json::jsonValue::array({1, 2, 3})}}}}; + Json::AbacusJson::write_to_json("test.json"); + std::ifstream file("test.json"); ASSERT_TRUE(file.is_open()); const Json::jsonValue result = Json::jsonValue::parse(file); EXPECT_EQ(result, document()); - file.close(); - EXPECT_EQ(std::remove(filename.c_str()), 0); + EXPECT_EQ(result.dump(), document().dump()); } TEST_F(AbacusJsonTest, GeneralInfo) @@ -119,7 +89,7 @@ TEST_F(AbacusJsonTest, GeneralInfo) EXPECT_EQ(keys, (std::vector{"version", "commit", "device", "mpi_num", "omp_num", "pseudo_dir", "orbital_dir", "stru_file", "kpt_file", "start_time", "end_time"})); - Json::AbacusJson::set_json({"init", "nkstot"}, 2); + Json::add_nkstot(2); Json::gen_general_info(param); EXPECT_EQ(document()["init"]["nkstot"], 2); EXPECT_EQ(document()["general_info"].size(), keys.size()); @@ -188,6 +158,14 @@ TEST_F(AbacusJsonTest, InitInfo) EXPECT_EQ(init["kmesh_type"], "gamma"); EXPECT_EQ(init["kspacing"], Json::jsonValue::array({0.04, 0.04, 0.04})); EXPECT_EQ(init["koffset"], Json::jsonValue::array({0.0, 0.0, 0.0})); + + // Rebuild the per-species maps rather than retaining entries from a previous call. + ucell.ntype = 2; + ucell.nat = 3; + Json::gen_init(&ucell, inp); + EXPECT_EQ(init.at("natom_each_type"), (Json::jsonValue{{"Si", 1}, {"C", 2}})); + EXPECT_EQ(init.at("nelectron_each_type"), (Json::jsonValue{{"Si", 3.0}, {"C", 4.0}})); + EXPECT_EQ(init.at("nkstot"), 1); } TEST_F(AbacusJsonTest, InitStructure) @@ -250,134 +228,11 @@ TEST_F(AbacusJsonTest, InitStructure) EXPECT_EQ(document().dump(), first.dump()); // Preserve key order, too. } -TEST_F(AbacusJsonTest, NullAndEmptyContainers) -{ - Json::AbacusJson::set_json({"null"}, nullptr); - Json::AbacusJson::set_json({"object"}, Json::jsonValue::object()); - Json::AbacusJson::set_json({"array"}, Json::jsonValue::array()); - Json::AbacusJson::append_json({"wrapped"}, Json::jsonValue::array()); - - const Json::jsonValue& root = document(); - EXPECT_TRUE(root.at("null").is_null()); - EXPECT_EQ(root.at("object"), Json::jsonValue::object()); - EXPECT_EQ(root.at("array"), Json::jsonValue::array()); - EXPECT_EQ(root.at("wrapped"), Json::jsonValue::array({Json::jsonValue::array()})); -} - -TEST_F(AbacusJsonTest, SetReplacesContainers) -{ - Json::AbacusJson::set_json({"value"}, Json::jsonValue::array({1, 2})); - Json::AbacusJson::set_json({"value"}, Json::jsonValue::array({3})); - EXPECT_EQ(document()["value"], Json::jsonValue::array({3})); - - Json::AbacusJson::set_json({"value"}, Json::jsonValue{{"old", 1}}); - Json::AbacusJson::set_json({"value"}, Json::jsonValue{{"new", 2}}); - EXPECT_EQ(document()["value"], (Json::jsonValue{{"new", 2}})); - Json::AbacusJson::set_json({"value"}, true); - EXPECT_TRUE(document()["value"].is_boolean()); - EXPECT_EQ(document()["value"], true); - Json::AbacusJson::set_json({"value"}, 1.25); - EXPECT_TRUE(document()["value"].is_number_float()); - EXPECT_DOUBLE_EQ(document()["value"].get(), 1.25); -} - -TEST_F(AbacusJsonTest, ArrayAppendAndIndexedReplacement) -{ - Json::AbacusJson::append_json({"array"}, 1); - Json::AbacusJson::append_json({"array"}, 2); - Json::AbacusJson::set_json({"array", -1}, 3); - Json::AbacusJson::set_json({"array", -2}, Json::jsonValue::array({4, 5})); - Json::AbacusJson::append_json({"array", 0}, 6); - EXPECT_EQ(document()["array"][0], Json::jsonValue::array({4, 5, 6})); - Json::AbacusJson::set_json({"array", 0}, 6); - EXPECT_EQ(document()["array"], Json::jsonValue::array({6, 3})); - - // Numeric strings and empty strings are object keys, not array indices. - Json::AbacusJson::set_json({"object", "0"}, 7); - Json::AbacusJson::set_json({"object", ""}, 8); - EXPECT_EQ(document()["object"]["0"], 7); - EXPECT_EQ(document()["object"][""], 8); -} - -TEST_F(AbacusJsonTest, AppendRejectsNonArrays) -{ - Json::AbacusJson::set_json({"null"}, nullptr); - Json::AbacusJson::set_json({"object"}, Json::jsonValue::object()); - Json::AbacusJson::set_json({"scalar"}, 1); - Json::AbacusJson::set_json({"array"}, Json::jsonValue::array({2})); - const Json::jsonValue before = document(); - - for (const char* key : {"null", "object", "scalar"}) - { - EXPECT_THROW(Json::AbacusJson::append_json({key}, 3), std::invalid_argument); - } - EXPECT_THROW(Json::AbacusJson::append_json({"array", 0}, 3), std::invalid_argument); - EXPECT_EQ(document(), before); -} - -TEST_F(AbacusJsonTest, InvalidPathsDoNotGrowArrays) -{ - Json::AbacusJson::append_json({"array"}, 1); - Json::AbacusJson::set_json({"empty"}, Json::jsonValue::array()); - Json::AbacusJson::set_json({"scalar"}, 2); - - for (const int index : {1, -2, std::numeric_limits::min()}) - { - EXPECT_THROW(Json::AbacusJson::set_json({"array", index}, 3), std::out_of_range); - EXPECT_THROW(Json::AbacusJson::append_json({"array", index}, 3), std::out_of_range); - } - EXPECT_THROW(Json::AbacusJson::set_json({"empty", -1}, 3), std::out_of_range); - EXPECT_THROW(Json::AbacusJson::append_json({"empty", -1}, 3), std::out_of_range); - EXPECT_THROW(Json::AbacusJson::set_json({"array", "key"}, 3), std::invalid_argument); - EXPECT_THROW(Json::AbacusJson::set_json({"scalar", "key"}, 3), std::invalid_argument); - EXPECT_THROW(Json::AbacusJson::set_json({0}, 3), std::invalid_argument); - EXPECT_THROW(Json::AbacusJson::append_json({0}, 3), std::invalid_argument); - EXPECT_EQ(document()["array"], Json::jsonValue::array({1})); - EXPECT_TRUE(document()["empty"].empty()); - - const Json::jsonValue before = document(); - Json::AbacusJson::set_json({}, 9); - Json::AbacusJson::append_json({}, 9); - EXPECT_EQ(document(), before); -} - -TEST_F(AbacusJsonTest, OwnedValuesAndStringEscaping) -{ - Json::jsonValue original = {{"value", "original"}}; - Json::AbacusJson::set_json({"copy"}, original); - original["value"] = "changed"; - EXPECT_EQ(document()["copy"]["value"], "original"); - - const std::string text = "quote: \"; slash: \\; newline: \n; UTF-8: \xCE\xB1"; - const std::string embedded_nul("a\0b", 3); - Json::AbacusJson::set_json({"text"}, text); - Json::AbacusJson::set_json({"embedded_nul"}, embedded_nul); - const Json::jsonValue result = Json::jsonValue::parse(document().dump(4)); - EXPECT_EQ(result["text"], text); - EXPECT_EQ(result["embedded_nul"].get(), embedded_nul); -} - -TEST_F(AbacusJsonTest, PreservesInsertionOrder) -{ - Json::AbacusJson::set_json({"z"}, 1); - Json::AbacusJson::set_json({"a"}, 2); - Json::AbacusJson::set_json({"m"}, 3); - Json::AbacusJson::set_json({"a"}, 4); - - const Json::jsonValue result = Json::jsonValue::parse(document().dump()); - std::vector keys; - for (Json::jsonValue::const_iterator it = result.begin(); it != result.end(); ++it) - { - keys.push_back(it.key()); - } - EXPECT_EQ(keys, (std::vector{"z", "a", "m"})); - EXPECT_EQ(result["a"], 4); -} - TEST_F(AbacusJsonTest, OutputRecords) { EXPECT_THROW(Json::add_output_energy(-1.0), std::invalid_argument); - Json::AbacusJson::set_json({"output"}, Json::jsonValue::array()); + EXPECT_FALSE(document().contains("output")); + Json::AbacusJson::document()["output"] = Json::jsonValue::array(); EXPECT_THROW(Json::add_output_energy(-1.0), std::out_of_range); Json::init_output_array_obj(); ASSERT_EQ(document().at("output").size(), 1u); @@ -416,6 +271,44 @@ TEST_F(AbacusJsonTest, OutputRecords) EXPECT_EQ(document()["output"][1]["energy"], -11.0); } +TEST_F(AbacusJsonTest, RejectsInvalidSections) +{ + for (const Json::jsonValue& invalid : {Json::jsonValue(nullptr), Json::jsonValue(1), + Json::jsonValue("invalid"), Json::jsonValue::array()}) + { + Json::AbacusJson::document()["init"] = invalid; + EXPECT_THROW(Json::add_nkstot(1), std::invalid_argument); + EXPECT_EQ(document().at("init"), invalid); + } + for (const Json::jsonValue& invalid : {Json::jsonValue(nullptr), Json::jsonValue(1), + Json::jsonValue("invalid"), Json::jsonValue::object()}) + { + Json::AbacusJson::document()["output"] = invalid; + EXPECT_THROW(Json::init_output_array_obj(), std::invalid_argument); + EXPECT_THROW(Json::add_output_energy(-1.0), std::invalid_argument); + EXPECT_EQ(document().at("output"), invalid); + } +} + +TEST_F(AbacusJsonTest, RejectsInvalidRecordsAndScfHistory) +{ + for (const Json::jsonValue& invalid : {Json::jsonValue(nullptr), Json::jsonValue(1), + Json::jsonValue::array()}) + { + Json::AbacusJson::document()["output"] = Json::jsonValue::array({invalid}); + EXPECT_THROW(Json::add_output_energy(-1.0), std::invalid_argument); + EXPECT_EQ(document().at("output").at(0), invalid); + } + Json::init_output_array_obj(); + for (const Json::jsonValue& invalid : {Json::jsonValue(nullptr), Json::jsonValue(1), + Json::jsonValue::object()}) + { + Json::AbacusJson::document()["output"].back()["scf"] = invalid; + EXPECT_THROW(Json::add_output_scf_mag(0.0, 0.0, -1.0, 0.0, 0.1, 0.1), std::invalid_argument); + EXPECT_EQ(document().at("output").back().at("scf"), invalid); + } +} + TEST_F(AbacusJsonTest, OutputStructureForceAndStress) { UnitCell ucell; @@ -479,11 +372,15 @@ TEST_F(AbacusJsonTest, OutputStructureForceAndStress) TEST_F(AbacusJsonTest, NonFiniteNumbersSerializeAsNull) { - Json::AbacusJson::set_json({"nan"}, std::numeric_limits::quiet_NaN()); - Json::AbacusJson::set_json({"inf"}, std::numeric_limits::infinity()); - const Json::jsonValue result = Json::jsonValue::parse(document().dump()); - EXPECT_TRUE(result["nan"].is_null()); - EXPECT_TRUE(result["inf"].is_null()); + Json::init_output_array_obj(); + Json::add_output_energy(std::numeric_limits::quiet_NaN()); + Json::add_output_efermi_converge(std::numeric_limits::infinity(), false); + Json::AbacusJson::write_to_json("test.json"); + std::ifstream file("test.json"); + ASSERT_TRUE(file.is_open()); + const Json::jsonValue result = Json::jsonValue::parse(file); + EXPECT_TRUE(result.at("output").at(0).at("energy").is_null()); + EXPECT_TRUE(result.at("output").at(0).at("e_fermi").is_null()); } TEST_F(AbacusJsonTest, FileOpenFailureIsReported)