diff --git a/AGENTS.md b/AGENTS.md index 56606c2ff40..648ae386896 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,9 @@ rules. Read the complete governance document before making or reviewing changes: guards and `#ifdef`/`#ifndef` usages of existing macros are exempt. 21. Do not initialize member variables from `PARAM` in `.h`/`.hpp` files; pass the value through the constructor or a setter instead. + 22. Print ionic-step indices one-based (starting from 1) in user-facing + output files, matching the `g{N}` filename suffix convention; do not + write zero-based step counters such as `STEP: 0` into output files. - Use LF line endings for text files. Only `.bat` and `.cmd` files may use CRLF. - Keep source file additions deterministic: update the relevant `CMakeLists.txt` or explain why the file is generated or included indirectly. diff --git a/docs/advanced/elec_properties/Mulliken.md b/docs/advanced/elec_properties/Mulliken.md index c768e83c7ff..afb585d0e29 100644 --- a/docs/advanced/elec_properties/Mulliken.md +++ b/docs/advanced/elec_properties/Mulliken.md @@ -1,62 +1,67 @@ # Mulliken Charge Analysis From version 2.1.0, ABACUS has the function of Mulliken population analysis. The example can be found in [examples/mulliken](https://github.com/deepmodeling/abacus-develop/tree/develop/examples/14_mulliken). \ -To use this function, set [out_mul](../input_files/input-main.md#out_mul) to `1` in the INPUT file. After calculation, there will be an output file named `mulliken.txt` in the output directory. In MD calculations, the output interval is controlled by the keyword [out_freq_ion](../input_files/input-main.md#out_freq_ion). In the file, there are contents like (`nspin 1`): +To use this function, set [out_mul](../input_files/input-main.md#out_mul) to `1` in the INPUT file. After calculation, there will be an output file named `mulliken.txt` in the output directory. In MD calculations, the output interval is controlled by the keyword [out_freq_ion](../input_files/input-main.md#out_freq_ion). In the file, there are contents like (`nspin 1`, H2): ``` -STEP: 0 -CALCULATE THE MULLIkEN ANALYSIS FOR EACH ATOM - Total charge of spin 1: 8 - Total charge: 8 -Decomposed Mulliken populations -0 Zeta of Si Spin 1 -s 0 1.2553358 - sum over m 1.2553358 -s 1 -0.030782972 - sum over m -0.030782972 - sum over m+zeta 1.2245529 -pz 0 0.85945806 -px 0 0.85945806 -py 0 0.85945806 - sum over m 2.5783742 -pz 1 0.0065801228 -px 1 0.0065801228 -py 1 0.0065801228 - sum over m 0.019740368 - sum over m+zeta 2.5981145 -dz^2 0 0.0189287 -dxz 0 0.046491729 -dyz 0 0.046491729 -dx^2-y^2 0 0.0189287 -dxy 0 0.046491729 - sum over m 0.17733259 - sum over m+zeta 0.17733259 -Total Charge on atom: Si 4 + --- Ionic Step 1 --- + Total charge 2 + Decomposed Mulliken population analysis for each atom + l and m from Ylm, z stands for zeta orbital + + ------------------ + Atom 1 is H + ------------------ + zeta spin1 + s 1 0.9980 + s 2 -0.0052 + sum mz 0.9928 + + pz 1 0.0072 + px 1 0.0000 + py 1 0.0000 + sum m 0.0072 + sum mz 0.0072 + + sum lmz 1.0000 + + total charge on atom 1 1.0000 ... ``` -The file gives Mulliken charge in turn according to the order of atoms in the system. For example, the following block is for the first atom in system (`nspin 2`), +The ionic step index starts from 1. The file gives Mulliken charge in turn according to the order of atoms in the system. For example, the following block is for the first atom in system (`nspin 2`), ``` -0 Zeta of Si Spin 1 Spin 2 Sum Diff + ------------------ + Atom 1 is Fe1 + ------------------ + zeta spin1 spin2 sum diff + s 1 0.9437 0.8381 1.7818 0.1056 ... -Total Charge on atom: Si 4 -Total Magnetism on atom: Si -1.2739809e-14 + sum lmz ... ``` And the next block is for the second atom in system, and so on. ``` -1 Zeta of Si Spin 1 Spin 2 Sum Diff + ------------------ + Atom 2 is Fe2 + ------------------ + zeta spin1 spin2 sum diff ... ``` For each atom, the file gives detailed Mulliken population analysis at different levels, -- magnetic quantum number level: such as lines beigin with ‘s,px,py,pz,...’ -- azimuthal quantum number level: such as lines begin with ‘sum over m’. -- principal quantum number level: such as lines begin with ‘sum over m+zeta’. Here ‘zeta’ - equals ‘zeta’ in the file, which means how many radial atomic orbitals there are for a given orbital angular momentum. -- atomic level: such as lines begin with ‘Total Charge on atom’. +- magnetic quantum number level: such as lines begin with 's, px, py, pz, ...'. The columns + are the radial index (`zeta`, one-based) followed by the spin channels. +- azimuthal quantum number level: such as lines begin with 'sum m', the sum over the + magnetic quantum number m for one radial orbital. +- principal quantum number level: such as lines begin with 'sum mz', the sum over both the + magnetic quantum number m and the radial index zeta for a given orbital angular momentum. + Here 'z' equals 'zeta' in the file, which means how many radial atomic orbitals there are + for a given orbital angular momentum. +- atomic level: such as lines begin with 'sum lmz' for the atom total and + 'total charge on atom' for the charge assigned to the atom. -More orbital information can be found in 'Orbital' file output with 'mulliken.txt' when `out_mul 1` +More orbital information can be found in 'nao.txt' file output with 'mulliken.txt' when `out_mul 1` diff --git a/docs/advanced/elec_properties/density_matrix.md b/docs/advanced/elec_properties/density_matrix.md index 2155af8a239..df755c3e723 100644 --- a/docs/advanced/elec_properties/density_matrix.md +++ b/docs/advanced/elec_properties/density_matrix.md @@ -6,11 +6,11 @@ out_dmk 1 ``` After finishing the calculation, the density matrix is written into `OUT.${suffix}/`. -For current develop versions: -- gamma-only (`gamma_only = 1`): `dmg1_nao.txt` (`nspin=1/4`) or `dms1g1_nao.txt` and `dms2g1_nao.txt` (`nspin=2`) -- multi-k (`gamma_only = 0`): `dmk1g1_nao.txt`, `dmk2g1_nao.txt`, ... (`nspin=1/4`) or `dmk1s1g1_nao.txt`, `dmk1s2g1_nao.txt`, ... (`nspin=2`) +For current develop versions, the default filenames (with `out_app_flag = 0` or `1`) are: +- gamma-only (`gamma_only = 1`): `dm_nao.txt` (`nspin=1/4`) or `dms1_nao.txt` and `dms2_nao.txt` (`nspin=2`) +- multi-k (`gamma_only = 0`): `dmk1_nao.txt`, `dmk2_nao.txt`, ... (`nspin=1/4`) or `dmk1s1_nao.txt`, `dmk1s2_nao.txt`, ... (`nspin=2`) -Here `g{istep}` denotes the geometry/step index in the output filename. +With `out_app_flag = 2`, a one-based geometry-step suffix `g{istep}` is inserted before `_nao` (for example `dmk1g1_nao.txt`). With `out_app_flag = 0` the fixed file is truncated and overwritten at every write, and with `out_app_flag = 1` records are appended to the fixed file. For 3.10-LTS, the corresponding keyword is `out_dm`, and file names follow the legacy style such as `SPIN1_DM` and `SPIN2_DM`. diff --git a/docs/advanced/elec_properties/dos.md b/docs/advanced/elec_properties/dos.md index 47076b54cd5..969998a46ff 100644 --- a/docs/advanced/elec_properties/dos.md +++ b/docs/advanced/elec_properties/dos.md @@ -68,7 +68,7 @@ Gamma 8 8 8 0 0 0 ``` -Run the program, and you will see a file named doss1g1.txt in the output directory. The columns are: energy(eV), dos (number of states in each energy bin), dos_int (integrated DOS), dos_smear(1/eV) (Gaussian-smeared DOS), dos_smear_int (integrated smeared DOS). Plot the file with graphing software, and you'll get the DOS. +Run the program, and you will see a file named doss1.txt in the output directory (with `out_app_flag = 2` the name carries a one-based `g{step}` index, e.g., `doss1g1.txt`). The columns are: energy(eV), dos (number of states in each energy bin), dos_int (integrated DOS), dos_smear(1/eV) (Gaussian-smeared DOS), dos_smear_int (integrated smeared DOS). Plot the file with graphing software, and you'll get the DOS. The `dos` column is a raw per-bin histogram, while `dos_smear` is the physically meaningful density of states in 1/eV. The `abacus-plot` tool plots `dos_smear` by default; set `"smearing": false` in its config.json to plot the raw histogram instead, or apply your own smearing to the raw column with `abacus_plot.utils.gaussian_smearing`. For nspin=2, two files (doss1*/doss2*) are written, one per spin channel; the tool pairs them automatically whether you pass either file, a directory, or a list of both files. For nspin=4, a single spinor-summed file is written. @@ -82,7 +82,7 @@ The `dos` column is a raw per-bin histogram, while `dos_smear` is the physically ## PDOS -Along with the DOS files, we also produce the projected density of states (PDOS) in files named pdoss{spin}g{geom}.txt (e.g., pdoss1g1.txt). +Along with the DOS files, we also produce the projected density of states (PDOS) in files named pdoss{spin}.txt; with `out_app_flag = 2` a one-based `g{geom}` index is inserted, e.g., pdoss1g1.txt. The PDOS file uses a plain-text format. Each row corresponds to one energy point and one atom. Columns: energy(eV), atom (1-based), species, then pdos values ordered as s(1), p(3), d(5), f(7), etc. Zeta components are summed, and values below 1e-6 are zeroed out. diff --git a/docs/advanced/elec_properties/hs_matrix.md b/docs/advanced/elec_properties/hs_matrix.md index 5adf14e3971..4cd3b715786 100644 --- a/docs/advanced/elec_properties/hs_matrix.md +++ b/docs/advanced/elec_properties/hs_matrix.md @@ -39,7 +39,7 @@ The output filenames depend on the k-point algorithm and `nspin`: Here `${k}` is the one-based k-point index. For `nspin = 2`, the overlap matrix is spin-independent, so only one overlap file is written for each physical k point. The gamma-only algorithm does not support `nspin = 4`; use the multi-k algorithm with an explicit Gamma-only `KPT` file for a noncollinear calculation at Gamma. -When `out_app_flag` is false, `g${step}` is inserted before `_nao`, where `${step}` is the one-based ionic-step index. For example, the first spin channel at the first k point and first ionic step is written to `hk1s1g1_nao.txt`. +Only when `out_app_flag` is 2 is `g${step}` inserted before `_nao`, where `${step}` is the one-based ionic-step index. For example, the first spin channel at the first k point and first ionic step is written to `hk1s1g1_nao.txt`. With `out_app_flag = 0` the fixed file is truncated and overwritten at every write, and with `out_app_flag = 1` it accumulates one record per written step. Each output block starts with a comment header containing the one-based ionic-step index, filename, `gamma only` flag, and matrix dimensions. It is followed by `Row 1`, `Row 2`, and so on. Each row contains the matrix elements from the diagonal through the upper triangle. @@ -57,7 +57,7 @@ For `out_hsk 2`, the filenames in the table above use `.dat` instead of `.txt`. The file therefore contains `sizeof(int) + N(N+1)/2 * sizeof(double)` bytes for a gamma-only record and `sizeof(int) + N(N+1) * sizeof(double)` bytes for a multi-k or spinor record. The format uses the host integer representation and byte order and is intended for readers using a compatible ABI. -When `out_app_flag` is true, the first ionic step truncates the shared file and every later step appends another complete record. When it is false, each ionic step has a separate filename containing `g${step}` before `_nao`. +When `out_app_flag` is 1, the first ionic step truncates the shared file and every later step appends another complete record. When `out_app_flag` is 0, the fixed file is truncated and overwritten at every write. Only when `out_app_flag` is 2 does each ionic step have a separate filename containing `g${step}` before `_nao`. ## out_hsr @@ -95,11 +95,11 @@ In gamma-only mode, every generated file reports one Bravais lattice vector and ### Native Binary CSR Format -Set `out_hsr 2` to write the same H(R) and S(R) matrix sets with a `.dat` suffix. For example, an `nspin = 2` calculation writes `hrs1_nao.dat`, `hrs2_nao.dat`, and `sr_nao.dat`. When `out_app_flag` is false, the one-based ionic step is included before `_nao`, for example `hrs1g1_nao.dat` and `srg1_nao.dat`. +Set `out_hsr 2` to write the same H(R) and S(R) matrix sets with a `.dat` suffix. For example, an `nspin = 2` calculation writes `hrs1_nao.dat`, `hrs2_nao.dat`, and `sr_nao.dat`. When `out_app_flag` is 2, the one-based ionic step is included before `_nao`, for example `hrs1g1_nao.dat` and `srg1_nao.dat`. -Each ionic step is a complete record with no padding or self-describing header: +Each ionic step is a complete record with no padding; its header is self-identifying through a magic value and a format version: -1. Native `int`: zero-based ionic step, matrix dimension, number of R blocks. +1. Versioned header: five native `int` values, namely the magic `0x48535242` (`HSRB`), the format version (`2`), the one-based ionic step, the matrix dimension, and the number of R blocks. The magic and version are absent from the unversioned layout described in the migration note below. 2. For every R block in lexicographic `(Rx, Ry, Rz)` order, four native `int` values: `Rx`, `Ry`, `Rz`, and `nnz`. 3. `nnz` matrix values in CSR order. Real matrices use one native `double`; complex matrices use consecutive real and imaginary `double` values. 4. `nnz` native `int` column indices. @@ -107,7 +107,14 @@ Each ionic step is a complete record with no padding or self-describing header: All R blocks stored by the internal HContainer are present, including blocks with zero nonzero values. The sparse threshold is `1e-10`, matching text CSR output. The format uses the host integer representation and byte order and therefore requires a compatible ABI. It does not contain unit-cell, spin, or matrix-label metadata; those are determined by the calculation input and filename. -When `out_app_flag` is true, the first ionic step truncates the shared file and later ionic steps append complete records. Otherwise every ionic step is written to its own file. +When `out_app_flag` is 1, the first ionic step truncates the shared file and later ionic steps append complete records. When `out_app_flag` is 0, the fixed file is truncated and overwritten at every write. Only when `out_app_flag` is 2 is every ionic step written to its own file. + +> **Format version and migration note.** The native binary record is now versioned. Version `2` starts with the magic `0x48535242` and the version itself, and stores a one-based ionic step. Earlier files are unversioned and start directly with the ionic step, so the first `int` of an unversioned file is a small step index (`0` or `1`) rather than the magic, which makes the two layouts distinguishable. +> - **v3.11.0-beta9 and v3.11.0-beta10** wrote unversioned records with a **zero-based** ionic step (the first ionic step is stored as `0`). +> - **Development builds between that change and this versioning scheme** wrote unversioned records with a **one-based** ionic step, so an unversioned file cannot be interpreted from its content alone; the writer version is required. +> - **Version `2` and later** write the magic, the format version, and a **one-based** ionic step. +> +> The matrix data itself is unchanged; only the header and the step convention differ. Readers should reject a record whose first `int` is not the expected magic instead of guessing the layout, and must consult the format version before interpreting the remaining fields. Reading an unversioned beta file as version `2`, or a version `2` file as unversioned, shifts the reported ionic-step index and misaligns every following field. ### NPZ Format diff --git a/docs/advanced/elec_properties/position_matrix.md b/docs/advanced/elec_properties/position_matrix.md index c504a866b64..c617c62f8fd 100644 --- a/docs/advanced/elec_properties/position_matrix.md +++ b/docs/advanced/elec_properties/position_matrix.md @@ -18,7 +18,7 @@ Each file or each section of the appended file starts with "STEP: " followed by Each block here contains the matrix for the corresponding cell. There are three columns in each block, giving the matrix elements in x, y, z directions, respectively. There are altogether nbasis * nbasis lines in each block, which emulates the matrix elements. -In molecular dynamics (MD) calculations, if [out_app_flag](../input_files/input-main.md#out_app_flag) is set to true, then `data-rR-tr` is written in an append manner. Otherwise, output files will be put in a separate directory, `matrix`, and named as `$x`_data-rR-tr, where `$x` is the number of MD step. In addition, the output frequency is controlled by [out_freq_ion](../input_files/input-main.md#out_freq_ion). For example, if we are running a 10-step MD with out_freq_ion = 3, then `$x` will be 0, 3, 6, and 9. +In molecular dynamics (MD) calculations, the output frequency is controlled by [out_freq_ion](../input_files/input-main.md#out_freq_ion). With out_app_flag = 2, each written ionic step has its own file named `rrg${step}_nao.txt`, where `${step}` is the one-based ionic-step index; with out_app_flag = 0 or 1 the fixed name `rr_nao.txt` is reused (mode 0 truncates and overwrites it at every write, mode 1 appends one record per written step). The single-shot output of `calculation = get_s` is always named `rr_nao.txt`. The files are stored in `OUT.${suffix}` when [out_app_flag](../input_files/input-main.md#out_app_flag) is 0 or 1, and in its `MAT/` subdirectory when out_app_flag is 2. For example, if we are running a 10-step MD with out_freq_ion = 3 and out_app_flag = 2, the files are `MAT/rrg1_nao.txt`, `MAT/rrg4_nao.txt`, `MAT/rrg7_nao.txt`, and `MAT/rrg10_nao.txt`. ## get_s We also offer the option of only calculating the position matrix without running SCF. For that purpose, in `INPUT` file we need to set the keyword [calculation](../input_files/input-main.md#calculation) to `get_s`, and [out_mat_r](../input_files/input-main.md#out_mat_r) to `true`. diff --git a/docs/advanced/elec_properties/wfc.md b/docs/advanced/elec_properties/wfc.md index 354d8844fc4..b9812b80327 100644 --- a/docs/advanced/elec_properties/wfc.md +++ b/docs/advanced/elec_properties/wfc.md @@ -12,13 +12,13 @@ For `basis_type=pw` and `esolver_type=ksdft`, [`out_wfc_pw`](https://abacus-rtd. The files are stored in `OUT.${suffix}/`. Their pattern is `wfk{k}[s{spin}][g{geometry step}][e{electronic iteration}]_pw.txt` for `out_wfc_pw=1` and `wfk{k}[s{spin}][g{geometry step}][e{electronic iteration}]_pw.dat` for `out_wfc_pw=2`. The `s*` label is omitted for `nspin=1`, is `s1` or `s2` for `nspin=2`, and is `s4` for `nspin=4`. All PW files include the `k*` label, including Gamma-only calculations. -With `out_freq_ion=0`, files are written only when the electronic calculation converges or reaches `scf_nmax`, and the names contain neither `g*` nor `e*`. During structural relaxation or molecular dynamics, each later ionic step overwrites the same files. With `out_freq_ion>0`, output is restricted to the ionic steps selected by `out_freq_ion` and occurs at multiples of `out_freq_elec`, at convergence, or at `scf_nmax`; both `g*` and `e*` are included in the file names. A static `calculation=scf` or `calculation=nscf` run also receives `g1e*` indices when `out_freq_ion>0`. +With `out_freq_ion=0`, files are written only when the electronic calculation converges or reaches `scf_nmax`; no `g*` or `e*` index is added. During structural relaxation or molecular dynamics with `out_app_flag=1`, the first written step truncates the unindexed files and later ionic steps append one frame per step; with `out_app_flag=0` (the default) the unindexed files are overwritten at every write. With `out_freq_ion>0`, output is restricted to the ionic steps selected by `out_freq_ion`. For `out_app_flag=0` or `2`, output occurs at multiples of `out_freq_elec`, at convergence, or at `scf_nmax`; for `out_app_flag=1` only the last electronic iteration of each selected ionic step is written (`out_freq_elec` is ignored), so the appended file holds exactly one frame per step. Both `g*` and `e*` indices are then added only when `out_app_flag=2`; with `out_app_flag=0` the fixed files are truncated and overwritten at every write, and with `out_app_flag=1` they accumulate one frame per written step. -The [`init_wfc file binary`](../scf/initialization.md#wavefunction) path reads only unindexed binary `wf*_pw.dat` files from `read_file_dir`. Generate directly reusable files with `out_wfc_pw=2` and normally `out_freq_ion=0`. Text `wf*_pw.txt` files and files containing `g*` or `e*` indices are not matched automatically. +The [`init_wfc file binary`](../scf/initialization.md#wavefunction) path reads only unindexed binary `wf*_pw.dat` files from `read_file_dir`, starting from the first frame. Such directly reusable files are normally generated with `out_wfc_pw=2` and `out_freq_ion=0`; multi-frame files accumulated under `out_app_flag=1` are trajectories and only their first frame is visible to the reader. Text `wf*_pw.txt` files and files containing `g*` or `e*` indices are not matched automatically. For `basis_type=lcao`, [`out_wfc_lcao`](https://abacus-rtd.readthedocs.io/en/latest/advanced/input_files/input-main.html#out-wfc-lcao) controls the output of numerical-atomic-orbital coefficients: `1` writes text files with the `.txt` suffix and `2` writes binary files with the `.dat` suffix. Multi-k calculations generate `wfs{spin}k{k-point}_nao.txt` or `.dat`, while Gamma-only calculations generate `wfs{spin}_nao.txt` or `.dat`. -To reuse LCAO coefficients, point `read_file_dir` to the coefficient files and set `init_wfc file txt` for text input or `init_wfc file binary` for binary input. The selected extension is required; there is no automatic format fallback. Only independent files without geometry-step indices are supported, not files accumulated with `out_app_flag`. +To reuse LCAO coefficients, point `read_file_dir` to the coefficient files and set `init_wfc file txt` for text input or `init_wfc file binary` for binary input. The selected extension is required; there is no automatic format fallback. Only independent files without geometry-step indices are supported, not files accumulated with `out_app_flag=1`. ## Wave Function in Real Space diff --git a/docs/advanced/output_files/output-specification.md b/docs/advanced/output_files/output-specification.md index 71e55026865..8aedf37a0a6 100644 --- a/docs/advanced/output_files/output-specification.md +++ b/docs/advanced/output_files/output-specification.md @@ -55,12 +55,21 @@ All output file naming conventions can be found in the online documentation (wit - All index numbers start from 1 (not 0) - For Gamma-only algorithm in LCAO, no `k` index is included - Overlap matrix `s` does not distinguish spin, so only one matrix is output +- `out_app_flag = 0` keeps the fixed cube filenames (chg/tau/pot/elf) and + truncates and overwrites them at every write. `out_app_flag = 1` also keeps + the fixed filenames: the first written ionic step truncates them and every + later written step appends one complete cube frame whose first comment line + is `Ionic_Step {#}` (one-based). Only `out_app_flag = 2` adds the `g#` suffix + and writes those suffixed files into the `CHG/`, `POT/` and `ELF/` + subdirectories - For initial charge density output (`out_chg = 2`): - - `out_freq_ion = 0`: `chg_ini.cube` (nspin=1) or `chgs{#}_ini.cube` (nspin=2/4) - output at every step (overwrite same file) - - `out_freq_ion > 0`: `chgg{#}_ini.cube` (nspin=1) or `chgs{#}g{#}_ini.cube` (nspin=2/4) - output every out_freq_ion steps + - `nspin=1`: `chg_ini.cube`; `nspin=2/4`: `chgs{#}_ini.cube` + - `out_freq_ion = 0` writes at every step; `out_freq_ion > 0` writes every `out_freq_ion` steps + - Only `out_app_flag = 2` inserts `g{#}` (e.g., `chgg1_ini.cube`, `chgs1g1_ini.cube`); modes 0 and 1 keep the fixed name - For initial potential output (`out_pot = 3`): - - `out_freq_ion = 0`: `pot_ini.cube` (nspin=1) or `pots{#}_ini.cube` (nspin=2/4) - output at every step (overwrite same file) - - `out_freq_ion > 0`: `potg{#}_ini.cube` (nspin=1) or `pots{#}g{#}_ini.cube` (nspin=2/4) - output every out_freq_ion steps + - `nspin=1`: `pot_ini.cube`; `nspin=2/4`: `pots{#}_ini.cube` + - `out_freq_ion = 0` writes at every step; `out_freq_ion > 0` writes every `out_freq_ion` steps + - Only `out_app_flag = 2` inserts `g{#}` (e.g., `potg1_ini.cube`, `pots1g1_ini.cube`); modes 0 and 1 keep the fixed name ### 2.2 Examples @@ -71,22 +80,22 @@ All output file naming conventions can be found in the online documentation (wit | `chgs3.cube` | Charge density, spin 3 (non-collinear with SOC) | | `chg_ini.cube` | Initial charge density (out_freq_ion=0, nspin=1) | | `chgs1_ini.cube` | Initial charge density (out_freq_ion=0, spin 1, nspin=2/4) | -| `chgg1_ini.cube` | Initial charge density, geometry step 1 (nspin=1) | -| `chgs1g1_ini.cube` | Initial charge density, spin 1, geometry step 1 (nspin=2/4) | +| `chgg1_ini.cube` | Initial charge density, geometry step 1, out_app_flag=2 (nspin=1) | +| `chgs1g1_ini.cube` | Initial charge density, spin 1, geometry step 1, out_app_flag=2 (nspin=2/4) | | `pot_ini.cube` | Initial potential (out_freq_ion=0, nspin=1) | | `pots1_ini.cube` | Initial potential (out_freq_ion=0, spin 1, nspin=2/4) | -| `potg1_ini.cube` | Initial potential, geometry step 1 (nspin=1) | -| `pots1g1_ini.cube` | Initial potential, spin 1, geometry step 1 (nspin=2/4) | +| `potg1_ini.cube` | Initial potential, geometry step 1, out_app_flag=2 (nspin=1) | +| `pots1g1_ini.cube` | Initial potential, spin 1, geometry step 1, out_app_flag=2 (nspin=2/4) | | `pots1.cube` | Local potential, spin 1 | | `elftot.cube` | ELF, total (nspin=1/4) | | `elfs1.cube` | ELF, spin 1 (nspin=2) | | `elfs2.cube` | ELF, spin 2 (nspin=2) | | `elftot.cube` | ELF, total (nspin=2) | -| `elftotg1.cube` | ELF, total, geometry step 1 (nspin=1/4) | -| `elfs1g1.cube` | ELF, spin 1, geometry step 1 (nspin=2) | -| `elftotg1.cube` | ELF, total, geometry step 1 (nspin=2) | +| `elftotg1.cube` | ELF, total, geometry step 1, out_app_flag=2 (nspin=1/4) | +| `elfs1g1.cube` | ELF, spin 1, geometry step 1, out_app_flag=2 (nspin=2) | +| `elftotg1.cube` | ELF, total, geometry step 1, out_app_flag=2 (nspin=2) | | `eig_occ.txt` | Eigenvalues and occupations | -| `doss1g1.txt` | DOS, spin 1, geometry step 1 | +| `doss1g1.txt` | DOS, spin 1, geometry step 1, out_app_flag=2 | | `wf_pw.dat` | Wavefunction, plane wave basis | | `sr.csr` | Overlap matrix in real space (no spin index) | @@ -95,31 +104,46 @@ All output file naming conventions can be found in the online documentation (wit | File Name | Description | |-----------|-------------| | `running_scf.log` | SCF iteration log | -| `doss1g1.txt` | Density of states | +| `doss1g1.txt` | Density of states, spin 1, geometry step 1, out_app_flag=2 | | `eig_occ.txt` | Eigenvalues and occupations | | `mulliken.txt` | Mulliken population analysis | -| `band.txt` | Band structure | +| `band.txt` | Band structure (nspin=1/4); `bands1.txt`/`bands2.txt` when nspin=2 | | `chgs1.cube`, `chgs2.cube` | Charge density (spin 1, spin 2) | | `chg.cube` | Total charge density | | Initial charge density (out_chg=2) | | | `chg_ini.cube` | Initial charge density (out_freq_ion=0, nspin=1) | | `chgs{#}_ini.cube` | Initial charge density (out_freq_ion=0, spin {#}, nspin=2/4) | -| `chgg{#}_ini.cube` | Initial charge density (out_freq_ion>0, geometry step {#}, nspin=1) | -| `chgs{#}g{#}_ini.cube` | Initial charge density (out_freq_ion>0, spin {#}, geometry step {#}, nspin=2/4) | +| `chgg{#}_ini.cube` | Initial charge density (geometry step {#}, nspin=1, out_app_flag=2) | +| `chgs{#}g{#}_ini.cube` | Initial charge density (spin {#}, geometry step {#}, nspin=2/4, out_app_flag=2) | | Initial potential (out_pot=3) | | | `pot_ini.cube` | Initial potential (out_freq_ion=0, nspin=1) | | `pots{#}_ini.cube` | Initial potential (out_freq_ion=0, spin {#}, nspin=2/4) | -| `potg{#}_ini.cube` | Initial potential (out_freq_ion>0, geometry step {#}, nspin=1) | -| `pots{#}g{#}_ini.cube` | Initial potential (out_freq_ion>0, spin {#}, geometry step {#}, nspin=2/4) | +| `potg{#}_ini.cube` | Initial potential (geometry step {#}, nspin=1, out_app_flag=2) | +| `pots{#}g{#}_ini.cube` | Initial potential (spin {#}, geometry step {#}, nspin=2/4, out_app_flag=2) | | `taus1.cube`, `taus2.cube` | Kinetic energy density (tau) | | `pots1.cube`, `pots2.cube` | Local potential | | ELF (out_elf) | | | `elftot.cube` | ELF, total (nspin=1/4) | | `elfs1.cube`, `elfs2.cube` | ELF, spin 1/2 (nspin=2) | | `elftot.cube` | ELF, total (nspin=2) | -| `elftotg{#}.cube` | ELF, total, geometry step {#} (nspin=1/4) | -| `elfs{#}g{#}.cube` | ELF, spin {#}, geometry step {#} (nspin=2) | -| `elftotg{#}.cube` | ELF, total, geometry step {#} (nspin=2) | +| `elftotg{#}.cube` | ELF, total, geometry step {#}, out_app_flag=2 (nspin=1/4) | +| `elfs{#}g{#}.cube` | ELF, spin {#}, geometry step {#}, out_app_flag=2 (nspin=2) | +| `elftotg{#}.cube` | ELF, total, geometry step {#}, out_app_flag=2 (nspin=2) | + +**Band structure files (`out_band`):** each ionic step of `band.txt`, +`bands1.txt` and `bands2.txt` is preceded by a provenance comment block, so the +k-point blocks appended by a relax/md run stay distinguishable: + +``` +# ABACUS version: v3.11.0-beta11 +# Written at 2026-10-20 15:40:45 +# IONIC STEP 1 (spin up) + 1 0.00000000 -5.36802595 6.70427084 ... +``` + +`# IONIC STEP` is one-based and carries the collinear spin-channel label only +when `nspin = 2`. Consumers that parse the numeric columns must skip lines +starting with `#`. ## 3. File Format Standards diff --git a/docs/advanced/scf/initialization.md b/docs/advanced/scf/initialization.md index 427146bf223..4517b691b44 100644 --- a/docs/advanced/scf/initialization.md +++ b/docs/advanced/scf/initialization.md @@ -26,7 +26,7 @@ For `basis_type=pw`, the available options are: * `nao+random`: Apply an approximately 5% multiplicative random perturbation to the NAO initialization; any bands not covered by NAO states are first initialized randomly. * `file binary`: Read binary `wf*_pw.dat` files generated with [`out_wfc_pw=2`](../elec_properties/wfc.md) from `read_file_dir`. The files must match the current k points, `nbands`, plane-wave layout, and lattice. The `txt` format is not supported for PW wavefunctions. -For `basis_type=lcao`, use `init_wfc file txt` to read text `wf*_nao.txt` files generated with [`out_wfc_lcao=1`](../elec_properties/wfc.md), or `init_wfc file binary` to read binary `wf*_nao.dat` files generated with `out_wfc_lcao=2`, from `read_file_dir`. LCAO wavefunctions initialize the density matrix and real-space charge density. The selected format is required; ABACUS does not automatically detect or fall back to the other format. The files must use a compatible NAO basis, match the current k-point and spin setup, and contain enough bands. Only independent files without geometry-step indices are supported, not files accumulated with `out_app_flag`. +For `basis_type=lcao`, use `init_wfc file txt` to read text `wf*_nao.txt` files generated with [`out_wfc_lcao=1`](../elec_properties/wfc.md), or `init_wfc file binary` to read binary `wf*_nao.dat` files generated with `out_wfc_lcao=2`, from `read_file_dir`. LCAO wavefunctions initialize the density matrix and real-space charge density. The selected format is required; ABACUS does not automatically detect or fall back to the other format. The files must use a compatible NAO basis, match the current k-point and spin setup, and contain enough bands. Only independent files without geometry-step indices are supported, not files accumulated with `out_app_flag=1`. For `basis_type=lcao_in_pw`, `init_wfc` is automatically set to `nao`. diff --git a/examples/17_relax/04_relax_with_output_lcao/INPUT b/examples/17_relax/04_relax_with_output_lcao/INPUT index f58cf4201de..e8408708355 100644 --- a/examples/17_relax/04_relax_with_output_lcao/INPUT +++ b/examples/17_relax/04_relax_with_output_lcao/INPUT @@ -34,7 +34,6 @@ out_dmr 1 out_dos 1 out_band 1 out_stru 1 -out_app_flag 0 out_mat_l 1 out_mat_r 1 @@ -42,3 +41,5 @@ out_mat_t 1 #out_mat_dh 1 out_freq_ion 2 + +out_app_flag 2 diff --git a/examples/18_md/02_lcao_output/INPUT b/examples/18_md/02_lcao_output/INPUT index 7c29d6e1355..c8333b2f0e8 100644 --- a/examples/18_md/02_lcao_output/INPUT +++ b/examples/18_md/02_lcao_output/INPUT @@ -36,3 +36,5 @@ md_tchain 1 dft_functional hse restart_save 1 + +out_app_flag 0 diff --git a/interfaces/Multiwfn_interface/molden.py b/interfaces/Multiwfn_interface/molden.py index 1935267a2c9..856cf55aad9 100644 --- a/interfaces/Multiwfn_interface/molden.py +++ b/interfaces/Multiwfn_interface/molden.py @@ -533,6 +533,10 @@ def read_abacus_lowf(flowf, pat=r'^(WFC_NAO_(K\d+|GAMMA\d+)(_ION\d+)?|wf((k\d+)? with open(flowf, 'r') as file: lines = file.readlines() + # ignore provenance comment lines (version/timestamp/ionic step) that + # ABACUS writes before the data block + lines = [line for line in lines if not line.lstrip().startswith('#')] + if lines[0].endswith('(index of k points)\n'): # discard the first two lines lines = lines[2:] diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 243b1ee57f4..97c72f0fad7 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -238,6 +238,7 @@ OBJS_CELL=atom_pseudo.o\ update_cell.o\ bcast_cell.o\ read_stru.o\ + stru_frame_io.o\ read_atom_species.o\ sep.o\ sep_cell.o\ diff --git a/source/source_base/fs_compat.h b/source/source_base/fs_compat.h index 3e3a58e0dca..4ad1c0054c2 100644 --- a/source/source_base/fs_compat.h +++ b/source/source_base/fs_compat.h @@ -43,6 +43,30 @@ inline int make_directory(const std::string& path) #endif } +/** + * @brief Ensure a directory exists, creating it lazily if needed. + * + * Intended for write-time directory creation: output writers call this just + * before writing a file into a directory that may not exist yet, instead of + * relying on a central startup routine to predict which directories will be + * needed. An already-existing directory (EEXIST) is not an error, so callers + * on different MPI ranks may invoke it concurrently for the same path. The + * parent directory must already exist (only one level is created). + * + * @param path directory path to ensure; an empty path is accepted as a no-op + * @return true if the directory now exists (created or already present), + * false on a real creation failure (errno is set) + */ +inline bool ensure_directory(const std::string& path) +{ + if (path.empty()) + { + return true; + } + const int ret = make_directory(path); + return ret == 0 || errno == EEXIST; +} + } // namespace ModuleBase #endif // MODULEBASE_FS_COMPAT_H diff --git a/source/source_base/global_file.cpp b/source/source_base/global_file.cpp index 76a16036be7..1889357c2c8 100644 --- a/source/source_base/global_file.cpp +++ b/source/source_base/global_file.cpp @@ -30,7 +30,6 @@ void ModuleBase::Global_File::make_dir_out( const bool &restart, const bool out_alllog, const std::string &global_out_dir, - const std::string &global_stru_dir, const std::string &global_matrix_dir, const std::string &global_wfc_dir, const std::string &global_mlkedf_descriptor_dir, @@ -84,45 +83,6 @@ void ModuleBase::Global_File::make_dir_out( MPI_Barrier(MPI_COMM_WORLD); #endif - if(calculation == "md") - { - int make_dir_stru = 0; - - times = 0; - while(times0) { break; -} - ++times; - } - -#ifdef __MPI - if(make_dir_stru==0) - { - std::cout << " CAN NOT MAKE THE STRU DIR......." << std::endl; - ModuleBase::QUIT(); - } - MPI_Barrier(MPI_COMM_WORLD); -#endif - } - // make dir for HS matrix output in md calculation if((out_dir) && calculation == "md") { diff --git a/source/source_base/global_file.h b/source/source_base/global_file.h index d051b5e81c4..4ce5adb18d3 100644 --- a/source/source_base/global_file.h +++ b/source/source_base/global_file.h @@ -28,7 +28,6 @@ namespace Global_File const bool &restart, const bool out_alllog, const std::string &global_out_dir, - const std::string &global_stru_dir, const std::string &global_matrix_dir, const std::string &global_wfc_dir, const std::string &global_mlkedf_descriptor_dir, diff --git a/source/source_base/module_out/filename.cpp b/source/source_base/module_out/filename.cpp index 4dd86dc2bea..3f1b464ca03 100644 --- a/source/source_base/module_out/filename.cpp +++ b/source/source_base/module_out/filename.cpp @@ -14,7 +14,7 @@ std::string filename_output( const int nspin, const int nkstot, const int out_type, - const bool out_app_flag, + const int out_app_flag, const bool gamma_only, const int istep, const int iter) @@ -86,7 +86,11 @@ std::string filename_output( else { // mohan add 20250921 - if(property=="hk" || property=="sk" || property=="tk") + // A property whose name already ends in 'k' denotes a k-space + // quantity (hk, sk, tk, vhk, vlk, vnlk, vxck, vexxk, ...); the + // k-point index then needs no extra 'k' prefix, otherwise the + // filename would contain a doubled 'k' (e.g. vhkk1). + if (!property.empty() && property.back() == 'k') { kpoint_block = std::to_string(ik0+1); } @@ -97,14 +101,14 @@ std::string filename_output( } std::string istep_block - = (istep >= 0 && (!out_app_flag)) + = (istep >= 0 && (out_app_flag == 2)) ? "g" + std::to_string(istep + 1) // istep starts from 0 - : ""; // only when istep >= 0 and out_app_flag is false will write each wfc to a separate file + : ""; // only mode 2 writes each step to a separate g{step} file std::string iter_block - = (iter >= 0 && (!out_app_flag)) + = (iter >= 0 && (out_app_flag == 2)) ? "e" + std::to_string(iter) // iter starts from 1 - : ""; // only when istep >= 0 and out_app_flag is false will write each wfc to a separate file + : ""; // only mode 2 writes each electronic iteration to a separate e{iter} file std::string suffix_block; if (out_type == 1) diff --git a/source/source_base/module_out/filename.h b/source/source_base/module_out/filename.h index d76970c769f..d95b24c2453 100644 --- a/source/source_base/module_out/filename.h +++ b/source/source_base/module_out/filename.h @@ -16,10 +16,13 @@ namespace ModuleIO * @param nspin: number of spin channels, 1,2 or 4 * @param nkstot: number of total k-points * @param out_type: two types of output file format, 1 for .txt and 2 for .dat (binary) - * @param out_app_flag: whether to append to existing file. + * @param out_app_flag: 0=fixed filename, overwritten at each write; 1=fixed filename, appended; + * 2=subdirectory layout with a g{step} suffix. * @param gamma_only: gamma_only algorithm or not. - * @param istep: index of the ion step starting from 0. If < 0, the step number is not included in the file name. - * @param iter: index of the electronic step starting from 0. If < 0, the step number is not included in the file name. + * @param istep: index of the ion step starting from 0. The step number is only embedded in the + * filename in mode 2 (out_app_flag == 2), and only when istep >= 0. + * @param iter: index of the electronic step starting from 0. The step number is only embedded in + * the filename in mode 2 (out_app_flag == 2), and only when iter >= 0. * @return The generated filename. */ std::string filename_output( @@ -31,7 +34,7 @@ std::string filename_output( const int nspin, const int nkstot, const int out_type, - const bool out_app_flag, + const int out_app_flag, const bool gamma_only, const int istep=-1, const int iter=-1); diff --git a/source/source_base/test/CMakeLists.txt b/source/source_base/test/CMakeLists.txt index 4bff3c97cbd..d6eeb48b714 100644 --- a/source/source_base/test/CMakeLists.txt +++ b/source/source_base/test/CMakeLists.txt @@ -281,3 +281,9 @@ if(ENABLE_GOOGLEBENCH) SOURCES perf_sphbes_test.cpp ../math_sphbes.cpp ../timer.cpp ) endif() + +AddTest( + TARGET MODULE_BASE_filename + LIBS parameter base device + SOURCES test_filename.cpp +) diff --git a/source/source_base/test/global_file_test.cpp b/source/source_base/test/global_file_test.cpp index 07184237553..fe7bdac7446 100644 --- a/source/source_base/test/global_file_test.cpp +++ b/source/source_base/test/global_file_test.cpp @@ -25,7 +25,6 @@ class GlobalFile : public testing::Test { protected: std::string global_out_dir; - std::string global_stru_dir; std::string global_matrix_dir; std::string global_wfc_dir; std::string global_mlkedf_descriptor_dir; @@ -34,7 +33,6 @@ class GlobalFile : public testing::Test { const std::string suffix = "Si"; global_out_dir = "OUT." + suffix + "/"; - global_stru_dir = global_out_dir + "STRU/"; global_matrix_dir = global_out_dir + "matrix/"; global_wfc_dir = global_out_dir + "WFC/"; global_mlkedf_descriptor_dir = global_out_dir + "MLKEDF_Descriptors/"; @@ -47,7 +45,7 @@ TEST_F(GlobalFile,mkdirout) std::string output; testing::internal::CaptureStdout(); ModuleBase::Global_File::make_dir_out("Si","m",false,false,0,true,true, - global_out_dir, global_stru_dir, global_matrix_dir, global_wfc_dir, + global_out_dir, global_matrix_dir, global_wfc_dir, global_mlkedf_descriptor_dir, global_deepks_label_elec_dir, "running_m_1.log", false, false); output = testing::internal::GetCapturedStdout(); @@ -58,22 +56,9 @@ TEST_F(GlobalFile,mkdirout) std::string dd = "OUT.Si/running_m_1.log"; remove(dd.c_str()); - testing::internal::CaptureStdout(); - ModuleBase::Global_File::make_dir_out("Si","md",false,false,0,true,false, - global_out_dir, global_stru_dir, global_matrix_dir, global_wfc_dir, - global_mlkedf_descriptor_dir, global_deepks_label_elec_dir, - "running_md.log", false, false); - output = testing::internal::GetCapturedStdout(); - EXPECT_THAT(output,testing::HasSubstr("MAKE THE STRU DIR")); - EXPECT_TRUE(GlobalV::ofs_running.is_open()); - GlobalV::ofs_running.close(); - GlobalV::ofs_warning.close(); - std::string bb = "OUT.Si/running_md.log"; - remove(bb.c_str()); - testing::internal::CaptureStdout(); ModuleBase::Global_File::make_dir_out("Si","md",true,false,0,true,true, - global_out_dir, global_stru_dir, global_matrix_dir, global_wfc_dir, + global_out_dir, global_matrix_dir, global_wfc_dir, global_mlkedf_descriptor_dir, global_deepks_label_elec_dir, "running_md_1.log", false, false); output = testing::internal::GetCapturedStdout(); @@ -84,7 +69,6 @@ TEST_F(GlobalFile,mkdirout) remove(cc.c_str()); std::string aa = "OUT.Si/warning.log"; remove(aa.c_str()); - rmdir(global_stru_dir.c_str()); rmdir(global_matrix_dir.c_str()); rmdir(global_out_dir.c_str()); } diff --git a/source/source_base/test/test_filename.cpp b/source/source_base/test/test_filename.cpp new file mode 100644 index 00000000000..254040cb0b3 --- /dev/null +++ b/source/source_base/test/test_filename.cpp @@ -0,0 +1,126 @@ +#ifndef TEST_FILENAME_CPP +#define TEST_FILENAME_CPP + +#include +#include +#include +#include "source_base/module_out/filename.h" + +// Keep local names short; do not pollute. +using ModuleIO::filename_output; + +// --------------------------------------------------------------------------- +// Fixture: minimal ik2iktot mapping for a 2-k-point pool. +// --------------------------------------------------------------------------- +class FilenameOutputTest : public ::testing::Test +{ +protected: + const std::vector ik2iktot = {0, 1}; + const int ik_local = 0; + const int nkstot = 2; + const int nspin = 2; + const std::string dir = "OUT.test/"; +}; + +// --------------------------------------------------------------------------- +// mode 0 = overwrite a fixed filename -> no g{N} suffix even when istep >= 0. +// --------------------------------------------------------------------------- +TEST_F(FilenameOutputTest, Mode0FixedNameIgnoresIstep) +{ + // PW wavefunction, binary, k1, spin up (ik0=0 < half_k=1 -> s1). + const std::string fn = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, nspin, nkstot, + /*out_type=*/2, /*out_app_flag=*/0, /*gamma_only=*/false, + /*istep=*/1, /*iter=*/-1); + EXPECT_EQ(fn, "OUT.test/wfk1s1_pw.dat"); +} + +// --------------------------------------------------------------------------- +// mode 1 = append -> fixed name, no g suffix even when istep >= 0. +// --------------------------------------------------------------------------- +TEST_F(FilenameOutputTest, Mode1FixedNameNoG) +{ + const std::string fn = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, nspin, nkstot, + /*out_type=*/2, /*out_app_flag=*/1, /*gamma_only=*/false, + /*istep=*/1, /*iter=*/-1); + EXPECT_EQ(fn, "OUT.test/wfk1s1_pw.dat"); +} + +// --------------------------------------------------------------------------- +// mode 2 = per-step file -> the only mode that embeds the g{N} suffix. +// The directory routing is handled by the caller (set_global_dir). +// --------------------------------------------------------------------------- +TEST_F(FilenameOutputTest, Mode2EmbedsStep) +{ + const std::string fn = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, nspin, nkstot, + /*out_type=*/2, /*out_app_flag=*/2, /*gamma_only=*/false, + /*istep=*/1, /*iter=*/-1); + EXPECT_EQ(fn, "OUT.test/wfk1s1g2_pw.dat"); +} + +// --------------------------------------------------------------------------- +// istep < 0 (SCF single-point) -> no g suffix in any mode. +// --------------------------------------------------------------------------- +TEST_F(FilenameOutputTest, SinglePointNoG) +{ + const std::string fn = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, nspin, nkstot, + /*out_type=*/2, /*out_app_flag=*/0, /*gamma_only=*/false, + /*istep=*/-1, /*iter=*/-1); + EXPECT_EQ(fn, "OUT.test/wfk1s1_pw.dat"); +} + +// --------------------------------------------------------------------------- +// iter >= 0 adds e{N} only in mode 2; modes 0 and 1 keep the fixed filename. +// --------------------------------------------------------------------------- +TEST_F(FilenameOutputTest, IterIndexOnlyInMode2) +{ + // mode 0: neither g nor e. + const std::string fn_mode0 = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, nspin, nkstot, + /*out_type=*/1, /*out_app_flag=*/0, /*gamma_only=*/false, + /*istep=*/1, /*iter=*/3); + EXPECT_EQ(fn_mode0, "OUT.test/wfk1s1_pw.txt"); + + // mode 1: neither g nor e. + const std::string fn_mode1 = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, nspin, nkstot, + /*out_type=*/1, /*out_app_flag=*/1, /*gamma_only=*/false, + /*istep=*/1, /*iter=*/3); + EXPECT_EQ(fn_mode1, "OUT.test/wfk1s1_pw.txt"); + + // mode 2: both g and e present. + const std::string fn_mode2 = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, nspin, nkstot, + /*out_type=*/1, /*out_app_flag=*/2, /*gamma_only=*/false, + /*istep=*/1, /*iter=*/3); + EXPECT_EQ(fn_mode2, "OUT.test/wfk1s1g2e3_pw.txt"); +} + +// --------------------------------------------------------------------------- +// gamma_only suppresses the k-point label for non-hk/sk/tk properties. +// --------------------------------------------------------------------------- +TEST_F(FilenameOutputTest, GammaOnlyNoKLabel) +{ + const std::string fn = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, nspin, nkstot, + /*out_type=*/2, /*out_app_flag=*/1, /*gamma_only=*/true, + /*istep=*/-1, /*iter=*/-1); + EXPECT_EQ(fn, "OUT.test/wfs1_pw.dat"); +} + +// --------------------------------------------------------------------------- +// nspin=4 (noncollinear SOC) -> spin block is s4, no half-k split. +// --------------------------------------------------------------------------- +TEST_F(FilenameOutputTest, NoncollinearSpin4) +{ + const std::string fn = filename_output( + dir, "wf", "pw", ik_local, ik2iktot, /*nspin=*/4, nkstot, + /*out_type=*/2, /*out_app_flag=*/2, /*gamma_only=*/false, + /*istep=*/0, /*iter=*/-1); + EXPECT_EQ(fn, "OUT.test/wfk1s4g1_pw.dat"); +} + +#endif // TEST_FILENAME_CPP diff --git a/source/source_cell/CMakeLists.txt b/source/source_cell/CMakeLists.txt index 3d83e0a783d..8e759788772 100644 --- a/source/source_cell/CMakeLists.txt +++ b/source/source_cell/CMakeLists.txt @@ -38,6 +38,7 @@ add_library( magnetism.cpp bcast_cell.cpp read_stru.cpp + stru_frame_io.cpp print_cell.cpp read_atom_species.cpp sep.cpp diff --git a/source/source_cell/cell_index.cpp b/source/source_cell/cell_index.cpp index c147afa936f..f6b9d3d8362 100644 --- a/source/source_cell/cell_index.cpp +++ b/source/source_cell/cell_index.cpp @@ -264,7 +264,7 @@ void CellIndex::cal_orbitalCounts() void CellIndex::write_orb_info(const std::string& out_dir) const { std::stringstream os; - os << out_dir << "Orbital"; + os << out_dir << "nao.txt"; std::ofstream out(os.str().c_str()); out << std::setw(5) << "#io" << std::setw(8) << "spec" << std::setw(5) << "l" << std::setw(5) << "m" << std::setw(5) << "z" << std::setw(5) << "sym" << std::endl; diff --git a/source/source_cell/module_neighlist/test/CMakeLists.txt b/source/source_cell/module_neighlist/test/CMakeLists.txt index f43b2aa505e..e11e704da35 100644 --- a/source/source_cell/module_neighlist/test/CMakeLists.txt +++ b/source/source_cell/module_neighlist/test/CMakeLists.txt @@ -121,6 +121,7 @@ if(ENABLE_MPI) ../../mdcell_reader.cpp ../../mdcell.cpp ../../print_cell.cpp + ../../stru_frame_io.cpp ../../domain_decomposition.cpp ../neighbor_search.cpp ../bin_manager.cpp diff --git a/source/source_cell/module_neighlist/test/test_mdcell_reader.cpp b/source/source_cell/module_neighlist/test/test_mdcell_reader.cpp index 50c907ab4f0..043e256c06f 100644 --- a/source/source_cell/module_neighlist/test/test_mdcell_reader.cpp +++ b/source/source_cell/module_neighlist/test/test_mdcell_reader.cpp @@ -1,9 +1,11 @@ #include "source_cell/domain_decomposition.h" +#include #include #include "source_cell/mdcell_reader.h" #include "source_cell/mdcell.h" #include "source_cell/print_cell.h" +#include "source_cell/stru_frame_io.h" #include "source_base/constants.h" #include "source_base/parallel_cell.h" #include "source_base/global_variable.h" @@ -152,10 +154,9 @@ TEST(MDCellReaderTest, ReadOwnedAtomsFromSTRUWithoutUnitCell) TEST(MDCellReaderTest, RestartStruPreservesAtomRecordsAcrossRanks) { - int rank = 0; - int size = 1; - MPI_Comm_rank(MPI_COMM_WORLD, &rank); - MPI_Comm_size(MPI_COMM_WORLD, &size); + const ModuleBase::CommunicationDomain world_domain = ModuleBase::world_comm_domain(); + const int rank = world_domain.rank(); + const int size = world_domain.size(); ASSERT_GE(size, 4); std::vector owned_atoms; @@ -227,7 +228,7 @@ TEST(MDCellReaderTest, RestartStruPreservesAtomRecordsAcrossRanks) StruMeta metadata; metadata.species.resize(2); const std::string output_file = "distributed_mdcell_restart.STRU"; - mdcell::print_stru_file(mdcell, metadata, output_file); + mdcell::print_stru_file(mdcell, metadata, output_file, "", false); DomainDecomposition reader_decomp; MDCell round_trip = MDCellReader::read_stru(output_file, @@ -319,6 +320,168 @@ TEST(MDCellReaderTest, RestartStruPreservesAtomRecordsAcrossRanks) if (rank == 0) std::remove(output_file.c_str()); } +// --------------------------------------------------------------------------- +// mdcell::print_stru_file append + frame_comment. +// --------------------------------------------------------------------------- +TEST(MDCellReaderTest, STRUAppendWithFrameComment) +{ + const ModuleBase::CommunicationDomain world_domain = ModuleBase::world_comm_domain(); + const int rank = world_domain.rank(); + + std::vector owned_atoms; + owned_atoms.push_back(LocalAtom(ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(1, 0, 1), + 1.0, + 0, + 0, + rank)); + + ModuleBase::Matrix3 lattice = make_lattice(); + MDCell mdcell; + mdcell.initialize_from_owned_atoms(lattice, + lattice.Inverse(), + 1.0, + 1.0, + 1, + owned_atoms, + std::vector{"A"}, + std::vector{1.0}, + std::vector{1}, + 0.0, + ModuleBase::world_comm_domain()); + StruMeta metadata; + metadata.species.resize(1); + const std::string output_file = "mdcell_append_test.STRU"; + + // First frame: append=false, frame comment set. + mdcell::print_stru_file(mdcell, metadata, output_file, + "# MD step 1\n\n", /*append=*/false); + // Second frame: append=true with a different comment. + mdcell::print_stru_file(mdcell, metadata, output_file, + "# MD step 2\n\n", /*append=*/true); + + if (rank == 0) + { + std::ifstream ifs(output_file.c_str()); + std::string str((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + // Both frame markers must be present. + EXPECT_THAT(str, testing::HasSubstr("# MD step 1")); + EXPECT_THAT(str, testing::HasSubstr("# MD step 2")); + // Two STRU blocks -> two ATOMIC_SPECIES markers. + int occurrences = 0; + size_t pos = 0; + while ((pos = str.find("ATOMIC_SPECIES", pos)) != std::string::npos) + { + ++occurrences; + pos += 14; + } + EXPECT_EQ(occurrences, 2); + std::remove(output_file.c_str()); + } +} + +// --------------------------------------------------------------------------- +// Fresh-run truncation: rewriting the fixed-name file (append=false) must +// drop the frames left by a previous run so extract_last_frame only sees the +// new run's frames. +// --------------------------------------------------------------------------- +TEST(MDCellReaderTest, STRUFreshRunTruncatesPreviousFrames) +{ + const ModuleBase::CommunicationDomain world_domain = ModuleBase::world_comm_domain(); + const int rank = world_domain.rank(); + + std::vector owned_atoms; + owned_atoms.push_back(LocalAtom(ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(1, 0, 1), + 1.0, + 0, + 0, + rank)); + + ModuleBase::Matrix3 lattice = make_lattice(); + const ModuleBase::Matrix3 inverse = lattice.Inverse(); + const std::vector species_labels = {"A"}; + const std::vector species_masses = {1.0}; + const std::vector species_counts = {1}; + MDCell mdcell; + mdcell.initialize_from_owned_atoms(lattice, + inverse, + 1.0, + 1.0, + 1, + owned_atoms, + species_labels, + species_masses, + species_counts, + 0.0, + world_domain); + StruMeta metadata; + metadata.species.resize(1); + const std::string output_file = testing::TempDir() + "mdcell_fresh_run_test.STRU"; + const std::string frame_file = testing::TempDir() + "mdcell_fresh_run_test.frame"; + + // Previous run: frames 1 and 2 accumulate. + mdcell::print_stru_file(mdcell, metadata, output_file, + "# MD step 1\n\n", /*append=*/false); + mdcell::print_stru_file(mdcell, metadata, output_file, + "# MD step 2\n\n", /*append=*/true); + + // The extraction runs on rank zero only; broadcast its result so every + // rank takes the same (collective-safe) return decision. Later + // print_stru_file calls broadcast/reduce across the whole communicator, + // so a rank-local ASSERT here would hang the remaining ranks. + const bool extracted = (rank == 0) + ? stru_frame_io::extract_last_frame(output_file, frame_file) + : true; + const int extracted_local = extracted ? 1 : 0; + const int extracted_all = world_domain.max(extracted_local); + ASSERT_EQ(extracted_all, 1); + + if (rank == 0) + { + std::ifstream prev(frame_file.c_str()); + std::string prev_str((std::istreambuf_iterator(prev)), + std::istreambuf_iterator()); + prev.close(); + EXPECT_THAT(prev_str, testing::HasSubstr("ATOMIC_SPECIES")); + } + + // Fresh run: the first frame truncates the fixed-name file (append=false) + // and starts numbering from step 1 again. + mdcell::print_stru_file(mdcell, metadata, output_file, + "# MD step 1\n\n", /*append=*/false); + + if (rank == 0) + { + std::ifstream ifs(output_file.c_str()); + std::string str((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + // Only the fresh run's single frame remains: one ATOMIC_SPECIES block, + // one frame marker, no trace of the previous step-2 frame. + int occurrences = 0; + size_t pos = 0; + while ((pos = str.find("ATOMIC_SPECIES", pos)) != std::string::npos) + { + ++occurrences; + pos += 14; + } + EXPECT_EQ(occurrences, 1); + EXPECT_THAT(str, testing::HasSubstr("# MD step 1")); + EXPECT_THAT(str, testing::Not(testing::HasSubstr("# MD step 2"))); + std::remove(output_file.c_str()); + std::remove(frame_file.c_str()); + } +} + int main(int argc, char** argv) { MPI_Init(&argc, &argv); diff --git a/source/source_cell/print_cell.cpp b/source/source_cell/print_cell.cpp index f98b3ab8a99..64822241cb4 100644 --- a/source/source_cell/print_cell.cpp +++ b/source/source_cell/print_cell.cpp @@ -101,7 +101,8 @@ namespace unitcell const bool& dpks_desc, const int& iproc, const ModuleBase::matrix& force, - const bool& has_force) + const bool& has_force, + const bool append) { ModuleBase::TITLE("UnitCell","print_stru_file"); if (iproc != 0) @@ -235,7 +236,12 @@ namespace unitcell nat_++; } } - std::ofstream ofs(fn.c_str()); + std::ios_base::openmode mode = std::ios::out; + if (append) + { + mode |= std::ios::app; + } + std::ofstream ofs(fn.c_str(), mode); ofs << str; ofs.close(); return; @@ -368,7 +374,8 @@ StruMeta make_stru_meta(const UnitCell& ucell) namespace mdcell { -void print_stru_file(const MDCell& cell, const StruMeta& stru_meta, const std::string& fn) +void print_stru_file(const MDCell& cell, const StruMeta& stru_meta, const std::string& fn, + const std::string& frame_comment, const bool append) { if (stru_meta.species.size() != cell.type_labels().size() || stru_meta.species.size() != cell.type_masses().size() @@ -376,22 +383,29 @@ void print_stru_file(const MDCell& cell, const StruMeta& stru_meta, const std::s { throw std::runtime_error("MDCell STRU metadata does not match the MDCell type data."); } - const std::string header = mdcell_stru_header(cell, stru_meta); + const std::string header = frame_comment + mdcell_stru_header(cell, stru_meta); #ifdef __MPI int rank = 0; const MPI_Comm comm = cell.communicator(); MPI_Comm_rank(comm, &rank); int header_ok = 1; + MPI_Offset base_offset = 0; if (rank == 0) { - const int header_file = open(fn.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0666); + // No O_APPEND: on Linux pwrite() ignores the given offset when the + // file was opened with O_APPEND. Truncate unless appending, then + // seek to the current end to learn the base offset. + const int open_flags = O_CREAT | O_WRONLY | (append ? 0 : O_TRUNC); + const int header_file = open(fn.c_str(), open_flags, 0666); if (header_file < 0) { header_ok = 0; } else { - const bool header_written = write_at(header_file, header, 0); + const off_t end_pos = lseek(header_file, 0, SEEK_END); + base_offset = end_pos >= 0 ? static_cast(end_pos) : 0; + const bool header_written = end_pos >= 0 && write_at(header_file, header, base_offset); const bool header_closed = close(header_file) == 0; header_ok = header_written && header_closed; } @@ -413,7 +427,13 @@ void print_stru_file(const MDCell& cell, const StruMeta& stru_meta, const std::s throw std::runtime_error("Unable to open MDCell restart STRU file: " + fn + ": " + std::strerror(errno)); } - MPI_Offset offset = static_cast(header.size()); + // Every rank learns the write position from the file itself instead of + // broadcasting the MPI_Offset held by rank 0: after the barrier the header + // is in place, so the end of the file is the current base offset plus the + // header that was just written (zero for a truncated fresh file). + const off_t file_end = lseek(file, 0, SEEK_END); + MPI_Offset offset = file_end >= 0 ? static_cast(file_end) + : static_cast(header.size()); for (std::size_t it = 0; it < stru_meta.species.size(); ++it) { const std::string type_header = mdcell_type_header(cell, stru_meta, it); @@ -453,7 +473,12 @@ void print_stru_file(const MDCell& cell, const StruMeta& stru_meta, const std::s throw std::runtime_error("Unable to close MDCell restart STRU file: " + fn + ": " + std::strerror(errno)); } #else - std::ofstream output(fn.c_str()); + std::ios_base::openmode mode = std::ios::out; + if (append) + { + mode |= std::ios::app; + } + std::ofstream output(fn.c_str(), mode); output << header; for (std::size_t it = 0; it < stru_meta.species.size(); ++it) output << mdcell_type_header(cell, stru_meta, it) << local_mdcell_atoms(cell, it); diff --git a/source/source_cell/print_cell.h b/source/source_cell/print_cell.h index 1370629b0cd..ab39ddcf0dc 100644 --- a/source/source_cell/print_cell.h +++ b/source/source_cell/print_cell.h @@ -51,6 +51,8 @@ namespace unitcell * @param iproc GlobalV::MY_RANK [in] * @param force atomic forces in Ry/Bohr, used only when has_force is true [in] * @param has_force true to emit per-atom f fields; force matrix must match nat [in] + * @param append true to append the structure block to an existing file + * (out_app_flag == 1 multi-step runs); false truncates [in] */ void print_stru_file(const UnitCell& ucell, const Atom* atoms, @@ -64,7 +66,8 @@ namespace unitcell const bool& dpks_desc, const int& iproc, const ModuleBase::matrix& force, - const bool& has_force); + const bool& has_force, + const bool append); /** * @brief Print basic unitcell information to output stream. @@ -79,9 +82,22 @@ namespace unitcell namespace mdcell { +/** + * @brief Write an MDCell structure file in STRU format. + * + * @param mdcell distributed MD cell data [in] + * @param stru_meta species/orbital metadata used for the header [in] + * @param fn output file path [in] + * @param frame_comment comment block prepended before the structure header + * (e.g. "# MD step N\n\n"); pass an empty string for no marker [in] + * @param append true to append the frame to an existing file + * (out_app_flag == 1 multi-step runs); false truncates [in] + */ void print_stru_file(const MDCell& mdcell, const StruMeta& stru_meta, - const std::string& fn); + const std::string& fn, + const std::string& frame_comment, + const bool append); } #endif diff --git a/source/source_cell/stru_frame_io.cpp b/source/source_cell/stru_frame_io.cpp new file mode 100644 index 00000000000..db6d7b474ab --- /dev/null +++ b/source/source_cell/stru_frame_io.cpp @@ -0,0 +1,105 @@ +/** + * @file stru_frame_io.cpp + * @brief Implementation of the cumulative STRU frame extraction helpers. + */ +#include "stru_frame_io.h" + +#include +#include +#include + +namespace +{ +const char* const frame_marker = "# MD step "; + +/// Parse the step number following a "# MD step " marker; 0 when malformed. +int parse_marker_step(const std::string& line) +{ + const std::string number = line.substr(std::string(frame_marker).size()); + if (number.empty()) + { + return 0; + } + char* end = nullptr; + const long value = std::strtol(number.c_str(), &end, 10); + if (end == number.c_str() || value <= 0) + { + return 0; + } + return static_cast(value); +} +} // namespace + +namespace stru_frame_io +{ +bool extract_last_frame(const std::string& cumulative_path, const std::string& frame_path) +{ + std::ifstream ifs(cumulative_path.c_str()); + if (!ifs) + { + return false; + } + std::string line; + std::string last_frame; + std::string current_frame; + bool in_frame = false; + while (std::getline(ifs, line)) + { + const bool is_marker = line.compare(0, std::string(frame_marker).size(), frame_marker) == 0; + if (is_marker) + { + if (in_frame) + { + last_frame = current_frame; + } + current_frame.clear(); + in_frame = true; + continue; + } + if (in_frame) + { + current_frame += line; + current_frame += '\n'; + } + } + if (in_frame) + { + last_frame = current_frame; + } + if (last_frame.empty()) + { + return false; + } + std::ofstream ofs(frame_path.c_str(), std::ios::out | std::ios::trunc); + if (!ofs) + { + return false; + } + ofs << last_frame; + return ofs.good(); +} + +int last_frame_step(const std::string& cumulative_path) +{ + std::ifstream ifs(cumulative_path.c_str()); + if (!ifs) + { + return 0; + } + int last_step = 0; + std::string line; + while (std::getline(ifs, line)) + { + const bool is_marker = line.compare(0, std::string(frame_marker).size(), frame_marker) == 0; + if (is_marker) + { + const int step = parse_marker_step(line); + if (step > 0) + { + last_step = step; + } + } + } + return last_step; +} +} // namespace stru_frame_io diff --git a/source/source_cell/stru_frame_io.h b/source/source_cell/stru_frame_io.h new file mode 100644 index 00000000000..fff0067c98f --- /dev/null +++ b/source/source_cell/stru_frame_io.h @@ -0,0 +1,37 @@ +#ifndef STRU_FRAME_IO_H +#define STRU_FRAME_IO_H + +#include + +/** + * @file stru_frame_io.h + * @brief Helpers to read frames from fixed-name cumulative STRU files. + * + * In out_app_flag == 1 mode run_md appends one structure frame per output + * step to a single "STRU" file, each frame introduced by a one-based + * "# MD step N" comment line. Restart readers such as MDCellReader::read_stru + * expect a single-frame STRU file, so the frames written this way must be + * split before they can be consumed. This header owns that frame-splitting + * knowledge so callers outside source_cell do not parse STRU text themselves. + */ +namespace stru_frame_io +{ +/// @brief Extract the last MD frame from a fixed-name cumulative STRU file. +/// @param cumulative_path path of the mode-1 "STRU" file holding one frame per +/// output step, each introduced by a "# MD step N" comment line +/// @param frame_path path of the single-frame file to (over)write with the +/// last frame; the caller passes this path to the structure readers +/// @return true when at least one frame was found and written +/// @details The last frame is copied verbatim, without its "# MD step N" +/// comment line, into frame_path. The side file is rewritten on every call so +/// repeated restarts always see the most recent frame. +bool extract_last_frame(const std::string& cumulative_path, const std::string& frame_path); + +/// @brief Read the one-based step number of the last frame of a cumulative STRU. +/// @param cumulative_path path of the mode-1 "STRU" file +/// @return the step number carried by the last "# MD step N" marker, or 0 when +/// the file is missing or carries no frame marker +int last_frame_step(const std::string& cumulative_path); +} // namespace stru_frame_io + +#endif // STRU_FRAME_IO_H diff --git a/source/source_cell/test/CMakeLists.txt b/source/source_cell/test/CMakeLists.txt index 77632f4f5f0..b64022f7c14 100644 --- a/source/source_cell/test/CMakeLists.txt +++ b/source/source_cell/test/CMakeLists.txt @@ -58,6 +58,16 @@ addtest( test_cell_geometry.cpp ../cell_geometry.cpp) +addtest( + TARGET + MODULE_CELL_stru_frame_io + LIBS + base + device + SOURCES + test_stru_frame_io.cpp + ../stru_frame_io.cpp) + if(NOT ENABLE_MPI) return() endif() diff --git a/source/source_cell/test/cell_index_test.cpp b/source/source_cell/test/cell_index_test.cpp index e0767ad6aaf..06a932c92d7 100644 --- a/source/source_cell/test/cell_index_test.cpp +++ b/source/source_cell/test/cell_index_test.cpp @@ -56,11 +56,11 @@ TEST_F(CellIndexTest, Index) TEST_F(CellIndexTest, WriteOrbInfo) { cell_index.write_orb_info("./"); - std::ifstream ifs("./Orbital"); + std::ifstream ifs("./nao.txt"); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); EXPECT_THAT(str, testing::HasSubstr("#io spec l m z sym")); EXPECT_THAT(str, testing::HasSubstr("0 C 2 4 1 dxy")); EXPECT_THAT(str, testing::HasSubstr("1 H 2 4 1 dxy")); EXPECT_THAT(str, testing::HasSubstr("2 H 2 4 1 dxy")); - remove("./Orbital"); + remove("./nao.txt"); } \ No newline at end of file diff --git a/source/source_cell/test/read_atoms_hlp_test.cpp b/source/source_cell/test/read_atoms_hlp_test.cpp index c536fa4eabf..194a9f67fc3 100644 --- a/source/source_cell/test/read_atoms_hlp_test.cpp +++ b/source/source_cell/test/read_atoms_hlp_test.cpp @@ -684,7 +684,7 @@ TEST_F(ReadAtomsHelperTest, RoundTripMultiAtomMixedFields) const std::string filename = "test_stru_roundtrip.tmp"; unitcell::print_stru_file(ucell, ucell.atoms, ucell.latvec, filename, "", - 2, false, false, false, false, 0, force, true); + 2, false, false, false, false, 0, force, true, false); std::ifstream ifpos(filename.c_str()); diff --git a/source/source_cell/test/test_print_cell.cpp b/source/source_cell/test/test_print_cell.cpp index b1d131ffcb4..c4ff6e08ae7 100644 --- a/source/source_cell/test/test_print_cell.cpp +++ b/source/source_cell/test/test_print_cell.cpp @@ -48,7 +48,7 @@ TEST_F(PrintCellTest, PrintSTRU_nspin1) std::string fn = "C1H2_STRU_nspin1"; unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 1, false, false, false, false, 0, ModuleBase::matrix(), false); + fn, "", 1, false, false, false, false, 0, ModuleBase::matrix(), false, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -91,7 +91,7 @@ TEST_F(PrintCellTest, PrintSTRU_zero_force_computed) force.zero_out(); unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 2, false, false, false, false, 0, force, true); + fn, "", 2, false, false, false, false, 0, force, true, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -114,7 +114,7 @@ TEST_F(PrintCellTest, PrintSTRU_force_matrix_without_flag) force(0, 0) = 0.1; force(0, 1) = 0.2; force(0, 2) = 0.3; unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 2, false, false, false, false, 0, force, false); + fn, "", 2, false, false, false, false, 0, force, false, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -131,7 +131,7 @@ TEST_F(PrintCellTest, PrintSTRU_nspin2_no_force) std::string fn = "C1H2_STRU_nspin2"; unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 2, true, false, false, false, 0, ModuleBase::matrix(), false); + fn, "", 2, true, false, false, false, 0, ModuleBase::matrix(), false, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -163,7 +163,7 @@ TEST_F(PrintCellTest, PrintSTRU_nspin2_with_mag) ucell->atoms[1].mag[0] = -0.5; // H1 ucell->atoms[1].mag[1] = 2.0; // H2 unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 2, false, false, false, false, 0, ModuleBase::matrix(), false); + fn, "", 2, false, false, false, false, 0, ModuleBase::matrix(), false, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -192,7 +192,7 @@ TEST_F(PrintCellTest, PrintSTRU_nspin2_mulliken) ucell->orbital_fn[1] = "__unittest_orbital_fn_1__"; ucell->atom_mulliken = {{-1, 0.5}, {-1, 0.4}, {-1, 0.3}}; unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 2, false, true, true, true, 0, ModuleBase::matrix(), false); + fn, "", 2, false, true, true, true, 0, ModuleBase::matrix(), false, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -221,7 +221,7 @@ TEST_F(PrintCellTest, PrintSTRU_nspin4_initial_mag) ucell->atoms[1].m_loc_[0].set(0.0, 1.0, 0.0); // H1 ucell->atoms[1].m_loc_[1].set(0.0, 0.0, 1.0); // H2 unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 4, false, false, false, false, 0, ModuleBase::matrix(), false); + fn, "", 4, false, false, false, false, 0, ModuleBase::matrix(), false, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -247,7 +247,7 @@ TEST_F(PrintCellTest, PrintSTRU_nspin4_mulliken) ucell->atom_mulliken = {{-1, 0.5, 0.1, 0.2}, {-1, 0.4, 0.3, 0.4}, {-1, 0.3, 0.5, 0.6}}; unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 4, false, true, false, false, 0, ModuleBase::matrix(), false); + fn, "", 4, false, true, false, false, 0, ModuleBase::matrix(), false, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -274,7 +274,7 @@ TEST_F(PrintCellTest, PrintSTRU_with_force) force(2, 0) = 0.05; force(2, 1) = 0.15; force(2, 2) = -0.25; // H2 unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, - fn, "", 2, false, false, false, false, 0, force, true); + fn, "", 2, false, false, false, false, 0, force, true, false); std::ifstream ifs; ifs.open(fn); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -390,3 +390,38 @@ TEST_F(PrintCellTest, PrintTauCartesian) // remove the file remove("print_tau_Cartesian"); } + +// --------------------------------------------------------------------------- +// STRU append flag: append=true accumulates successive structures in one file. +// --------------------------------------------------------------------------- +TEST_F(PrintCellTest, STRUAppend) +{ + UcellTestPrepare utp = UcellTestLib["C1H2-Index"]; + ucell = utp.SetUcellInfo(); + const std::string fn = "STRU_append_test"; + + // First call: append=false -> create/truncate. + unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, + fn, "", 1, false, false, false, false, 0, ModuleBase::matrix(), false, + /*append=*/false); + // Second call: append=true -> accumulate into same file. + unitcell::print_stru_file(*ucell, ucell->atoms, ucell->latvec, + fn, "", 1, false, false, false, false, 0, ModuleBase::matrix(), false, + /*append=*/true); + + std::ifstream ifs(fn); + std::string str((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + // Two STRU blocks -> two ATOMIC_SPECIES markers. + int occurrences = 0; + size_t pos = 0; + while ((pos = str.find("ATOMIC_SPECIES", pos)) != std::string::npos) + { + ++occurrences; + pos += 14; + } + EXPECT_EQ(occurrences, 2); + + remove(fn.c_str()); +} diff --git a/source/source_cell/test/test_stru_frame_io.cpp b/source/source_cell/test/test_stru_frame_io.cpp new file mode 100644 index 00000000000..2f280c1196d --- /dev/null +++ b/source/source_cell/test/test_stru_frame_io.cpp @@ -0,0 +1,112 @@ +/** + * @file test_stru_frame_io.cpp + * @brief Unit test for stru_frame_io: last-frame extraction from a fixed-name + * cumulative STRU file written in out_app_flag == 1 mode. + */ +#include "source_cell/stru_frame_io.h" + +#include +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "source_base/fs_compat.h" + +class StruFrameIoTest : public testing::Test +{ + protected: + std::string case_dir; + + void SetUp() override + { + case_dir = testing::TempDir() + "/abacus_stru_frame_io/"; + ModuleBase::make_directory(case_dir); + } + + std::string read_all(const std::string& path) + { + std::ifstream ifs(path.c_str()); + std::stringstream content; + content << ifs.rdbuf(); + return content.str(); + } +}; + +TEST_F(StruFrameIoTest, ExtractLastFramePicksNewest) +{ + const std::string cumulative = case_dir + "STRU"; + std::ofstream ofs(cumulative.c_str()); + ofs << "# MD step 1\n\nATOMIC_SPECIES\nSi 28.0855\n"; + ofs << "# MD step 2\n\nATOMIC_SPECIES\nSi 28.0855\n\nLATTICE_CONSTANT\n10.2\n"; + ofs.close(); + + const std::string frame = case_dir + "STRU_restart_frame"; + EXPECT_TRUE(stru_frame_io::extract_last_frame(cumulative, frame)); + + const std::string content = read_all(frame); + // the extracted file must hold only the last frame, not the earlier one + EXPECT_EQ(content.find("# MD step"), std::string::npos); + EXPECT_NE(content.find("LATTICE_CONSTANT"), std::string::npos); +} + +TEST_F(StruFrameIoTest, ExtractLastFrameRewritesOnSecondCall) +{ + const std::string cumulative = case_dir + "STRU"; + std::ofstream ofs(cumulative.c_str()); + ofs << "# MD step 1\n\nATOMIC_SPECIES\nSi 28.0855\n"; + ofs.close(); + + const std::string frame = case_dir + "STRU_restart_frame"; + EXPECT_TRUE(stru_frame_io::extract_last_frame(cumulative, frame)); + + // simulate a later restart: append another frame and re-extract + std::ofstream app(cumulative.c_str(), std::ios::app); + app << "# MD step 2\n\nLATTICE_CONSTANT\n10.9\n"; + app.close(); + EXPECT_TRUE(stru_frame_io::extract_last_frame(cumulative, frame)); + + const std::string content = read_all(frame); + EXPECT_NE(content.find("10.9"), std::string::npos); +} + +TEST_F(StruFrameIoTest, ExtractLastFrameFailsOnMissingFile) +{ + const std::string missing = case_dir + "NO_SUCH_STRU"; + const std::string frame = case_dir + "STRU_restart_frame"; + EXPECT_FALSE(stru_frame_io::extract_last_frame(missing, frame)); +} + +TEST_F(StruFrameIoTest, ExtractLastFrameFailsWithoutMarker) +{ + const std::string cumulative = case_dir + "STRU"; + std::ofstream ofs(cumulative.c_str()); + ofs << "ATOMIC_SPECIES\nSi 28.0855\n"; + ofs.close(); + + const std::string frame = case_dir + "STRU_restart_frame"; + EXPECT_FALSE(stru_frame_io::extract_last_frame(cumulative, frame)); +} + +TEST_F(StruFrameIoTest, LastFrameStepReportsNewestMarker) +{ + const std::string cumulative = case_dir + "STRU"; + std::ofstream ofs(cumulative.c_str()); + ofs << "# MD step 1\n\nATOMIC_SPECIES\nSi 28.0855\n"; + ofs << "# MD step 3\n\nLATTICE_CONSTANT\n10.2\n"; + ofs.close(); + + EXPECT_EQ(stru_frame_io::last_frame_step(cumulative), 3); +} + +TEST_F(StruFrameIoTest, LastFrameStepIsZeroOnMissingOrUnmarkedFile) +{ + EXPECT_EQ(stru_frame_io::last_frame_step(case_dir + "NO_SUCH_STRU"), 0); + + const std::string unmarked = case_dir + "STRU_no_marker"; + std::ofstream ofs(unmarked.c_str()); + ofs << "ATOMIC_SPECIES\nSi 28.0855\n"; + ofs.close(); + + EXPECT_EQ(stru_frame_io::last_frame_step(unmarked), 0); +} diff --git a/source/source_esolver/esolver_dm2rho.cpp b/source/source_esolver/esolver_dm2rho.cpp index 77b7825cc34..6c5dc31302a 100644 --- a/source/source_esolver/esolver_dm2rho.cpp +++ b/source/source_esolver/esolver_dm2rho.cpp @@ -99,6 +99,7 @@ void ESolver_DM2rho::runner(BaseCell& basecell, const int istep) 1, PARAM.globalv.two_fermi, false, + false, GlobalV::ofs_running, desc_chg); } diff --git a/source/source_esolver/esolver_gets.cpp b/source/source_esolver/esolver_gets.cpp index d0a2d727587..2a4d410ef63 100644 --- a/source/source_esolver/esolver_gets.cpp +++ b/source/source_esolver/esolver_gets.cpp @@ -1,5 +1,6 @@ #include "esolver_gets.h" +#include "source_base/fs_compat.h" #include "source_base/timer.h" #include "source_cell/module_neighbor/sltk_atom_arrange.h" #include "source_cell/read_pp_ucell.h" @@ -196,6 +197,7 @@ void ESolver_GetS::runner(BaseCell& basecell, const int istep) } const std::string fn = PARAM.globalv.global_out_dir + "sr_nao.csr"; + const std::string global_matrix_dir = PARAM.globalv.global_matrix_dir; auto* hamilt_ptr = static_cast>*>(this->p_hamilt); const bool binary = false; @@ -209,7 +211,7 @@ void ESolver_GetS::runner(BaseCell& basecell, const int istep) sparse_threshold, precision, PARAM.globalv.global_out_dir, - PARAM.globalv.global_matrix_dir, + global_matrix_dir, PARAM.inp.calculation, PARAM.inp.out_app_flag, PARAM.inp.nspin, @@ -217,6 +219,8 @@ void ESolver_GetS::runner(BaseCell& basecell, const int istep) if (this->inp_->out_mat_r[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_matrix_dir); Position_op r_matrix; const bool cal_force = PARAM.inp.cal_force; const int nlocal = PARAM.globalv.nlocal; @@ -226,7 +230,7 @@ void ESolver_GetS::runner(BaseCell& basecell, const int istep) -1, // get_s has no ionic step; use -1 for no step suffix this->inp_->out_mat_r[1], PARAM.globalv.global_out_dir, - PARAM.globalv.global_matrix_dir, + global_matrix_dir, PARAM.inp.calculation, PARAM.inp.out_app_flag, nlocal, @@ -236,13 +240,15 @@ void ESolver_GetS::runner(BaseCell& basecell, const int istep) if (this->inp_->out_mat_ds[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_matrix_dir); LCAO_HS_Arrays HS_Arrays; // store sparse arrays ModuleIO::MatROutputOptions mat_R_options; mat_R_options.binary = binary; mat_R_options.sparse_threshold = sparse_threshold; mat_R_options.precision = this->inp_->out_mat_ds[1]; mat_R_options.global_out_dir = PARAM.globalv.global_out_dir; - mat_R_options.global_matrix_dir = PARAM.globalv.global_matrix_dir; + mat_R_options.global_matrix_dir = global_matrix_dir; mat_R_options.calculation = PARAM.inp.calculation; mat_R_options.out_app_flag = PARAM.inp.out_app_flag; mat_R_options.nspin = PARAM.inp.nspin; diff --git a/source/source_esolver/esolver_ks.cpp b/source/source_esolver/esolver_ks.cpp index dcb2a3c61f6..8c80c068116 100644 --- a/source/source_esolver/esolver_ks.cpp +++ b/source/source_esolver/esolver_ks.cpp @@ -496,7 +496,7 @@ void ESolver_KS::after_scf(UnitCell& ucell, const int istep, const bool conv_eso istep); // 4) write band information to band.txt - ModuleIO::write_bands(*this->inp_, this->pelec->ekb, this->kv, GlobalV::ofs_running); + ModuleIO::write_bands(*this->inp_, this->pelec->ekb, this->kv, GlobalV::ofs_running, istep); } diff --git a/source/source_esolver/esolver_ks_lcao.cpp b/source/source_esolver/esolver_ks_lcao.cpp index f476cf78d90..ae1c99417b8 100644 --- a/source/source_esolver/esolver_ks_lcao.cpp +++ b/source/source_esolver/esolver_ks_lcao.cpp @@ -174,6 +174,9 @@ void ESolver_KS_LCAO::before_scf(UnitCell& ucell, const int istep, const ModuleBase::TITLE("ESolver_KS_LCAO", "before_scf"); ModuleBase::timer::start("ESolver_KS_LCAO", "before_scf"); + // Record the ionic step (not output_step) for provenance headers written in after_all_runners. + this->istep_ = istep; + //! 1) call before_scf() of ESolver_KS. // The base preparation uses this index only for initial charge output. ESolver_KS::before_scf(ucell, output_step); @@ -443,6 +446,7 @@ void ESolver_KS_LCAO::after_all_runners(BaseCell& basecell) ModuleIO::ctrl_runner_lcao(ucell, *this->inp_, + this->istep_, this->kv, this->pelec, this->dmat, diff --git a/source/source_esolver/esolver_ks_lcao.h b/source/source_esolver/esolver_ks_lcao.h index 6fca9c417ec..093625c3210 100644 --- a/source/source_esolver/esolver_ks_lcao.h +++ b/source/source_esolver/esolver_ks_lcao.h @@ -60,6 +60,11 @@ class ESolver_KS_LCAO : public ESolver_KS //! Prepare an ionic step with a separate index for initial charge and potential output. void before_scf(UnitCell& ucell, const int istep, const int output_step); + /// Ionic step index of the most recent ionic step, recorded in before_scf(). + /// Read by after_all_runners() when labelling provenance headers of files + /// written once at the end of a run. + int istep_ = 0; + //! Prepare charge mixing and Hamiltonian updates for an SCF iteration. virtual void iter_init(UnitCell& ucell, const int istep, const int iter) override; diff --git a/source/source_esolver/esolver_ks_lcaopw.cpp b/source/source_esolver/esolver_ks_lcaopw.cpp index 5c6ba6e9549..90fe4ab537d 100644 --- a/source/source_esolver/esolver_ks_lcaopw.cpp +++ b/source/source_esolver/esolver_ks_lcaopw.cpp @@ -13,6 +13,7 @@ #include "source_pw/module_pwdft/stress_pw.h" //--------------------------------------------------- #include "source_base/global_variable.h" +#include "source_base/fs_compat.h" #include "source_base/parallel_comm.h" #include "source_estate/elecstate_pw.h" #include "source_pw/module_pwdft/hamilt_lcaopw.h" @@ -277,6 +278,9 @@ namespace ModuleESolver #ifdef __LCAO if (this->inp_->out_mat_xc) { + const std::string global_mat_dir = PARAM.globalv.global_mat_dir; + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); #ifdef __EXX bool cal_exx = this->general_exx_info_.cal_exx; double hybrid_alpha = this->general_exx_info_.hybrid_alpha; @@ -284,7 +288,12 @@ namespace ModuleESolver bool cal_exx = false; double hybrid_alpha = 0.0; #endif + // one-shot output after the whole run; the single frame is step 1 + const int istep_xc = 0; + const double etot_xc = this->pelec->f_en.etot; ModuleIO::write_Vxc_LIP(this->inp_->nspin, + istep_xc, + etot_xc, PARAM.globalv.nlocal, GlobalV::DRANK, *this->stp.template get_psi_t(), @@ -299,7 +308,7 @@ namespace ModuleESolver this->kv, this->pelec->wg, PARAM.globalv.gamma_only_local, - PARAM.globalv.global_out_dir, + global_mat_dir, this->inp_->out_ndigits, this->inp_->ks_solver, cal_exx, diff --git a/source/source_estate/write_elecstat_pot.cpp b/source/source_estate/write_elecstat_pot.cpp index f530c3d3d0c..dc0d968359f 100644 --- a/source/source_estate/write_elecstat_pot.cpp +++ b/source/source_estate/write_elecstat_pot.cpp @@ -25,6 +25,7 @@ void write_elecstat_pot( const std::vector* pcc_potential, const std::vector* solvent_potential, const int precision, + const bool append, std::ofstream& ofs_running) { ModuleBase::TITLE("ModuleIO", "write_elecstat_pot"); @@ -122,6 +123,7 @@ void write_elecstat_pot( out_fermi, PARAM.globalv.two_fermi, false, + append, ofs_running, "electrostatic potential"); diff --git a/source/source_estate/write_elecstat_pot.h b/source/source_estate/write_elecstat_pot.h index 15c7dc28662..8bb53d243b2 100644 --- a/source/source_estate/write_elecstat_pot.h +++ b/source/source_estate/write_elecstat_pot.h @@ -21,7 +21,8 @@ namespace ModuleIO /// @param ucell_ /// @param v_eff_fixed /// @param solvent: for solvation model -/// #param precision: output precision +/// @param precision: output precision +/// @param append: append a frame to an existing cube file (out_app_flag == 1) void write_elecstat_pot( #ifdef __MPI const int& bz, @@ -37,6 +38,7 @@ void write_elecstat_pot( const std::vector* pcc_potential, const std::vector* solvent_potential, const int precision, + const bool append, std::ofstream& ofs_running); } // namespace ModuleIO diff --git a/source/source_estate/write_init.cpp b/source/source_estate/write_init.cpp index 6b083c1986a..a1057b1bfc0 100644 --- a/source/source_estate/write_init.cpp +++ b/source/source_estate/write_init.cpp @@ -6,6 +6,7 @@ // - Frequency is out_freq_td for RT-TDDFT and out_freq_ion otherwise. // - Frequency = 0: chg_ini.cube/pot_ini.cube, chgs1_ini.cube/chgs2_ini.cube // - Frequency > 0: chgg{#}_ini.cube/potg{#}_ini.cube, chgs{#}g{#}_ini.cube +// (modes 0/1 keep the fixed names without the g{#} index; only mode 2 numbers them) // - File indices start from 1 (istep + 1); RT-TDDFT uses electronic steps. // // Usage: @@ -82,7 +83,11 @@ void ModuleIO::write_chg_init(const UnitCell& ucell, if (should_output) { - const bool include_geom_step = (output_frequency > 0); + // Modes 0 and 1 reuse the same fixed filename (mode 0 overwrites, + // mode 1 accumulates one complete cube frame per written step); + // only mode 2 embeds the geometry-step index. + const bool include_geom_step = (output_frequency > 0) && (inp.out_app_flag == 2); + const bool append = (inp.out_app_flag == 1) && (istep > 0); for (int is = 0; is < nspin; is++) { @@ -119,6 +124,7 @@ void ModuleIO::write_chg_init(const UnitCell& ucell, 1, two_fermi, false, + append, ofs_running, desc_chg); } @@ -151,7 +157,11 @@ void ModuleIO::write_pot_init(const UnitCell& ucell, if (should_output) { - const bool include_geom_step = (output_frequency > 0); + // Modes 0 and 1 reuse the same fixed filename (mode 0 overwrites, + // mode 1 accumulates one complete cube frame per written step); + // only mode 2 embeds the geometry-step index. + const bool include_geom_step = (output_frequency > 0) && (inp.out_app_flag == 2); + const bool append = (inp.out_app_flag == 1) && (istep > 0); for (int is = 0; is < nspin; is++) { @@ -171,6 +181,7 @@ void ModuleIO::write_pot_init(const UnitCell& ucell, 0, two_fermi, false, + append, ofs_running, desc_pot); } diff --git a/source/source_io/CMakeLists.txt b/source/source_io/CMakeLists.txt index e16e5b31e1c..2719b147f23 100644 --- a/source/source_io/CMakeLists.txt +++ b/source/source_io/CMakeLists.txt @@ -157,6 +157,7 @@ add_executable( EXCLUDE_FROM_ALL input_yaml_main.cpp input_help.cpp + ../source_cell/stru_frame_io.cpp ../source_base/global_file.cpp ../source_base/global_function.cpp ../source_base/global_variable.cpp diff --git a/source/source_io/module_chgpot/get_pchg_lcao.cpp b/source/source_io/module_chgpot/get_pchg_lcao.cpp index 3ececff4b1f..9add37b9758 100644 --- a/source/source_io/module_chgpot/get_pchg_lcao.cpp +++ b/source/source_io/module_chgpot/get_pchg_lcao.cpp @@ -80,7 +80,7 @@ void Get_pchg_lcao::begin_gamma(const UnitCell& ucell, const std::string desc_pchg = ModuleIO::make_data_desc("partial charge", "partial magnetization m", is, nspin_); - ModuleIO::write_vdata_palgrid(pgrid, rho[is].data(), is, nspin_, 0, ssc.str(), 0.0, &ucell, precision, 0, false, false, ofs_running, desc_pchg); + ModuleIO::write_vdata_palgrid(pgrid, rho[is].data(), is, nspin_, 0, ssc.str(), 0.0, &ucell, precision, 0, false, false, false, ofs_running, desc_pchg); } } } @@ -172,7 +172,7 @@ void Get_pchg_lcao::begin_k(const ModulePW::PW_Basis& rho_pw, const std::string desc_pchg = ModuleIO::make_data_desc("partial charge", "partial magnetization m", is, nspin_); - ModuleIO::write_vdata_palgrid(pgrid, rho[is].data(), is, nspin_, 0, ssc.str(), 0.0, &ucell, precision, 0, false, false, ofs_running, desc_pchg); + ModuleIO::write_vdata_palgrid(pgrid, rho[is].data(), is, nspin_, 0, ssc.str(), 0.0, &ucell, precision, 0, false, false, false, ofs_running, desc_pchg); } } } @@ -212,7 +212,7 @@ void Get_pchg_lcao::begin_k(const ModulePW::PW_Basis& rho_pw, const std::string desc_pchg = ModuleIO::make_data_desc("partial charge", "partial magnetization m", is, nspin_); - ModuleIO::write_vdata_palgrid(pgrid, rho[is].data(), is, nspin_, 0, ssc.str(), 0.0, &ucell, precision, 0, false, false, ofs_running, desc_pchg); + ModuleIO::write_vdata_palgrid(pgrid, rho[is].data(), is, nspin_, 0, ssc.str(), 0.0, &ucell, precision, 0, false, false, false, ofs_running, desc_pchg); } } } diff --git a/source/source_io/module_chgpot/get_pchg_pw.cpp b/source/source_io/module_chgpot/get_pchg_pw.cpp index 10561069885..554ef4caf8e 100644 --- a/source/source_io/module_chgpot/get_pchg_pw.cpp +++ b/source/source_io/module_chgpot/get_pchg_pw.cpp @@ -474,7 +474,7 @@ void Get_pchg_pw::write_cube(const int band, } filename << ".cube"; const std::string desc_pchg = ModuleIO::make_data_desc("partial charge", "partial magnetization m", component, nspin_); - ModuleIO::write_vdata_palgrid(pgrid, values.data(), component, nspin_, 0, filename.str(), 0.0, &ucell, 11, 0, false, separate_k, ofs_running, desc_pchg); + ModuleIO::write_vdata_palgrid(pgrid, values.data(), component, nspin_, 0, filename.str(), 0.0, &ucell, 11, 0, false, separate_k, false, ofs_running, desc_pchg); } // Explicit instantiation emits both precisions for each supported device from this .cpp file. diff --git a/source/source_io/module_ctrl/ctrl_output_fp.cpp b/source/source_io/module_ctrl/ctrl_output_fp.cpp index 57bed1d3a12..24da68b8eb6 100644 --- a/source/source_io/module_ctrl/ctrl_output_fp.cpp +++ b/source/source_io/module_ctrl/ctrl_output_fp.cpp @@ -1,5 +1,6 @@ #include "ctrl_output_fp.h" // use ctrl_output_fp() +#include "source_base/fs_compat.h" // use ModuleBase::ensure_directory #include "source_basis/module_pw/pw_basis_big.h" // use PW_Basis_Big #include "source_estate/elecstate.h" // use elecstate::ElecState #include "source_estate/module_charge/charge.h" // use Charge @@ -33,10 +34,14 @@ void ctrl_output_fp(UnitCell& ucell, ModuleBase::TITLE("ModuleIO", "ctrl_output_fp"); ModuleBase::timer::start("ModuleIO", "ctrl_output_fp"); - const bool out_app_flag = inp.out_app_flag; + const int out_app_flag = inp.out_app_flag; const bool gamma_only = PARAM.globalv.gamma_only_local; const int nspin = inp.nspin; const std::string global_out_dir = PARAM.globalv.global_out_dir; + const std::string global_chg_dir = PARAM.globalv.global_chg_dir; + const std::string global_pot_dir = PARAM.globalv.global_pot_dir; + const std::string global_elf_dir = PARAM.globalv.global_elf_dir; + const std::string global_dip_dir = PARAM.globalv.global_dip_dir; const int output_frequency = inp.esolver_type == "tddft" ? inp.out_freq_td : inp.out_freq_ion; // Print the global electronic step for TDDFT and the ionic step otherwise. @@ -53,16 +58,32 @@ void ctrl_output_fp(UnitCell& ucell, bool should_output = (output_frequency == 0); if (istep_in >= 0) { - geom_block = "g" + std::to_string(istep + 1); + // Modes 0 and 1 reuse a fixed filename: mode 0 truncates and overwrites + // it at every write, while mode 1 appends one complete frame per written + // step. Only mode 2 keeps a per-step g{index} suffix, which makes every + // file hold exactly one frame. + if (out_app_flag == 2) + { + geom_block = "g" + std::to_string(istep + 1); + } should_output = true; } + // Mirror the out_wfc_lcao convention: in append mode the first ionic step + // (istep == 0, always written because every out_freq_ion divides it) + // truncates the fixed-name cube files and later steps append one complete + // frame each. Single-point SCF/NSCF runs stop at istep == 0, so they keep + // overwriting instead of stacking on a stale file from an earlier run. + const bool append_cube = (out_app_flag == 1) && (istep > 0); + // 4) write charge density if (inp.out_chg[0] > 0 && should_output) { + // Mode-2 CHG/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_chg_dir); for (int is = 0; is < nspin; ++is) { - std::string fn = PARAM.globalv.global_out_dir + "chg"; + std::string fn = global_chg_dir + "chg"; std::string spin_block; if (nspin == 2 || nspin == 4) @@ -90,12 +111,13 @@ void ctrl_output_fp(UnitCell& ucell, 1, PARAM.globalv.two_fermi, false, + append_cube, ofs_running, desc_chg); if (XC_Functional::get_ked_flag()) { - fn = PARAM.globalv.global_out_dir + "tau"; + fn = global_chg_dir + "tau"; fn += spin_block + geom_block + ".cube"; @@ -113,6 +135,7 @@ void ctrl_output_fp(UnitCell& ucell, 1, // default out_fermi PARAM.globalv.two_fermi, false, + append_cube, ofs_running, desc_tau); } @@ -122,9 +145,11 @@ void ctrl_output_fp(UnitCell& ucell, // 5) write potential if ((inp.out_pot[0] == 1 || inp.out_pot[0] == 3) && should_output) { + // Mode-2 POT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_pot_dir); for (int is = 0; is < nspin; is++) { - std::string fn = PARAM.globalv.global_out_dir + "pot"; + std::string fn = global_pot_dir + "pot"; std::string spin_block; if (nspin == 2 || nspin == 4) @@ -152,13 +177,16 @@ void ctrl_output_fp(UnitCell& ucell, 0, // out_fermi PARAM.globalv.two_fermi, false, + append_cube, ofs_running, desc_pot); } } else if (inp.out_pot[0] == 2 && should_output) { - std::string fn = PARAM.globalv.global_out_dir + "potes"; + // Mode-2 POT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_pot_dir); + std::string fn = global_pot_dir + "potes"; fn += geom_block + ".cube"; const std::vector* pcc_potential = pelec->pot->pcc_electron_potential(); @@ -178,6 +206,7 @@ void ctrl_output_fp(UnitCell& ucell, pcc_potential, solvent_potential, inp.out_pot[1], + append_cube, ofs_running); } @@ -201,6 +230,7 @@ void ctrl_output_fp(UnitCell& ucell, 0, // out_fermi false, // two_fermi, unused without out_fermi false, // reduce_all_pool + append_cube, ofs_running, data_desc); } @@ -217,7 +247,9 @@ void ctrl_output_fp(UnitCell& ucell, module_charge::cal_rhog_symm(is, chr, pw_rhod, ucell.symm); } - std::string out_dir = PARAM.globalv.global_out_dir; + std::string out_dir = global_elf_dir; + // Mode-2 ELF/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(out_dir); ModuleIO::write_elf(out_dir, istep, nspin, @@ -229,6 +261,7 @@ void ctrl_output_fp(UnitCell& ucell, inp.out_elf[1], geom_block, PARAM.globalv.two_fermi, + append_cube, ofs_running); } @@ -251,12 +284,14 @@ void ctrl_output_fp(UnitCell& ucell, const bool output_dipole = inp.esolver_type == "tddft" || should_output; if (inp.out_dipole == 1 && output_dipole) { + // Mode-2 DIP/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_dip_dir); const int dipole_channels = nspin == 4 ? 1 : nspin; // In a spinor density only channel zero is charge; the other three channels are magnetization. for (int is = 0; is < dipole_channels; ++is) { std::stringstream ss_dipole; - ss_dipole << global_out_dir << "dipole_s" << is + 1 << ".txt"; + ss_dipole << global_dip_dir << "dipole_s" << is + 1 << ".txt"; ModuleIO::write_dipole(ucell, chr.rho[is], pw_rhod, istep, ss_dipole.str(), ofs_running); } } diff --git a/source/source_io/module_ctrl/ctrl_output_pw.cpp b/source/source_io/module_ctrl/ctrl_output_pw.cpp index 0a3b13120dd..435a6412bd4 100644 --- a/source/source_io/module_ctrl/ctrl_output_pw.cpp +++ b/source/source_io/module_ctrl/ctrl_output_pw.cpp @@ -8,6 +8,7 @@ #include "../module_wannier/to_w90_pw.h" // wannier90 interface #include "../module_wf/write_wfc_pw.h" // use write_wfc_pw #include "source_base/formatter.h" +#include "source_base/fs_compat.h" // use ModuleBase::ensure_directory #include "source_io/module_chgpot/get_pchg_pw.h" #include "source_io/module_wf/get_wf_pw.h" #include "source_lcao/module_deltaspin/deltaspin_pw_mi.h" @@ -47,7 +48,17 @@ void ModuleIO::ctrl_iter_pw(const int istep, { if (istep % output_frequency == 0) { - if (iter % inp.out_freq_elec == 0 || iter == inp.scf_nmax || conv_esolver || final_output) + // In mode 1 the fixed-name file accumulates one frame per ionic + // step, so only the last electronic iteration of each selected + // step is written (out_freq_elec is ignored). In modes 0 and 2 + // out_freq_elec still selects intermediate iterations. + const bool last_elec_iter = (iter == inp.scf_nmax || conv_esolver || final_output); + bool write_iter = last_elec_iter; + if (inp.out_app_flag != 1) + { + write_iter = write_iter || (iter % inp.out_freq_elec == 0); + } + if (write_iter) { istep_in = istep; iter_in = iter; @@ -62,9 +73,17 @@ void ModuleIO::ctrl_iter_pw(const int istep, if (out_wfc_flag && (inp.out_wfc_pw == 1 || inp.out_wfc_pw == 2)) { + const std::string global_psi_dir = PARAM.globalv.global_psi_dir; + // Mode-2 PSI/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_psi_dir); stp.sync_cpu(); + // istep_in is only the filename sentinel (-1 omits the step suffix); + // pass the real ionic step separately so append mode keeps + // accumulating frames even when the filename carries no step. + const int append_step = istep; ModuleIO::write_wfc_pw(istep_in, iter_in, + append_step, GlobalV::KPAR, GlobalV::MY_POOL, GlobalV::MY_RANK, @@ -74,8 +93,9 @@ void ModuleIO::ctrl_iter_pw(const int istep, GlobalV::RANK_IN_POOL, GlobalV::NPROC_IN_POOL, inp.out_wfc_pw, + inp.out_app_flag, inp.ecutwfc, - PARAM.globalv.global_out_dir, + global_psi_dir, stp.psi_cpu[0], kv, pw_wfc, @@ -156,6 +176,9 @@ void ModuleIO::ctrl_scf_pw(const int istep, if (out_dos_tmp) { + const std::string global_dos_dir = PARAM.globalv.global_dos_dir; + // Mode-2 DOS/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_dos_dir); ModuleIO::write_dos_pw(ucell, pelec->ekb, pelec->wg, @@ -175,7 +198,7 @@ void ModuleIO::ctrl_scf_pw(const int istep, PARAM.globalv.two_fermi, inp.out_app_flag, inp.bndpar, - PARAM.globalv.global_out_dir, + global_dos_dir, ofs_running); } } @@ -186,17 +209,23 @@ void ModuleIO::ctrl_scf_pw(const int istep, //------------------------------------------------------------------ if (inp.out_pchg.size() > 0 && output_td_state) { + const std::string global_chg_dir = PARAM.globalv.global_chg_dir; + // Mode-2 CHG/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_chg_dir); // Use the solver's native wavefunction precision for FFT and projector overlaps. ModuleIO::Get_pchg_pw output(*stp.template get_psi_t(), *pw_wfc, *pw_rho, *pw_rhod, ppcell, inp.nspin, inp.nbands); - output.begin(&ucell, para_grid, kv, inp.out_pchg, PARAM.globalv.global_out_dir, inp.if_separate_k, inp.noncolin, ofs_running); + output.begin(&ucell, para_grid, kv, inp.out_pchg, global_chg_dir, inp.if_separate_k, inp.noncolin, ofs_running); } if ((inp.out_wfc_norm.size() > 0 || inp.out_wfc_re_im.size() > 0) && output_td_state) { + const std::string global_psi_dir = PARAM.globalv.global_psi_dir; + // Mode-2 PSI/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_psi_dir); // Use the solver's native wavefunction precision for FFT. ModuleIO::Get_wf_pw output(*stp.template get_psi_t(), *pw_wfc, *pw_rho, *pw_rhod, inp.nspin, inp.nbands); - output.begin(ucell, para_grid, kv, inp.out_wfc_norm, inp.out_wfc_re_im, PARAM.globalv.global_out_dir, ofs_running); + output.begin(ucell, para_grid, kv, inp.out_wfc_norm, inp.out_wfc_re_im, global_psi_dir, ofs_running); } //------------------------------------------------------------------ @@ -289,6 +318,9 @@ void ModuleIO::ctrl_runner_pw(UnitCell& ucell, //---------------------------------------------------------- if (inp.out_ldos[0]) { + const std::string global_dos_dir = PARAM.globalv.global_dos_dir; + // Mode-2 DOS/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_dos_dir); stp.update_psi_d(); ModuleIO::cal_ldos_pw(reinterpret_cast>*>(pelec), *stp.template get_psi_d(), @@ -298,7 +330,7 @@ void ModuleIO::ctrl_runner_pw(UnitCell& ucell, inp.out_ldos, inp.stm_bias, inp.nspin, - PARAM.globalv.global_out_dir, + global_dos_dir, PARAM.globalv.two_fermi, inp.nbands, inp.dos_edelta_ev, diff --git a/source/source_io/module_ctrl/ctrl_runner_lcao.cpp b/source/source_io/module_ctrl/ctrl_runner_lcao.cpp index 5ceec5f9165..6d054d1a0a5 100644 --- a/source/source_io/module_ctrl/ctrl_runner_lcao.cpp +++ b/source/source_io/module_ctrl/ctrl_runner_lcao.cpp @@ -1,5 +1,6 @@ #include "ctrl_runner_lcao.h" // use ctrl_runner_lcao() +#include "source_base/fs_compat.h" // use ModuleBase::ensure_directory #include "source_estate/elecstate_lcao.h" // use elecstate::ElecState #include "source_lcao/hamilt_lcao.h" // use hamilt::HamiltLCAO #include "source_hamilt/module_xc/exx_info.h" @@ -20,6 +21,7 @@ namespace ModuleIO template void ctrl_runner_lcao(UnitCell& ucell, // unitcell const Input_para &inp, // input + const int istep, // ionic step index, zero-based K_Vectors &kv, // k-point elecstate::ElecState* pelec,// electronic info const module_dm::Setup_DM &dmat, // mohan add 2025-11-02 @@ -52,21 +54,37 @@ void ctrl_runner_lcao(UnitCell& ucell, // unitcell // 2) out ldos if (inp.out_ldos[0]) { + const std::string global_dos_dir = PARAM.globalv.global_dos_dir; + // Mode-2 DOS/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_dos_dir); ModuleIO::Cal_ldos::cal_ldos_lcao(pelec->eferm, chr, dmat, kv, pelec->ekb, pelec->wg, psi[0], pgrid, pv, gd, ucell, - inp.stm_bias, inp.nspin, PARAM.globalv.global_out_dir, + inp.stm_bias, inp.nspin, global_dos_dir, PARAM.globalv.two_fermi, inp.out_ldos[1], ofs_running); } - // 3) print out exchange-correlation potential - if (inp.out_mat_xc) + // 3) print out exchange-correlation potential: the band-decomposed orbital + // energies (eband_vxc.txt etc.) are produced for either switch, while + // the full MO-basis matrices (vxc_nao.txt etc.) are only for out_mat_xc. + const bool out_vxc_orb = inp.out_mat_xc || inp.out_eband; + if (out_vxc_orb) { + const std::string global_mat_dir = PARAM.globalv.global_mat_dir; + // Only out_mat_xc writes matrices into MAT/ (out_eband files land in + // the output root); the mode-2 subdirectory is created lazily here. + if (inp.out_mat_xc) + { + ModuleBase::ensure_directory(global_mat_dir); + } bool cal_exx = exx_info.info_global.cal_exx; + const double etot_xc = pelec->f_en.etot; #ifdef __EXX auto* hexxd_ptr = exx_nao.exd ? &exx_nao.exd->get_Hexxs() : nullptr; auto* hexxc_ptr = exx_nao.exc ? &exx_nao.exc->get_Hexxs() : nullptr; #endif ModuleIO::write_Vxc(inp.nspin, + istep, + etot_xc, PARAM.globalv.nlocal, GlobalV::DRANK, &pv, @@ -84,6 +102,8 @@ void ctrl_runner_lcao(UnitCell& ucell, // unitcell gd, inp.dft_plus_u, PARAM.globalv.gamma_only_local, + inp.out_mat_xc, + global_mat_dir, PARAM.globalv.global_out_dir, inp.out_ndigits, inp.ks_solver, @@ -99,6 +119,9 @@ void ctrl_runner_lcao(UnitCell& ucell, // unitcell if (inp.out_mat_xc2[0]) { + const std::string global_mat_dir = PARAM.globalv.global_mat_dir; + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); bool cal_exx = exx_info.info_global.cal_exx; double hybrid_alpha = exx_info.info_global.hybrid_alpha; bool real_number = exx_info.info_ri.real_number; @@ -119,7 +142,7 @@ void ctrl_runner_lcao(UnitCell& ucell, // unitcell kv, orb.cutoffs(), gd, - PARAM.globalv.global_out_dir, + global_mat_dir, cal_exx, hybrid_alpha, real_number @@ -136,11 +159,14 @@ void ctrl_runner_lcao(UnitCell& ucell, // unitcell // write eband terms - if (inp.out_eband_terms) + if (inp.out_eband) { + const double etot = pelec->f_en.etot; ModuleIO::write_eband_terms(inp.nspin, + istep, + etot, + GlobalV::MY_RANK, PARAM.globalv.nlocal, - GlobalV::DRANK, &pv, *psi, ucell, @@ -154,13 +180,7 @@ void ctrl_runner_lcao(UnitCell& ucell, // unitcell pelec->wg, gd, orb.cutoffs(), - two_center_bundle, - exx_info -#ifdef __EXX - , - exx_nao.exd ? &exx_nao.exd->get_Hexxs() : nullptr, - exx_nao.exc ? &exx_nao.exc->get_Hexxs() : nullptr -#endif + two_center_bundle ); } @@ -173,6 +193,7 @@ void ctrl_runner_lcao(UnitCell& ucell, // unitcell // TK: double TR: double template void ctrl_runner_lcao(UnitCell& ucell, // unitcell const Input_para &inp, // input + const int istep, // ionic step index, zero-based K_Vectors &kv, // k-point elecstate::ElecState* pelec,// electronic info const module_dm::Setup_DM &dmat, // mohan add 2025-11-02 @@ -196,6 +217,7 @@ template void ctrl_runner_lcao(UnitCell& ucell, // unitcell // TK: complex TR: double template void ctrl_runner_lcao, double>(UnitCell& ucell, // unitcell const Input_para &inp, // input + const int istep, // ionic step index, zero-based K_Vectors &kv, // k-point elecstate::ElecState* pelec,// electronic info const module_dm::Setup_DM> &dmat, // mohan add 2025-11-02 @@ -219,6 +241,7 @@ template void ctrl_runner_lcao, double>(UnitCell& ucell, // TK: complex TR: complex template void ctrl_runner_lcao, std::complex>(UnitCell& ucell, // unitcell const Input_para &inp, // input + const int istep, // ionic step index, zero-based K_Vectors &kv, // k-point elecstate::ElecState* pelec,// electronic info const module_dm::Setup_DM> &dmat, // mohan add 2025-11-02 diff --git a/source/source_io/module_ctrl/ctrl_runner_lcao.h b/source/source_io/module_ctrl/ctrl_runner_lcao.h index 96434852343..a9f36154f48 100644 --- a/source/source_io/module_ctrl/ctrl_runner_lcao.h +++ b/source/source_io/module_ctrl/ctrl_runner_lcao.h @@ -18,6 +18,7 @@ namespace ModuleIO template void ctrl_runner_lcao(UnitCell& ucell, // unitcell const Input_para &inp, // input + const int istep, // ionic step index, zero-based K_Vectors &kv, // k-point elecstate::ElecState* pelec,// electronic info const module_dm::Setup_DM &dmat, // mohan add 2025-11-02 diff --git a/source/source_io/module_ctrl/ctrl_scf_lcao.cpp b/source/source_io/module_ctrl/ctrl_scf_lcao.cpp index 6c21487e0a4..16ec490487d 100644 --- a/source/source_io/module_ctrl/ctrl_scf_lcao.cpp +++ b/source/source_io/module_ctrl/ctrl_scf_lcao.cpp @@ -2,6 +2,7 @@ #include "source_hamilt/module_xc/exx_info.h" #include "source_base/formatter.h" +#include "source_base/fs_compat.h" // use ModuleBase::ensure_directory #include "source_base/module_out/filename.h" #include "source_base/tool_quit.h" // use ModuleBase::WARNING_QUIT #include "source_estate/elecstate_lcao.h" // use elecstate::ElecState @@ -53,8 +54,8 @@ void setup_exx_h_params(ModuleIO::WriteHParams& h_params, Exx_NAO& exx_nao, // EXX-H output is unsupported. Reject it explicitly here so the request cannot be silently // dropped (the WARNING_QUIT inside write_h_exx is unreachable at multi-k). ModuleBase::WARNING_QUIT("setup_exx_h_params", - "out_mat_h_exx is only supported for gamma-only: the V^EXX(R) " - "output is not available at multi-k. Use gamma_only."); + "out_mat_exxk (legacy out_mat_h_exx) is only supported for gamma-only: " + "the V^EXX(R) output is not available at multi-k. Use gamma_only."); } template <> @@ -136,16 +137,24 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, //***** - const bool out_app_flag = inp.out_app_flag; + const int out_app_flag = inp.out_app_flag; const bool gamma_only = PARAM.globalv.gamma_only_local; const int nspin = inp.nspin; const std::string global_out_dir = PARAM.globalv.global_out_dir; + const std::string global_mat_dir = PARAM.globalv.global_mat_dir; + const std::string global_matrix_dir = PARAM.globalv.global_matrix_dir; + const int npol = PARAM.globalv.npol; + const int nlocal = PARAM.globalv.nlocal; + const bool cal_force = inp.cal_force; //------------------------------------------------------------------ //! 1) print out density of states (DOS) //------------------------------------------------------------------ if (inp.out_dos) { + const std::string global_dos_dir = PARAM.globalv.global_dos_dir; + // Mode-2 DOS/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_dos_dir); ModuleIO::write_dos_lcao(psi, p_hamilt, pv, @@ -168,7 +177,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, inp.dos_emin_ev, PARAM.globalv.two_fermi, inp.bndpar, - PARAM.globalv.global_out_dir, + global_dos_dir, ofs_running); } @@ -177,10 +186,15 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, //------------------------------------------------------------------ if (inp.out_dmr[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); const int precision = inp.out_dmr[1]; - - ModuleIO::write_dmr(dm->get_dmr_vec(), &ucell, precision, pv, out_app_flag, - ucell.get_iat2iwt(), ucell.nat, istep, ofs_running); + // File-name selection and append/truncate are independent decisions: + // mode 0 keeps the fixed filename and overwrites it at every write, + // mode 1 keeps the fixed filename and appends later steps, and mode 2 + // embeds the ionic step in the g{step} filename. + ModuleIO::write_dmr(dm->get_dmr_vec(), &ucell, precision, pv, out_app_flag, + ucell.get_iat2iwt(), ucell.nat, global_mat_dir, istep, ofs_running); } //------------------------------------------------------------------ @@ -188,6 +202,8 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, //------------------------------------------------------------------ if (inp.out_dmk[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); std::vector efermis(nspin == 2 ? 2 : 1); for (int ispin = 0; ispin < efermis.size(); ispin++) { @@ -195,7 +211,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, } const int precision = inp.out_dmk[1]; - ModuleIO::write_dmk(dm->get_dmk_vec(), kv, precision, efermis, &(ucell), pv, global_out_dir, istep, ofs_running); + ModuleIO::write_dmk(dm->get_dmk_vec(), kv, precision, efermis, &(ucell), pv, global_mat_dir, istep, out_app_flag, ofs_running); } //------------------------------------------------------------------ @@ -204,8 +220,10 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, const int hsk_out_type = inp.out_hsk[0]; if (hsk_out_type == 1 || hsk_out_type == 2) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); const int precision = inp.out_hsk[1]; - ModuleIO::write_hsk(global_out_dir, + ModuleIO::write_hsk(global_mat_dir, nspin, kv.get_nks(), kv.get_nkstot(), @@ -274,13 +292,15 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, //------------------------------------------------------------------ if (inp.out_hsr[0] == 1 || inp.out_hsr[0] == 2) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); const int precision = inp.out_hsr[1]; std::vector*> hr_vec = p_hamilt->getHR_vector(); const hamilt::HContainer* sr = p_hamilt->getSR(); ModuleIO::write_hsr(hr_vec, sr, &ucell, inp.out_hsr[0], precision, pv, out_app_flag, gamma_only, ucell.get_iat2iwt(), ucell.nat, istep, - PARAM.globalv.global_out_dir, pelec->eferm, + global_mat_dir, pelec->eferm, ofs_running); } @@ -290,28 +310,34 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, const bool output_hsr_npz = inp.out_hsr[0] == 3 || inp.out_hsr_npz_compat; if (output_hsr_npz) { - std::string zipname = PARAM.globalv.global_out_dir + "sr_nao.npz"; + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); + std::string zipname = global_mat_dir + "sr_nao.npz"; ModuleIO::output_mat_npz(ucell, zipname, *(p_hamilt->getSR())); } if (inp.out_hr_npz || output_hsr_npz) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); std::vector*> hr_vec = p_hamilt->getHR_vector(); for (int ispin = 0; ispin < hr_vec.size(); ++ispin) { std::string zipname - = PARAM.globalv.global_out_dir + "hrs" + std::to_string(ispin + 1) + "_nao.npz"; + = global_mat_dir + "hrs" + std::to_string(ispin + 1) + "_nao.npz"; ModuleIO::output_mat_npz(ucell, zipname, *(hr_vec[ispin])); } } if (inp.out_dm_npz) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); const std::vector*>& dmr_vec = dm->get_dmr_vec(); for (int ispin = 0; ispin < dmr_vec.size(); ++ispin) { std::string zipname - = PARAM.globalv.global_out_dir + "output_DM" + std::to_string(ispin) + ".npz"; + = global_mat_dir + "output_DM" + std::to_string(ispin) + ".npz"; ModuleIO::output_mat_npz(ucell, zipname, *(dmr_vec[ispin])); } } @@ -343,6 +369,15 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, kv, p_ham_tk, &dftu, + global_out_dir, + global_matrix_dir, + inp.calculation, + out_app_flag, + nspin, + gamma_only, + npol, + nlocal, + cal_force, ofs_running); //------------------------------------------------------------------ @@ -399,7 +434,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, dh_params.nspin = inp.nspin; dh_params.istep = istep; dh_params.gamma_only = gamma_only; - dh_params.append = out_app_flag; + dh_params.append = (out_app_flag == 1); dh_params.ofs_running = &ofs_running; if (PARAM.inp.nspin == 1 || PARAM.inp.nspin == 2) { @@ -442,7 +477,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, h_params.nrxx = pw_rho->nrxx; h_params.nspin = nspin; h_params.istep = istep; - h_params.append = out_app_flag; + h_params.append = (out_app_flag == 1); h_params.iat2iwt = ucell.get_iat2iwt(); h_params.nat = ucell.nat; h_params.nlocal = PARAM.globalv.nlocal; @@ -453,35 +488,51 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, h_params.gga_grad = PARAM.inp.gga_grad; h_params.out_app_flag = out_app_flag; h_params.calculation = inp.calculation; - h_params.global_out_dir = global_out_dir; - h_params.global_matrix_dir = PARAM.globalv.global_matrix_dir; + // H(k) component matrices follow the selected matrix directory so + // they land in MAT/ together with the other matrix outputs in mode 2 + h_params.global_out_dir = global_mat_dir; + h_params.global_matrix_dir = global_matrix_dir; h_params.ks_solver = PARAM.inp.ks_solver; h_params.drank = GlobalV::DRANK; h_params.ofs_running = &ofs_running; - if (inp.out_mat_h_t[0]) - { - ModuleIO::write_h_t(h_params); - } - if (inp.out_mat_h_vnl[0]) + // out_mat_h_t is a legacy alias for out_mat_tk (resolved at input + // normalization); the dedicated write_h_t path is removed because it + // duplicated the T(k) output of the out_mat_tk block below. + if (inp.out_mat_vnlk[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); + h_params.precision = inp.out_mat_vnlk[1]; ModuleIO::write_h_vnl(h_params); } - if (inp.out_mat_h_vl[0]) + if (inp.out_mat_vlk[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); + h_params.precision = inp.out_mat_vlk[1]; ModuleIO::write_h_vl(h_params); } - if (inp.out_mat_h_vh[0]) + if (inp.out_mat_vhk[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); + h_params.precision = inp.out_mat_vhk[1]; ModuleIO::write_h_vh(h_params); } - if (inp.out_mat_h_vxc[0]) + if (inp.out_mat_vxck[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); + h_params.precision = inp.out_mat_vxck[1]; ModuleIO::write_h_vxc(h_params); } #ifdef __EXX - if (inp.out_mat_h_exx[0] && exx_info.info_global.cal_exx) + if (inp.out_mat_exxk[0] && exx_info.info_global.cal_exx) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); // V^EXX(R) output is wired for the gamma (TK==double) exx interfaces. + h_params.precision = inp.out_mat_exxk[1]; setup_exx_h_params(h_params, exx_nao, exx_info); } #endif @@ -491,6 +542,8 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, //------------------------------------------------------------------ if (inp.out_mat_tk[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); hamilt::HS_Matrix_K hsk(&pv, true); hamilt::HContainer hR(&pv); hamilt::Operator* ekinetic @@ -509,7 +562,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, const int out_label = 1; // 1: .txt, 2: .dat - std::string t_fn = ModuleIO::filename_output(global_out_dir, + std::string t_fn = ModuleIO::filename_output(global_mat_dir, "tk", "nao", ik, @@ -527,7 +580,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, false, // bit inp.out_mat_tk[1], 1, // true for upper triangle matrix - inp.out_app_flag, + (inp.out_app_flag == 1), t_fn, pv, GlobalV::DRANK, @@ -547,6 +600,8 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, //------------------------------------------------------------------ if (inp.out_mat_l[0]) { + // Mode-2 MAT/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(global_mat_dir); ModuleIO::Angmom_op mylcalculator(inp.orbital_dir, ucell, orb.get_rcutmax_Phi(), @@ -558,7 +613,7 @@ void ModuleIO::ctrl_scf_lcao(UnitCell& ucell, PARAM.globalv.gamma_only_local, &ofs_running, GlobalV::MY_RANK); - mylcalculator.calculate(inp.suffix, global_out_dir, ucell, inp.out_mat_l[1], GlobalV::MY_RANK, istep_in); + mylcalculator.calculate(inp.suffix, global_mat_dir, ucell, inp.out_mat_l[1], GlobalV::MY_RANK, istep_in, out_app_flag); } //------------------------------------------------------------------ diff --git a/source/source_io/module_dhs/write_dh.cpp b/source/source_io/module_dhs/write_dh.cpp index 9a61cf3dee9..3acd1220972 100644 --- a/source/source_io/module_dhs/write_dh.cpp +++ b/source/source_io/module_dhs/write_dh.cpp @@ -1,5 +1,6 @@ #include "write_dh.h" +#include "source_base/fs_compat.h" #include "source_base/global_function.h" #include "source_base/timer.h" #include "source_io/module_hs/hs_dense_io.h" @@ -43,10 +44,20 @@ void write_dh_perI(WriteDHParams& params, const int nspin_k = (nspin == 2 ? 2 : 1); const int nks = params.kv->get_nks() / nspin_k; const int nlocal = PARAM.globalv.nlocal; - const std::string global_out_dir = PARAM.globalv.global_out_dir; - const bool out_app_flag = PARAM.inp.out_app_flag; - const std::string r_dir - = (PARAM.inp.calculation == "md" && !out_app_flag) ? PARAM.globalv.global_matrix_dir : global_out_dir; + // All dH component outputs (CSR dH/dR, dense dH/dk) belong to the unified + // matrix directory: global_mat_dir is set to OUT.root in modes 0/1 and to + // OUT.root/MAT/ in mode 2 by ReadInput::set_global_dir, so no caller-side + // calculation or out_app_flag branching is needed here. + const std::string mat_dir = PARAM.globalv.global_mat_dir; + // The mode-2 MAT/ subdirectory is created lazily here at write time; in + // modes 0/1 this is a no-op because mat_dir is the existing output dir. + ModuleBase::ensure_directory(mat_dir); + const int out_app_flag = PARAM.inp.out_app_flag; + // Mode 1 appends one frame per ionic step; the first step (istep == 0) + // truncates the fixed file so a rerun does not keep stale records. Only + // mode 2 embeds the step in the name. + const bool append = (out_app_flag == 1) && (params.istep > 0); + const bool numbered = (out_app_flag == 2); #ifdef __MPI Parallel_Orbitals serialV; @@ -79,15 +90,15 @@ void write_dh_perI(WriteDHParams& params, if (GlobalV::MY_RANK == 0) #endif { - std::string fr = r_dir + ModuleIO::dhr_gen_fname(rprefix + tag, ispin, params.append, params.istep); + std::string fr = mat_dir + ModuleIO::dhr_gen_fname(rprefix + tag, ispin, numbered, params.istep); // dH/dR is not a Hamiltonian: no Fermi energy annotation in the header const double no_efermi = 0.0; #ifdef __MPI ModuleIO::write_hcontainer_csr( - fr, &ucell, 8, &hR_s, params.istep, ispin, nspin, label, "", no_efermi, false); + fr, &ucell, 8, &hR_s, params.istep, append, ispin, nspin, label, "", no_efermi, false); #else ModuleIO::write_hcontainer_csr( - fr, &ucell, 8, hR, params.istep, ispin, nspin, label, "", no_efermi, false); + fr, &ucell, 8, hR, params.istep, append, ispin, nspin, label, "", no_efermi, false); #endif if (params.ofs_running != nullptr) { @@ -117,19 +128,20 @@ void write_dh_perI(WriteDHParams& params, #else hamilt::folding_HR(*hR, hk.data(), params.kv->kvec_d[ik], nlocal, 0); #endif - std::string fk = global_out_dir + kprefix + tag; + std::string fk = mat_dir + kprefix + tag; if (nks > 1) { fk += "_ik" + std::to_string(params.kv->ik2iktot[ik]); } fk += "_nao.txt"; + const bool append_fk = (out_app_flag == 1) && (params.istep > 0); ModuleIO::save_mat(params.istep, hk.data(), nlocal, false, 8, false, - out_app_flag, + append_fk, fk, pv, GlobalV::DRANK, diff --git a/source/source_io/module_dipole/write_dipole.cpp b/source/source_io/module_dipole/write_dipole.cpp index 19fb6bdcea6..f1bc24b05b5 100644 --- a/source/source_io/module_dipole/write_dipole.cpp +++ b/source/source_io/module_dipole/write_dipole.cpp @@ -70,6 +70,9 @@ void write_dipole(const UnitCell& ucell, ofs_running << " Write dipole data to file: " << fn << std::endl; // Empty local FFT partitions contribute zero but must still join the subsequent reduction. + // Do NOT reject nplane == 0 alone: some MPI ranks legitimately hold zero + // z-planes (empty partition), in which case nrxx is also 0 and the + // accumulation loop below is a harmless no-op. if (rhopw->nx == 0 || rhopw->ny == 0 || rhopw->nz == 0 || rhopw->nxyz == 0 || (rhopw->nplane == 0 && rhopw->nrxx != 0)) { ModuleBase::WARNING_QUIT("ModuleIO::write_dipole", "Invalid global grid dimensions or inconsistent local plane count"); @@ -119,13 +122,16 @@ void write_dipole(const UnitCell& ucell, // Output electronic dipole moment print_dipole_moment(ofs_running, "Electronic dipole moment", dipole_elec[0], dipole_elec[1], dipole_elec[2]); - // Write to file: step index and three dipole components - ofs << std::right << std::setw(6) << istep + 1 << std::scientific << std::setprecision(precision - 1); - for (int direction = 0; direction < 3; ++direction) + // Write to file on master process only; ofs is opened only there. + if (GlobalV::MY_RANK == 0) { - ofs << " " << std::setw(19) << dipole_elec[direction]; + ofs << std::right << std::setw(6) << istep + 1 << std::scientific << std::setprecision(precision - 1); + for (int direction = 0; direction < 3; ++direction) + { + ofs << " " << std::setw(19) << dipole_elec[direction]; + } + ofs << std::endl; } - ofs << std::endl; // Calculate ionic dipole moment // Accumulate Z_v * taud, then transform to Cartesian coordinates. diff --git a/source/source_io/module_dm/test/test_write_dmr.cpp b/source/source_io/module_dm/test/test_write_dmr.cpp index e5876e7c07a..f8946e280ec 100644 --- a/source/source_io/module_dm/test/test_write_dmr.cpp +++ b/source/source_io/module_dm/test/test_write_dmr.cpp @@ -67,12 +67,26 @@ void fill_matrix(hamilt::HContainer& matrix, Parallel_Orbitals& pv, doub hamilt::AtomPair pair(0, 0, 0, 0, 0, &pv, values); matrix.insert_pair(pair); } + +int count_frames(const std::string& content) +{ + const std::string tag = " --- Ionic Step "; + int count = 0; + std::string::size_type pos = 0; + while ((pos = content.find(tag, pos)) != std::string::npos) + { + ++count; + pos += tag.size(); + } + return count; +} } // namespace TEST(WriteDmr, GenFnameKeepsCurrentContract) { EXPECT_EQ(ModuleIO::dmr_gen_fname(1, 0, true, -1), "dmrs1_nao.csr"); - EXPECT_EQ(ModuleIO::dmr_gen_fname(1, 1, false, 2), "dmrs2g3_nao.csr"); + EXPECT_EQ(ModuleIO::dmr_gen_fname(1, 1, false, 2), "dmrs2_nao.csr"); + EXPECT_EQ(ModuleIO::dmr_gen_fname(1, 1, true, 2), "dmrs2g3_nao.csr"); EXPECT_EQ(ModuleIO::dmr_gen_fname(2, 0, true, 5), "dmrs1_nao.npz"); } @@ -89,7 +103,7 @@ TEST(WriteDmr, CsrHeaderKeepsCurrentFormat) double values[4] = {0.25, 0.0, 0.0, 0.75}; fill_matrix(matrix, pv, values); - ModuleIO::write_dmr_csr(filename, &ucell, 4, &matrix, 0, 1, 2); + ModuleIO::write_dmr_csr(filename, &ucell, 4, &matrix, 0, 1, 2, false); const std::string output = read_file(filename); EXPECT_THAT(output, testing::HasSubstr(" --- Ionic Step 1 ---\n")); @@ -101,3 +115,40 @@ TEST(WriteDmr, CsrHeaderKeepsCurrentFormat) std::remove(filename.c_str()); } + +TEST(WriteDmr, ModeOneFixedNameTruncatesOnRerun) +{ + // Mirror the controller policy for out_app_flag == 1: the DM(R) + // filename stays fixed for every step; write_dmr_csr truncates at + // istep <= 0 and appends otherwise. A 0 -> 1 -> 0 sequence (two steps + // of one run, then the first step of a rerun in the same directory) + // must therefore use one filename and drop the previous run's frames. + const bool numbered = false; // out_app_flag == 1 + EXPECT_EQ(ModuleIO::dmr_gen_fname(1, 0, numbered, 0), "dmrs1_nao.csr"); + EXPECT_EQ(ModuleIO::dmr_gen_fname(1, 0, numbered, 1), "dmrs1_nao.csr"); + + UnitCell ucell; + init_unitcell(ucell); + Parallel_Orbitals pv; + init_serial_orbitals(pv); + hamilt::HContainer matrix(&pv); + double values[4] = {0.25, 0.0, 0.0, 0.75}; + fill_matrix(matrix, pv, values); + + std::string filename = testing::TempDir() + "/dmrs1_nao.csr"; + std::remove(filename.c_str()); + + ModuleIO::write_dmr_csr(filename, &ucell, 4, &matrix, 0, 0, 1, true); + ModuleIO::write_dmr_csr(filename, &ucell, 4, &matrix, 1, 0, 1, true); + const std::string after_first_run = read_file(filename); + EXPECT_EQ(count_frames(after_first_run), 2); + EXPECT_THAT(after_first_run, testing::HasSubstr(" --- Ionic Step 2 ---\n")); + + // First step of a rerun must truncate the accumulated frames. + ModuleIO::write_dmr_csr(filename, &ucell, 4, &matrix, 0, 0, 1, true); + const std::string after_rerun = read_file(filename); + EXPECT_EQ(count_frames(after_rerun), 1); + EXPECT_THAT(after_rerun, testing::HasSubstr(" --- Ionic Step 1 ---\n")); + + std::remove(filename.c_str()); +} diff --git a/source/source_io/module_dm/test/write_dmk_test.cpp b/source/source_io/module_dm/test/write_dmk_test.cpp index 8a2998c58c0..da27cbe92c8 100644 --- a/source/source_io/module_dm/test/write_dmk_test.cpp +++ b/source/source_io/module_dm/test/write_dmk_test.cpp @@ -165,8 +165,8 @@ TEST(DMKTest,WriteDMK) { kv.kd_done = true; std::ofstream ofs_log; - ModuleIO::write_dmk(dmk, kv, 3, efs, ucell, pv, out_dir, istep, ofs_log); - ModuleIO::write_dmk(dmk_multik, kv, 3, efs, ucell, pv, out_dir, istep, ofs_log); + ModuleIO::write_dmk(dmk, kv, 3, efs, ucell, pv, out_dir, istep, 0, ofs_log); + ModuleIO::write_dmk(dmk_multik, kv, 3, efs, ucell, pv, out_dir, istep, 0, ofs_log); std::ifstream ifs; @@ -195,6 +195,8 @@ TEST(DMKTest,WriteDMK) { std::istreambuf_iterator()); EXPECT_THAT(str, testing::HasSubstr("0 # Fermi energy in Ry")); EXPECT_THAT(str, testing::HasSubstr("20 # number of localized basis")); + // the matrix body must end with a newline so appended records are not glued + EXPECT_EQ(str.back(), '\n'); ifs.close(); fn = "dmk2s1_nao.txt"; @@ -233,6 +235,181 @@ TEST(DMKTest,WriteDMK) { }; +// --------------------------------------------------------------------------- +// out_app_flag = 0: one fixed filename, truncated and overwritten each call. +// --------------------------------------------------------------------------- +TEST(DMKTest, Mode0FixedNameOverwrite) { + UnitCell* ucell; + UcellTestPrepare utp = UcellTestLib["Si"]; + ucell = utp.SetUcellInfo(); + + int nspin = 2; + int nk = 1; + int nlocal = 20; + std::vector> dmk; + std::vector efs; + Parallel_2D pv; + init_pv(nlocal, pv); + gen_dmk(dmk, efs, nspin, nk, nlocal, pv); + + K_Vectors kv; + kv.set_nkstot(1); + kv.set_nkstot_nospin(1); + kv.set_nks(1); + kv.set_spin_mult(2); + kv.kvec_c.resize(1); + kv.kvec_c[0].x = 0.0; + kv.kvec_c[0].y = 0.0; + kv.kvec_c[0].z = 0.0; + kv.kvec_d.resize(1); + kv.kvec_d[0].x = 0.0; + kv.kvec_d[0].y = 0.0; + kv.kvec_d[0].z = 0.0; + kv.wk.resize(1); + kv.wk[0] = 1.0; + kv.isk.resize(1); + kv.isk[0] = 0; + + const std::string out_dir = "./"; + const int out_app_flag = 0; + std::ofstream ofs_log; + + // Two different isteps -> the same fixed filename, overwritten each call. + ModuleIO::write_dmk(dmk, kv, /*precision=*/3, efs, ucell, pv, out_dir, /*istep=*/0, out_app_flag, ofs_log); + ModuleIO::write_dmk(dmk, kv, /*precision=*/3, efs, ucell, pv, out_dir, /*istep=*/1, out_app_flag, ofs_log); + + if (GlobalV::MY_RANK == 0) + { + std::ifstream ifs("dms1_nao.txt"); + std::string str((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + // The second write replaced the first record: only step 2 remains. + EXPECT_THAT(str, testing::HasSubstr(" --- Ionic Step 2 ---")); + EXPECT_THAT(str, testing::Not(testing::HasSubstr(" --- Ionic Step 1 ---"))); + // g-suffixed files must NOT exist. + ifs.open("dms1g1_nao.txt"); + EXPECT_FALSE(ifs.good()); + ifs.open("dms1g2_nao.txt"); + EXPECT_FALSE(ifs.good()); + remove("dms1_nao.txt"); + remove("dms2_nao.txt"); + } + delete ucell; +} + +// --------------------------------------------------------------------------- +// out_app_flag = 1: fixed filename, multiple calls append successive records. +// --------------------------------------------------------------------------- +TEST(DMKTest, Mode1FixedNameAppend) { + UnitCell* ucell; + UcellTestPrepare utp = UcellTestLib["Si"]; + ucell = utp.SetUcellInfo(); + + int nspin = 2; + int nk = 1; + int nlocal = 20; + std::vector> dmk; + std::vector efs; + Parallel_2D pv; + init_pv(nlocal, pv); + gen_dmk(dmk, efs, nspin, nk, nlocal, pv); + + K_Vectors kv; + kv.set_nkstot(1); + kv.set_nkstot_nospin(1); + kv.set_nks(1); + kv.set_spin_mult(2); + kv.kvec_c.resize(1); + kv.kvec_c[0].x = 0.0; + kv.kvec_c[0].y = 0.0; + kv.kvec_c[0].z = 0.0; + kv.kvec_d.resize(1); + kv.kvec_d[0].x = 0.0; + kv.kvec_d[0].y = 0.0; + kv.kvec_d[0].z = 0.0; + kv.wk.resize(1); + kv.wk[0] = 1.0; + kv.isk.resize(1); + kv.isk[0] = 0; + + const std::string out_dir = "./"; + const int out_app_flag = 1; + std::ofstream ofs_log; + + // Two ionic steps -> both write to dms1_nao.txt. + ModuleIO::write_dmk(dmk, kv, /*precision=*/3, efs, ucell, pv, out_dir, /*istep=*/0, out_app_flag, ofs_log); + ModuleIO::write_dmk(dmk, kv, /*precision=*/3, efs, ucell, pv, out_dir, /*istep=*/1, out_app_flag, ofs_log); + + if (GlobalV::MY_RANK == 0) + { + std::ifstream ifs("dms1_nao.txt"); + std::string str((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + // Two "Ionic Step" headers present -> two appended records. + EXPECT_THAT(str, testing::HasSubstr(" --- Ionic Step 1 ---")); + EXPECT_THAT(str, testing::HasSubstr(" --- Ionic Step 2 ---")); + // g-suffixed file must NOT exist. + ifs.open("dms1g1_nao.txt"); + EXPECT_FALSE(ifs.good()); + remove("dms1_nao.txt"); + } + delete ucell; +} + +// --------------------------------------------------------------------------- +// out_app_flag = 2: per-step g{step} filename; directory routing is handled by +// set_global_dir and is covered by io_sys_var_test. +// --------------------------------------------------------------------------- +TEST(DMKTest, Mode2PerStepFile) { + UnitCell* ucell; + UcellTestPrepare utp = UcellTestLib["Si"]; + ucell = utp.SetUcellInfo(); + + int nspin = 2; + int nk = 1; + int nlocal = 20; + std::vector> dmk; + std::vector efs; + Parallel_2D pv; + init_pv(nlocal, pv); + gen_dmk(dmk, efs, nspin, nk, nlocal, pv); + + K_Vectors kv; + kv.set_nkstot(1); + kv.set_nkstot_nospin(1); + kv.set_nks(1); + kv.set_spin_mult(2); + kv.kvec_c.resize(1); + kv.kvec_c[0].x = 0.0; + kv.kvec_c[0].y = 0.0; + kv.kvec_c[0].z = 0.0; + kv.kvec_d.resize(1); + kv.kvec_d[0].x = 0.0; + kv.kvec_d[0].y = 0.0; + kv.kvec_d[0].z = 0.0; + kv.wk.resize(1); + kv.wk[0] = 1.0; + kv.isk.resize(1); + kv.isk[0] = 0; + + const std::string out_dir = "./"; + const int out_app_flag = 2; + std::ofstream ofs_log; + + ModuleIO::write_dmk(dmk, kv, /*precision=*/3, efs, ucell, pv, out_dir, /*istep=*/2, out_app_flag, ofs_log); + + if (GlobalV::MY_RANK == 0) + { + std::ifstream ifs("dms1g3_nao.txt"); + EXPECT_TRUE(ifs.good()); + ifs.close(); + remove("dms1g3_nao.txt"); + } + delete ucell; +} + /* // no function in the main code calls read_dmk??? mohan note 2025-05-25 TEST(DMKTest, ReadDMK) { diff --git a/source/source_io/module_dm/write_dmk.cpp b/source/source_io/module_dm/write_dmk.cpp index beec822652a..10e02669bb5 100644 --- a/source/source_io/module_dm/write_dmk.cpp +++ b/source/source_io/module_dm/write_dmk.cpp @@ -188,11 +188,23 @@ void ModuleIO::write_dmk(const std::vector>& dmk, const Parallel_2D& pv, const std::string& dmk_dir, const int istep, + const int out_app_flag, std::ofstream& ofs_running) { ModuleBase::TITLE("ModuleIO", "write_dmk"); ModuleBase::timer::start("ModuleIO", "write_dmk"); + // Modes 0 and 1 reuse a fixed filename (mode 0 overwrites the previous + // snapshot, mode 1 appends a new record); only mode 2 embeds the ionic + // step in the filename. The file body always records the real ionic step. + const int istep_file = (out_app_flag == 2) ? istep : -1; + // Each multi-step run (MD or relax) appends a new record to the fixed + // file, mirroring the open_sparse_file convention; single-point SCF + // passes istep = -1 and starts a fresh file. The first written step + // (istep == 0) truncates so a fresh run drops records left by a previous + // run; only later steps (istep > 0) append. + const bool append_on_restart = (out_app_flag == 1) && (istep > 0); + int my_rank = 0; #ifdef __MPI MPI_Comm_rank(pv.comm(), &my_rank); @@ -239,9 +251,15 @@ void ModuleIO::write_dmk(const std::vector>& dmk, if (my_rank == 0) { std::string fn = dmk_dir - + dmk_gen_fname(gamma_only, ispin, nspin, ik, istep); + + dmk_gen_fname(gamma_only, ispin, nspin, ik, istep_file); - std::ofstream ofs(fn.c_str()); + std::ofstream ofs; + std::ios_base::openmode mode = std::ios::out; + if (append_on_restart) + { + mode |= std::ios::app; + } + ofs.open(fn.c_str(), mode); ofs << " --- Ionic Step " << istep+1 << " ---" << std::endl; @@ -315,6 +333,10 @@ void ModuleIO::write_dmk(const std::vector>& dmk, } } } + // Terminate the matrix body with a newline so that the next + // appended record (out_app_flag == 1) starts on a fresh line + // instead of being glued to the last value of this step. + ofs << std::endl; ofs.close(); } // rank0 } // ik @@ -347,6 +369,7 @@ template void ModuleIO::write_dmk(const std::vector> const Parallel_2D& pv, const std::string& dmk_dir, const int istep, + const int out_app_flag, std::ofstream& ofs_running); template void ModuleIO::write_dmk>(const std::vector>>& dmk, @@ -357,5 +380,6 @@ template void ModuleIO::write_dmk>(const std::vector void write_dmk(const std::vector>& dmk, @@ -78,6 +81,7 @@ void write_dmk(const std::vector>& dmk, const Parallel_2D& pv, const std::string& dmk_dir, const int istep, + const int out_app_flag, std::ofstream& ofs_running); } // namespace ModuleIO diff --git a/source/source_io/module_dm/write_dmr.cpp b/source/source_io/module_dm/write_dmr.cpp index 6b95e726a31..12af7520c80 100644 --- a/source/source_io/module_dm/write_dmr.cpp +++ b/source/source_io/module_dm/write_dmr.cpp @@ -1,19 +1,19 @@ #include "write_dmr.h" #include "source_io/module_output/spin_tag.h" -#include "source_io/module_parameter/parameter.h" +#include "source_base/global_variable.h" #include "source_hamilt/module_hcontainer/hcontainer_funcs.h" #include "source_hamilt/module_hcontainer/output_hcontainer.h" #include "source_cell/ucell_io.h" namespace ModuleIO { -std::string dmr_gen_fname(const int out_type, const int ispin, const bool append, const int istep) +std::string dmr_gen_fname(const int out_type, const int ispin, const bool numbered, const int istep) { std::string fname = "dmr.csr"; if (out_type == 1) { - if (!append && istep >= 0) + if (numbered && istep >= 0) { // spa stands for sparse fname = "dmrs" + std::to_string(ispin+1) + "g" + std::to_string(istep + 1) + "_nao.csr"; @@ -40,20 +40,24 @@ void write_dmr_csr(std::string& fname, hamilt::HContainer* dm_serial, const int istep, const int ispin, - const int nspin) + const int nspin, + const bool append) { // write the head: ION step number, basis number and R loop number std::ofstream ofs; - // mohan update 2025-05-26 - if(istep<=0) + // The first written step of a run (or a single-point SCF) truncates the + // fixed file so a rerun does not keep stale records; later steps append + // only when the caller asks for it (mode 1). + const bool append_now = append && (istep > 0); + if (append_now) { - ofs.open(fname); + ofs.open(fname, std::ios::app); } - else if(istep>0) + else { - ofs.open(fname, std::ios::app); + ofs.open(fname); } @@ -82,14 +86,20 @@ void write_dmr(const std::vector*> dmr, const UnitCell* ucell, const int precision, const Parallel_2D& paraV, - const bool append, + const int out_app_flag, const int* iat2iwt, const int nat, + const std::string& dmr_dir, const int istep, std::ofstream& ofs_running) { const int nspin = dmr.size(); assert(nspin > 0); + // Mode 1 appends one frame per ionic step; the first step (istep == 0) + // truncates the fixed file so a rerun does not keep stale records. Only + // mode 2 embeds the step in the name. + const bool append = (out_app_flag == 1) && (istep > 0); + const bool numbered = (out_app_flag == 2); for (int ispin = 0; ispin < nspin; ispin++) { const int nbasis = dmr[ispin]->get_nbasis(); @@ -110,8 +120,8 @@ void write_dmr(const std::vector*> dmr, // out_type = 1, csr format; // out_type = 2, npz format (currently not support) const int out_type = 1; - std::string fname = PARAM.globalv.global_out_dir + dmr_gen_fname(out_type, ispin, append, istep); - write_dmr_csr(fname, ucell, precision, &dm_serial, istep, ispin, nspin); + std::string fname = dmr_dir + dmr_gen_fname(out_type, ispin, numbered, istep); + write_dmr_csr(fname, ucell, precision, &dm_serial, istep, ispin, nspin, append); const std::string spin_tag = ModuleIO::make_spin_tag(ispin, nspin); ofs_running << " Write DM(R)" << spin_tag << " matrix in NAO basis to file: " << fname << std::endl; } diff --git a/source/source_io/module_dm/write_dmr.h b/source/source_io/module_dm/write_dmr.h index 644e2828b52..640fb54d896 100644 --- a/source/source_io/module_dm/write_dmr.h +++ b/source/source_io/module_dm/write_dmr.h @@ -16,13 +16,14 @@ namespace ModuleIO * * @param out_type The output type. 1: csr, 2: npz. * @param ispin The spin value, starting from 0. - * @param append A boolean indicating whether append the data to one file or create a new file. + * @param numbered Whether the ionic step is embedded in the filename as a g{step} suffix + * (true for out_app_flag == 2). * @param istep The ION step (default: -1), starting from 0. * @return The generated filename as a string. */ std::string dmr_gen_fname(const int out_type, const int ispin, - const bool append = true, + const bool numbered = false, const int istep = -1); /** @@ -33,6 +34,8 @@ std::string dmr_gen_fname(const int out_type, * @param precision Control the ouptut precision * @param dm_serial A pointer to the Hamiltonian container. * @param istep The current step number. + * @param append Whether to append to the existing file. The first written step + * (istep <= 0) always truncates. */ void write_dmr_csr(std::string& fname, const UnitCell *ucell, @@ -40,7 +43,8 @@ void write_dmr_csr(std::string& fname, hamilt::HContainer* dm_serial, const int istep, const int ispin, - const int nspin); + const int nspin, + const bool append); /** * Writes DMR to a file. @@ -49,20 +53,21 @@ void write_dmr_csr(std::string& fname, * @param ucell stands for unitcell * @param precision Control the output precision * @param paraV The parallel 2D object. - * @param out_type The output file type. 1: csr, 2: npz. - * @param sparse Whether output the sparse DM. - * @param ispin The spin index, starting from 0. - * @param append Whether to append the data to an existing file or create a new file. The file name is related to this - * flag. + * @param out_app_flag Output mode: 0 = fixed filename, overwritten at every write; 1 = fixed filename, + * appended; 2 = per-step g{step} filename. + * @param iat2iwt The mapping from atom index to the first local orbital index. + * @param nat The number of atoms. + * @param dmr_dir The directory the DM(R) files are written into. * @param istep The ION step, starting from 0. */ void write_dmr(const std::vector*> dmr, const UnitCell *ucell, const int precision, const Parallel_2D& paraV, - const bool append, + const int out_app_flag, const int* iat2iwt, const int nat, + const std::string& dmr_dir, const int istep, std::ofstream& ofs_running); } // namespace ModuleIO diff --git a/source/source_io/module_dos/cal_dos.cpp b/source/source_io/module_dos/cal_dos.cpp index 7975c5e6ded..4305faa4b98 100644 --- a/source/source_io/module_dos/cal_dos.cpp +++ b/source/source_io/module_dos/cal_dos.cpp @@ -108,7 +108,7 @@ bool ModuleIO::cal_dos(const int& is, // index for spin const ModuleBase::matrix& ekb, // energy for each k point and each band const ModuleBase::matrix& wg, // weight of k-points and bands const int istep, // ionic step - const bool out_app_flag, + const int out_app_flag, const int bndpar) { ModuleBase::TITLE("ModuleIO", "cal_dos"); @@ -117,7 +117,8 @@ bool ModuleIO::cal_dos(const int& is, // index for spin if (GlobalV::MY_RANK == 0) { - if(out_app_flag==true) + const bool append_step = (out_app_flag == 1) && (istep > 0); + if (append_step) { ofs_dos.open(fn.c_str(), std::ios::app); } diff --git a/source/source_io/module_dos/cal_dos.h b/source/source_io/module_dos/cal_dos.h index d133c4e9423..e8f305b685b 100644 --- a/source/source_io/module_dos/cal_dos.h +++ b/source/source_io/module_dos/cal_dos.h @@ -37,7 +37,7 @@ namespace ModuleIO const ModuleBase::matrix &ekb, //store energy for each k point and each band const ModuleBase::matrix &wg, //weight of (kpoint,bands)) const int istep_in, // ionic_step - const bool out_app_flag, // whether to append output + const int out_app_flag, // 0: no append, 1: append, 2: subdirectory const int bndpar); // number of band parallel groups } diff --git a/source/source_io/module_dos/cal_ldos.cpp b/source/source_io/module_dos/cal_ldos.cpp index 63f0e2c3f5b..5e8e14de0d9 100644 --- a/source/source_io/module_dos/cal_ldos.cpp +++ b/source/source_io/module_dos/cal_ldos.cpp @@ -102,6 +102,7 @@ void Cal_ldos::cal_ldos_lcao( 0, two_fermi, false, + false, ofs_running, "local DOS"); @@ -230,7 +231,7 @@ void stm_mode_pw(const elecstate::ElecStatePW>* pelec, << ".cube"; const int precision = out_ldos_precision; - ModuleIO::write_vdata_palgrid(pgrid, ldos.data(), 0, nspin, 0, fn.str(), 0, &ucell, precision, 0, two_fermi, false, ofs_running, "local DOS"); + ModuleIO::write_vdata_palgrid(pgrid, ldos.data(), 0, nspin, 0, fn.str(), 0, &ucell, precision, 0, two_fermi, false, false, ofs_running, "local DOS"); } } diff --git a/source/source_io/module_dos/cal_pdos_gamma.cpp b/source/source_io/module_dos/cal_pdos_gamma.cpp index 3b9ee1b1dd6..d042fb6ede5 100644 --- a/source/source_io/module_dos/cal_pdos_gamma.cpp +++ b/source/source_io/module_dos/cal_pdos_gamma.cpp @@ -23,6 +23,7 @@ void ModuleIO::cal_pdos( const double& dos_edelta_ev, const double& bcoeff, const int istep, + const int out_app_flag, const int nlocal, const int nspin, const std::string& global_out_dir) @@ -148,7 +149,7 @@ void ModuleIO::cal_pdos( if (GlobalV::MY_RANK == 0) { write_pdos_text(ucell, pdos.data(), nspin, nlocal, npoints, - emin, dos_edelta_ev, global_out_dir, istep); + emin, dos_edelta_ev, global_out_dir, istep, out_app_flag); ModuleIO::write_orb_info(&ucell); } } diff --git a/source/source_io/module_dos/cal_pdos_gamma.h b/source/source_io/module_dos/cal_pdos_gamma.h index e5122464262..0af1fca0540 100644 --- a/source/source_io/module_dos/cal_pdos_gamma.h +++ b/source/source_io/module_dos/cal_pdos_gamma.h @@ -25,6 +25,7 @@ namespace ModuleIO const double& dos_edelta_ev, const double& bcoeff, const int istep, + const int out_app_flag, const int nlocal, const int nspin, const std::string& global_out_dir); diff --git a/source/source_io/module_dos/cal_pdos_multik.cpp b/source/source_io/module_dos/cal_pdos_multik.cpp index 95ee8c7ba1c..219b5bf69cb 100644 --- a/source/source_io/module_dos/cal_pdos_multik.cpp +++ b/source/source_io/module_dos/cal_pdos_multik.cpp @@ -22,6 +22,7 @@ void ModuleIO::cal_pdos( const double& dos_edelta_ev, const double& bcoeff, const int istep, + const int out_app_flag, const int nlocal, const int nspin, const std::string& global_out_dir) @@ -188,7 +189,7 @@ void ModuleIO::cal_pdos( if (GlobalV::MY_RANK == 0) { write_pdos_text(ucell, pdos.data(), nspin, nlocal, npoints, - emin, dos_edelta_ev, global_out_dir, istep); + emin, dos_edelta_ev, global_out_dir, istep, out_app_flag); ModuleIO::write_orb_info(&ucell); } } diff --git a/source/source_io/module_dos/cal_pdos_multik.h b/source/source_io/module_dos/cal_pdos_multik.h index 110009becb4..a4b194d967b 100644 --- a/source/source_io/module_dos/cal_pdos_multik.h +++ b/source/source_io/module_dos/cal_pdos_multik.h @@ -26,6 +26,7 @@ namespace ModuleIO const double& dos_edelta_ev, const double& bcoeff, const int istep, + const int out_app_flag, const int nlocal, const int nspin, const std::string& global_out_dir); diff --git a/source/source_io/module_dos/unittests/test_cal_dos.cpp b/source/source_io/module_dos/unittests/test_cal_dos.cpp index abcf942bad4..82c99219112 100644 --- a/source/source_io/module_dos/unittests/test_cal_dos.cpp +++ b/source/source_io/module_dos/unittests/test_cal_dos.cpp @@ -37,7 +37,7 @@ TEST_F(DosTest,Dos) EXPECT_EQ(dosp.is,0); const int istep = 1; - const bool out_app_flag = false; + const int out_app_flag = 0; const int bndpar = 1; ModuleIO::cal_dos(dosp.is, dosp.fa, @@ -124,7 +124,7 @@ TEST_F(DosTest,DosW1) GlobalV::ofs_warning.open("warning1.log"); const int istep = 1; - const bool out_app_flag = false; + const int out_app_flag = 0; const int bndpar = 1; EXPECT_NO_THROW(ModuleIO::cal_dos(dosp.is, dosp.fa, @@ -171,7 +171,7 @@ TEST_F(DosTest,DosW2) GlobalV::ofs_warning.open("warning2.log"); const int istep = 1; - const bool out_app_flag = false; + const int out_app_flag = 0; const int bndpar = 1; EXPECT_NO_THROW(ModuleIO::cal_dos(dosp.is, dosp.fa, @@ -207,6 +207,92 @@ TEST_F(DosTest,DosW2) #endif } +// --------------------------------------------------------------------------- +// out_app_flag = 1: cal_dos appends successive records to the same file. +// --------------------------------------------------------------------------- +TEST_F(DosTest, Mode1Appends) +{ + const std::string fname = "doss1_mode1.txt"; + DosPrepare dosp(0, fname, 0.005, 18, -6, 0.07, + dos_test_data::nks, dos_test_data::nkstot, dos_test_data::nbands); + dosp.set_isk(); + dosp.set_wk(); + dosp.set_istate_info(); + + const int out_app_flag = 1; + const int bndpar = 1; + + // First call: istep=0 -> "1 # ionic step". + ModuleIO::cal_dos(dosp.is, dosp.fa, dosp.de_ev, dosp.emax_ev, dosp.emin_ev, + dosp.bcoeff, dosp.nks, dosp.nkstot, dosp.wk, dosp.isk, + dosp.nbands, dosp.ekb, dosp.wg, + /*istep=*/0, out_app_flag, bndpar); + // Second call: istep=1 -> "2 # ionic step", appended. + ModuleIO::cal_dos(dosp.is, dosp.fa, dosp.de_ev, dosp.emax_ev, dosp.emin_ev, + dosp.bcoeff, dosp.nks, dosp.nkstot, dosp.wk, dosp.isk, + dosp.nbands, dosp.ekb, dosp.wg, + /*istep=*/1, out_app_flag, bndpar); + +#ifdef __MPI + if (GlobalV::MY_RANK == 0) +#endif + { + std::ifstream ifs(fname); + std::string str((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + // Two ionic step headers present. + EXPECT_THAT(str, testing::HasSubstr("1 # ionic step")); + EXPECT_THAT(str, testing::HasSubstr("2 # ionic step")); + remove(fname.c_str()); + } +} + +// --------------------------------------------------------------------------- +// out_app_flag = 2: same overwrite behaviour as mode 0 (file is truncated each +// call; naming is handled by the caller write_dos_* which adds g{N}). +// --------------------------------------------------------------------------- +TEST_F(DosTest, Mode2Overwrites) +{ + const std::string fname = "doss1_mode2.txt"; + DosPrepare dosp(0, fname, 0.005, 18, -6, 0.07, + dos_test_data::nks, dos_test_data::nkstot, dos_test_data::nbands); + dosp.set_isk(); + dosp.set_wk(); + dosp.set_istate_info(); + + const int out_app_flag = 2; + const int bndpar = 1; + + // First call writes istep 1, second overwrites with istep 2. + ModuleIO::cal_dos(dosp.is, dosp.fa, dosp.de_ev, dosp.emax_ev, dosp.emin_ev, + dosp.bcoeff, dosp.nks, dosp.nkstot, dosp.wk, dosp.isk, + dosp.nbands, dosp.ekb, dosp.wg, + /*istep=*/0, out_app_flag, bndpar); + ModuleIO::cal_dos(dosp.is, dosp.fa, dosp.de_ev, dosp.emax_ev, dosp.emin_ev, + dosp.bcoeff, dosp.nks, dosp.nkstot, dosp.wk, dosp.isk, + dosp.nbands, dosp.ekb, dosp.wg, + /*istep=*/1, out_app_flag, bndpar); + +#ifdef __MPI + if (GlobalV::MY_RANK == 0) +#endif + { + std::ifstream ifs(fname); + std::string str((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + // Only the last ionic step header must be present. + EXPECT_THAT(str, testing::HasSubstr("2 # ionic step")); + // A simple count check: "1 # ionic step" must appear only inside + // the DOS grid lines as a value, not as a header. Check that the + // header for step 1 is absent by verifying it is NOT found as a + // standalone header line. + EXPECT_THAT(str, testing::Not(testing::HasSubstr("\n1 # ionic step"))); + remove(fname.c_str()); + } +} + #ifdef __MPI int main(int argc, char **argv) { diff --git a/source/source_io/module_dos/unittests/test_write_dos_pw.cpp b/source/source_io/module_dos/unittests/test_write_dos_pw.cpp index 384af45ac63..5e719bfe740 100644 --- a/source/source_io/module_dos/unittests/test_write_dos_pw.cpp +++ b/source/source_io/module_dos/unittests/test_write_dos_pw.cpp @@ -67,7 +67,7 @@ TEST_F(DosPWTest,Dos1) const bool dos_setemax = true; const bool dos_setemin = true; const bool two_fermi = false; - const bool out_app_flag = false; + const int out_app_flag = 0; const int bndpar = 1; const std::string global_out_dir = "./"; @@ -101,14 +101,14 @@ TEST_F(DosPWTest,Dos1) { #endif std::ifstream ifs; - ifs.open("doss1g1.txt"); + ifs.open("doss1.txt"); std::string str((std::istreambuf_iterator(ifs)),std::istreambuf_iterator()); EXPECT_THAT(str, testing::HasSubstr("4801 # number of points")); EXPECT_THAT(str, testing::HasSubstr(" -5.000000 0.500000 0.500000")); EXPECT_THAT(str, testing::HasSubstr(" 18.000000 0.000000 8.000000")); EXPECT_THAT(str, testing::HasSubstr("2.849588")); ifs.close(); - remove("doss1g1.txt"); + remove("doss1.txt"); #ifdef __MPI } #endif @@ -144,7 +144,7 @@ TEST_F(DosPWTest,Dos2) const int nspin = 1; const int out_dos = 1; const bool two_fermi = false; - const bool out_app_flag = false; + const int out_app_flag = 0; const int bndpar = 1; const std::string global_out_dir = "./"; const bool dos_setemax = false; @@ -180,7 +180,7 @@ TEST_F(DosPWTest,Dos2) { #endif std::ifstream ifs; - ifs.open("doss1g1.txt"); + ifs.open("doss1.txt"); std::string str1((std::istreambuf_iterator(ifs)),std::istreambuf_iterator()); EXPECT_THAT(str1, testing::HasSubstr("2526 # number of points")); EXPECT_THAT(str1, testing::HasSubstr(" -5.002500 0.500000 0.500000")); @@ -222,7 +222,7 @@ TEST_F(DosPWTest,Dos2) } EXPECT_EQ(row_count, declared_points); ifs.close(); - remove("doss1g1.txt"); + remove("doss1.txt"); #ifdef __MPI } #endif diff --git a/source/source_io/module_dos/unittests/test_write_pdos_text.cpp b/source/source_io/module_dos/unittests/test_write_pdos_text.cpp index 00681a3cb5f..add0fa08007 100644 --- a/source/source_io/module_dos/unittests/test_write_pdos_text.cpp +++ b/source/source_io/module_dos/unittests/test_write_pdos_text.cpp @@ -93,7 +93,7 @@ TEST_F(WritePdosTextTest, BasicOutput) } ModuleIO::write_pdos_text(*ucell, &pdos, nspin, nlocal, npoints, - emin, dos_edelta_ev, out_dir, istep); + emin, dos_edelta_ev, out_dir, istep, 0); const std::string str = read_file("./pdoss1.txt"); @@ -113,8 +113,9 @@ TEST_F(WritePdosTextTest, BasicOutput) remove("test_write_pdos_text.log"); } -// istep >= 0 appends g{istep+1} to the file name. -TEST_F(WritePdosTextTest, IstepFileName) +// mode 0 keeps a fixed name even when istep >= 0; the step only appears in +// the record header. +TEST_F(WritePdosTextTest, Mode0FixedNameWithStep) { GlobalV::ofs_running.open("test_write_pdos_text.log"); @@ -127,12 +128,12 @@ TEST_F(WritePdosTextTest, IstepFileName) pdos.zero_out(); ModuleIO::write_pdos_text(*ucell, &pdos, nspin, nlocal, npoints, - emin, dos_edelta_ev, "./", 2); + emin, dos_edelta_ev, "./", 2, 0); - const std::string str = read_file("./pdoss1g3.txt"); + const std::string str = read_file("./pdoss1.txt"); EXPECT_THAT(str, testing::HasSubstr("# istep: 3")); - remove("./pdoss1g3.txt"); + remove("./pdoss1.txt"); GlobalV::ofs_running.close(); remove("test_write_pdos_text.log"); } @@ -163,7 +164,7 @@ TEST_F(WritePdosTextTest, SpinPolarized) } ModuleIO::write_pdos_text(*ucell, pdos_arr, nspin, nlocal, npoints, - emin, dos_edelta_ev, "./", -1); + emin, dos_edelta_ev, "./", -1, 0); const std::string s1 = read_file("./pdoss1.txt"); const std::string s2 = read_file("./pdoss2.txt"); @@ -177,3 +178,67 @@ TEST_F(WritePdosTextTest, SpinPolarized) GlobalV::ofs_running.close(); remove("test_write_pdos_text.log"); } + +// --------------------------------------------------------------------------- +// out_app_flag = 1: fixed-name files, successive calls append records. +// --------------------------------------------------------------------------- +TEST_F(WritePdosTextTest, Mode1FixedNameAppend) +{ + GlobalV::ofs_running.open("test_write_pdos_text.log"); + + const int nspin = 1; + const int nlocal = 4; + const int npoints = 2; + const double emin = 0.0; + const double dos_edelta_ev = 1.0; + const int out_app_flag = 1; + + ModuleBase::matrix pdos(nlocal, npoints); + pdos.zero_out(); + + // First call: istep=0, mode 1 -> fixed name pdoss1.txt. + ModuleIO::write_pdos_text(*ucell, &pdos, nspin, nlocal, npoints, + emin, dos_edelta_ev, "./", /*istep=*/0, out_app_flag); + // Second call: istep=1, mode 1 -> append to same file. + ModuleIO::write_pdos_text(*ucell, &pdos, nspin, nlocal, npoints, + emin, dos_edelta_ev, "./", /*istep=*/1, out_app_flag); + + const std::string str = read_file("./pdoss1.txt"); + // Two istep headers present -> two appended records. + EXPECT_THAT(str, testing::HasSubstr("# istep: 1")); + EXPECT_THAT(str, testing::HasSubstr("# istep: 2")); + // g-suffixed file must NOT exist. + std::ifstream ifs("./pdoss1g1.txt"); + EXPECT_FALSE(ifs.good()); + + remove("./pdoss1.txt"); + GlobalV::ofs_running.close(); + remove("test_write_pdos_text.log"); +} + +// --------------------------------------------------------------------------- +// out_app_flag = 2: per-step g{step} filename (the only mode that adds it). +// --------------------------------------------------------------------------- +TEST_F(WritePdosTextTest, Mode2PerStepFile) +{ + GlobalV::ofs_running.open("test_write_pdos_text.log"); + + const int nspin = 1; + const int nlocal = 4; + const int npoints = 2; + const double emin = 0.0; + const double dos_edelta_ev = 1.0; + const int out_app_flag = 2; + ModuleBase::matrix pdos(nlocal, npoints); + pdos.zero_out(); + + ModuleIO::write_pdos_text(*ucell, &pdos, nspin, nlocal, npoints, + emin, dos_edelta_ev, "./", 3, out_app_flag); + + const std::string str = read_file("./pdoss1g4.txt"); + EXPECT_THAT(str, testing::HasSubstr("# istep: 4")); + + remove("./pdoss1g4.txt"); + GlobalV::ofs_running.close(); + remove("test_write_pdos_text.log"); +} diff --git a/source/source_io/module_dos/write_dos_lcao.cpp b/source/source_io/module_dos/write_dos_lcao.cpp index 926d94bd4e5..e068430f1e1 100644 --- a/source/source_io/module_dos/write_dos_lcao.cpp +++ b/source/source_io/module_dos/write_dos_lcao.cpp @@ -20,7 +20,7 @@ void write_dos_lcao( const double& dos_edelta_ev, const double& dos_scale, const double& bcoeff, - const bool out_app_flag, + const int out_app_flag, const int istep, const int nspin, const int nlocal, @@ -62,7 +62,9 @@ void write_dos_lcao( ss << global_out_dir << "doss" << is + 1; - if(istep>=0) + // Modes 0 and 1 reuse the fixed name (mode 0 overwrites, mode 1 appends + // a new DOS block per written step); only mode 2 keeps the g{N} suffix. + if(istep>=0 && out_app_flag==2) { ss << "g" << istep+1; } @@ -102,6 +104,7 @@ void write_dos_lcao( dos_edelta_ev, bcoeff, istep, + out_app_flag, nlocal, nspin, global_out_dir); @@ -125,7 +128,7 @@ template void write_dos_lcao( const double& dos_edelta_ev, const double& dos_scale, const double& bcoeff, - const bool out_app_flag, + const int out_app_flag, const int istep, const int nspin, const int nlocal, @@ -152,7 +155,7 @@ template void write_dos_lcao( const double& dos_edelta_ev, const double& dos_scale, const double& bcoeff, - const bool out_app_flag, + const int out_app_flag, const int istep, const int nspin, const int nlocal, diff --git a/source/source_io/module_dos/write_dos_lcao.h b/source/source_io/module_dos/write_dos_lcao.h index 0e9017149a4..25e8b6d14a6 100644 --- a/source/source_io/module_dos/write_dos_lcao.h +++ b/source/source_io/module_dos/write_dos_lcao.h @@ -28,7 +28,7 @@ namespace ModuleIO const double& dos_edelta_ev, // Delta energy const double& dos_scale, const double& bcoeff, - const bool out_app_flag, + const int out_app_flag, const int istep, const int nspin, const int nlocal, diff --git a/source/source_io/module_dos/write_dos_pw.cpp b/source/source_io/module_dos/write_dos_pw.cpp index 1ce707f59de..434a39c6f80 100644 --- a/source/source_io/module_dos/write_dos_pw.cpp +++ b/source/source_io/module_dos/write_dos_pw.cpp @@ -20,7 +20,7 @@ void ModuleIO::write_dos_pw( const bool dos_setemin, const double dos_emin_ev, const bool two_fermi, - const bool out_app_flag, + const int out_app_flag, const int bndpar, const std::string& global_out_dir, std::ofstream& ofs_running) @@ -54,7 +54,9 @@ void ModuleIO::write_dos_pw( std::stringstream ss; ss << global_out_dir << "doss" << is + 1; - if(istep_in>=0) + // Modes 0 and 1 reuse the fixed name (mode 0 overwrites, mode 1 appends + // a new DOS block per written step); only mode 2 keeps the g{N} suffix. + if(istep_in>=0 && out_app_flag==2) { ss << "g" << istep_in+1; } diff --git a/source/source_io/module_dos/write_dos_pw.h b/source/source_io/module_dos/write_dos_pw.h index 4b2417d8f42..2491c7d303a 100644 --- a/source/source_io/module_dos/write_dos_pw.h +++ b/source/source_io/module_dos/write_dos_pw.h @@ -27,7 +27,7 @@ namespace ModuleIO const bool dos_setemin, const double dos_emin_ev, const bool two_fermi, - const bool out_app_flag, + const int out_app_flag, const int bndpar, const std::string& global_out_dir, std::ofstream& ofs_running); diff --git a/source/source_io/module_dos/write_pdos_text.cpp b/source/source_io/module_dos/write_pdos_text.cpp index f0b3331dcf1..c3ded7e75c6 100644 --- a/source/source_io/module_dos/write_pdos_text.cpp +++ b/source/source_io/module_dos/write_pdos_text.cpp @@ -17,24 +17,38 @@ void ModuleIO::write_pdos_text( const double& emin, const double& dos_edelta_ev, const std::string& out_dir, - const int istep) + const int istep, + const int out_app_flag) { ModuleBase::TITLE("ModuleIO", "write_pdos_text"); // number of spin channels to write const int nspin0 = (nspin == 2) ? 2 : 1; + // Modes 0 and 1 reuse the fixed filename (mode 0 overwrites, mode 1 appends + // each ionic step); only mode 2 embeds the step number in the filename. + // Single-point SCF passes istep = -1 and truncates. + // The first written step (istep == 0) truncates so a fresh run does not + // keep records from a previous run; only later steps (istep > 0) append. + const int istep_file = (out_app_flag == 2) ? istep : -1; + const bool append_step = (out_app_flag == 1) && (istep > 0); + for (int is = 0; is < nspin0; ++is) { std::stringstream ss; ss << out_dir << "pdoss" << is + 1; - if (istep >= 0) + if (istep_file >= 0) { - ss << "g" << istep + 1; + ss << "g" << istep_file + 1; } ss << ".txt"; - std::ofstream ofs(ss.str().c_str()); + std::ios_base::openmode mode = std::ios::out; + if (append_step) + { + mode |= std::ios::app; + } + std::ofstream ofs(ss.str().c_str(), mode); // iw2m stores m as 0..2l, mapping to physical m values: // 0->0, 1->+1, 2->-1, 3->+2, 4->-2, 5->+3, 6->-3 diff --git a/source/source_io/module_dos/write_pdos_text.h b/source/source_io/module_dos/write_pdos_text.h index dcb9a184623..434c6f7ebe9 100644 --- a/source/source_io/module_dos/write_pdos_text.h +++ b/source/source_io/module_dos/write_pdos_text.h @@ -8,7 +8,7 @@ namespace ModuleIO { /// Write PDOS in plain text long-table format. -/// File name: pdoss{spin}g{geom}.txt +/// File name: pdoss{spin}g{geom}.txt (g{geom} omitted when out_app_flag == 1) /// Columns: energy(eV) atom species l z m pdos(1/eV) /// For nspin=4, the two spinor components are summed. void write_pdos_text( @@ -20,7 +20,8 @@ void write_pdos_text( const double& emin, const double& dos_edelta_ev, const std::string& out_dir, - const int istep); + const int istep, + const int out_app_flag); } diff --git a/source/source_io/module_elf/write_elf.cpp b/source/source_io/module_elf/write_elf.cpp index fe6164c0107..911c6cb47a1 100644 --- a/source/source_io/module_elf/write_elf.cpp +++ b/source/source_io/module_elf/write_elf.cpp @@ -18,6 +18,7 @@ void write_elf( const int& precision, const std::string& geom_block, const bool two_fermi, + const bool append, std::ofstream& ofs_running) { ModuleBase::timer::start("ModuleIO", "write_elf"); @@ -35,8 +36,14 @@ void write_elf( const int nrxx = rho_basis->nrxx; const int npw = rho_basis->npw; - assert(nrxx>0); - assert(npw>0); + // A rank may own zero real-space grid points (nrxx == 0) when the FFT + // grid is decomposed across more processes than it has z-slabs. Such + // empty partitions are legitimate: the per-grid-point loops below simply + // do not execute, but every rank must still call real2recip / recip2real + // because PW_Basis::gatherp_scatters performs an MPI_Alltoallv that + // requires all ranks in POOL_WORLD to participate. Skipping the FFT on + // empty ranks would deadlock the grid-owning ranks inside Alltoallv. + // The asserts are removed to allow nrxx == 0 / npw == 0 through. std::vector> elf(nspin_eff, std::vector(nrxx, 0.)); // 1) calculate the kinetic energy density of vW KEDF @@ -154,7 +161,12 @@ void write_elf( } } - // 4) output the ELF = 1 / (1 + F^2) to cube file + // 4) output the ELF = 1 / (1 + F^2) to cube file. + // Every rank, including empty-partition ranks with nrxx == 0, must + // enter this block so write_vdata_palgrid (which calls + // Parallel_Grid::reduce / MPI_Gatherv) sees a collective call from all + // ranks. Empty partitions pass a null pointer which Parallel_Grid::reduce + // accepts only when nrxx == 0 (sendcount 0 ignores the buffer). const double ef_tmp = 0.0; const int out_fermi = 0; @@ -175,6 +187,7 @@ void write_elf( out_fermi, two_fermi, false, + append, ofs_running, "electron localization function"); } @@ -200,6 +213,7 @@ void write_elf( out_fermi, two_fermi, false, + append, ofs_running, desc_elf); } @@ -238,6 +252,7 @@ void write_elf( out_fermi, two_fermi, false, + append, ofs_running, "electron localization function"); } diff --git a/source/source_io/module_elf/write_elf.h b/source/source_io/module_elf/write_elf.h index 970843e6526..e0396824d22 100644 --- a/source/source_io/module_elf/write_elf.h +++ b/source/source_io/module_elf/write_elf.h @@ -21,6 +21,7 @@ void write_elf( const int& precision, const std::string& geom_block, const bool two_fermi, + const bool append, std::ofstream& ofs_running); } diff --git a/source/source_io/module_energy/write_bands.cpp b/source/source_io/module_energy/write_bands.cpp index f796b19f07a..6d6880aa33f 100644 --- a/source/source_io/module_energy/write_bands.cpp +++ b/source/source_io/module_energy/write_bands.cpp @@ -7,6 +7,7 @@ #include "source_base/module_parallel/para_bridge.h" #include "source_base/timer.h" #include "source_base/tool_title.h" +#include "source_io/module_output/provenance_header.h" #include "source_io/module_output/spin_tag.h" #include "source_io/module_parameter/parameter.h" // mohan add 20250911 @@ -14,8 +15,28 @@ #include #endif +namespace +{ + +/** + * @brief build the STRU-style provenance header that precedes the k-point + * block of one ionic step in a band output file. + * + * @param istep zero-based ionic step index, printed one-based + * @param spin_tag label of the collinear spin channel, empty when nspin != 2 + * @return three comment lines: ABACUS version, local timestamp, ionic step + */ +std::string build_band_header(const int istep, const std::string& spin_tag) +{ + const int istep_one_based = istep + 1; + const std::string step_line = FmtCore::format("# IONIC STEP %d%s\n", istep_one_based, spin_tag.c_str()); + return ModuleIO::provenance_header(ModuleIO::local_timestamp()) + step_line; +} + +} // namespace + void ModuleIO::write_bands(const Input_para& inp, const ModuleBase::matrix& ekb, const K_Vectors& kv, - std::ofstream& ofs_running) + std::ofstream& ofs_running, const int istep) { // write band information to band.txt if (inp.out_band[0]) @@ -41,9 +62,11 @@ void ModuleIO::write_bands(const Input_para& inp, const ModuleBase::matrix& ekb, ss << ".txt"; const std::string eig_file = ss.str(); - const std::string spin_tag = ModuleIO::make_spin_tag(is, nspin0); - ofs_running << " Write eigenvalues" << spin_tag << " to file: " << eig_file << std::endl; + const std::string log_spin_tag = ModuleIO::make_spin_tag(is, nspin0); + ofs_running << " Write eigenvalues" << log_spin_tag << " to file: " << eig_file << std::endl; + const std::string header_spin_tag = ModuleIO::make_compact_spin_tag(is, nspin0); + const std::string header = build_band_header(istep, header_spin_tag); const double eshift = 0.0; nscf_bands(is, eig_file, @@ -51,7 +74,8 @@ void ModuleIO::write_bands(const Input_para& inp, const ModuleBase::matrix& ekb, eshift, inp.out_band[1], // precision global_ekb, - kv); + kv, + header); } } } @@ -62,7 +86,8 @@ void ModuleIO::nscf_bands(const int& is, const double& fermie, const int& precision, const ModuleBase::matrix& ekb, - const K_Vectors& kv) + const K_Vectors& kv, + const std::string& header) { ModuleBase::TITLE("ModuleIO", "nscf_bands"); ModuleBase::timer::start("ModuleIO", "nscf_bands"); @@ -89,7 +114,7 @@ void ModuleIO::nscf_bands(const int& is, const int band_group = Parallel::make_band_world().rank(); if (GlobalV::MY_RANK == 0) { - if (PARAM.inp.out_app_flag == true) + if (PARAM.inp.out_app_flag == 1) { // do nothing } @@ -125,6 +150,10 @@ void ModuleIO::nscf_bands(const int& is, if (kv.para_k.rank_in_pool == 0 && band_group == 0) { std::ofstream ofs(eig_file.c_str(), std::ios::app); + if (ik == 0) + { + ofs << header; + } ofs << FmtCore::format("%4d", ik + 1); int width = precision + 4; std::string fmtstr = " %." + std::to_string(precision) + "f"; @@ -149,7 +178,7 @@ void ModuleIO::nscf_bands(const int& is, if (GlobalV::MY_RANK == 0) { - if (PARAM.inp.out_app_flag == true) + if (PARAM.inp.out_app_flag == 1) { ofs.open(eig_file.c_str(), std::ios::app); } @@ -171,6 +200,10 @@ void ModuleIO::nscf_bands(const int& is, const int spin_k = ik + is * nks_np; if (kv.isk[spin_k] == is) { + if (ik == 0) + { + ofs << header; + } ofs << FmtCore::format("%4d", ik + 1); int width = precision + 4; std::string fmtstr = " %." + std::to_string(precision) + "f"; diff --git a/source/source_io/module_energy/write_bands.h b/source/source_io/module_energy/write_bands.h index 09c2135aff3..39b912498e5 100644 --- a/source/source_io/module_energy/write_bands.h +++ b/source/source_io/module_energy/write_bands.h @@ -11,21 +11,36 @@ namespace ModuleIO { +/** + * @brief write the eigenvalues of every spin channel to band.txt (nspin=1/4) + * or to bands1.txt/bands2.txt (nspin=2). + * + * @param inp input parameters, out_band[0] enables the output and out_band[1] + * sets the number of printed decimals + * @param ekb eigenvalues of k points and bands + * @param kv klist + * @param ofs_running running log stream + * @param istep zero-based index of the current ionic step, printed one-based + * in the per-step provenance header + */ void write_bands(const Input_para& inp, const ModuleBase::matrix& ekb, const K_Vectors& kv, - std::ofstream& ofs_running); + std::ofstream& ofs_running, + const int istep); /** * @brief calculate the band structure * * @param is spin index is = 0 or 1 - * @param out_band_dir directory to save the band structure + * @param eig_file file to save the band structure * @param nband number of bands * @param fermie fermi energy * @param precision precision of the output * @param ekb eigenvalues of k points and bands * @param kv klist + * @param header provenance header written once in front of the k-point block; + * pass an empty string to omit it */ void nscf_bands(const int& is, const std::string &eig_file, @@ -33,7 +48,8 @@ void nscf_bands(const int& is, const double& fermie, const int& precision, const ModuleBase::matrix& ekb, - const K_Vectors& kv); + const K_Vectors& kv, + const std::string& header); } #endif diff --git a/source/source_io/module_energy/write_eband_terms.cpp b/source/source_io/module_energy/write_eband_terms.cpp index b67094449cc..5f30588ceca 100644 --- a/source/source_io/module_energy/write_eband_terms.cpp +++ b/source/source_io/module_energy/write_eband_terms.cpp @@ -1,16 +1,18 @@ #include "write_eband_terms.h" -#include "source_io/module_hs/vxc_op_mat.h" +#include "source_io/module_hs/vxc_op_tools.h" +#include "source_io/module_output/provenance_header.h" #include "source_io/module_parameter/parameter.h" #include "source_lcao/module_operator_lcao/ekinetic.h" #include "source_lcao/module_operator_lcao/nonlocal.h" +#include "source_lcao/module_operator_lcao/veff_lcao.h" #include "source_cell/module_neighbor/sltk_grid_driver.h" +#include "source_base/constants.h" #include "source_psi/psi.h" #include "source_cell/unitcell.h" #include "source_cell/klist.h" #include "source_estate/module_pot/potential_new.h" -#include "source_hamilt/module_xc/exx_info.h" #include "source_basis/module_nao/two_center_bundle.h" #include "source_basis/module_ao/parallel_orbitals.h" #include "source_estate/module_charge/charge.h" @@ -18,16 +20,16 @@ #include "source_hamilt/module_surchem/surchem.h" #include "source_basis/module_pw/pw_basis.h" -#ifdef __EXX -#include "source_lcao/module_operator_lcao/op_exx_lcao.h" -#endif +#include namespace ModuleIO { template void write_eband_terms(const int nspin, + const int istep, + const double etot, + const int my_rank, const int nbasis, - const int drank, const Parallel_Orbitals* pv, const psi::Psi& psi, const UnitCell& ucell, @@ -41,18 +43,14 @@ void write_eband_terms(const int nspin, const ModuleBase::matrix& wg, Grid_Driver& gd, const std::vector& orb_cutoff, - const TwoCenterBundle& two_center_bundle, - const Exx_Info& exx_info -#ifdef __EXX - , - std::vector>>>* Hexxd, - std::vector>>>>* Hexxc -#endif + const TwoCenterBundle& two_center_bundle ) { // 0. prepare const int& nbands = wg.nc; const int nspin0 = (nspin == 2) ? 2 : 1; + const double etot_eV = etot * ModuleBase::Ry_to_eV; + const std::string time_stamp = local_timestamp(); double etxc = 0.0; double vtxc = 0.0; @@ -68,6 +66,20 @@ void write_eband_terms(const int nspin, } }; + // Only rank 0 writes; the orbital energies above are reduced on all ranks. + auto write_term = [&](const std::vector>& e_orb, + const std::string& out_name, + const std::string& term_symbol) + { + if (my_rank != 0) + { + return; + } + const std::string header = build_eband_header(istep, etot_eV, term_symbol, time_stamp); + const std::string& out_dir = PARAM.globalv.global_out_dir; + write_orb_energy(kv, nspin0, nbands, e_orb, out_name, header, out_dir); + }; + auto all_band_energy = [&wg](const int ik, const std::vector& e_orb)->double { double e = 0; @@ -107,7 +119,7 @@ void write_eband_terms(const int nspin, cVc(kinetic_k_ao.get_hk(), &psi(ik, 0, 0), nbasis, nbands, *pv, p2d), p2d)); } - write_orb_energy(kv, nspin0, nbands, e_orb_kinetic, "kinetic", "", PARAM.globalv.global_out_dir); + write_term(e_orb_kinetic, "eband_kin.txt", "T"); } // 2. pp: local @@ -139,7 +151,7 @@ void write_eband_terms(const int nspin, e_orb_pp_local.emplace_back(orbital_energy(ik, nbands, cVc(v_pp_local_k_ao.get_hk(), &psi(ik, 0, 0), nbasis, nbands, *pv, p2d), p2d)); } - write_orb_energy(kv, nspin0, nbands, e_orb_pp_local, "vpp_local", "", PARAM.globalv.global_out_dir); + write_term(e_orb_pp_local, "eband_vl.txt", "V_loc"); } // 3. pp: nonlocal @@ -159,7 +171,7 @@ void write_eband_terms(const int nspin, e_orb_pp_nonlocal.emplace_back(orbital_energy(ik, nbands, cVc(v_pp_nonlocal_k_ao.get_hk(), &psi(ik, 0, 0), nbasis, nbands, *pv, p2d), p2d)); } - write_orb_energy(kv, nspin0, nbands, e_orb_pp_nonlocal, "vpp_nonlocal", "", PARAM.globalv.global_out_dir); + write_term(e_orb_pp_nonlocal, "eband_vnl.txt", "V_nonlocal"); } // 4. hartree @@ -192,77 +204,33 @@ void write_eband_terms(const int nspin, cVc(v_hartree_k_ao.get_hk(), &psi(ik, 0, 0), nbasis, nbands, *pv, p2d), p2d)); } for (auto& op : v_hartree_op) { delete op; } - write_orb_energy(kv, nspin0, nbands, e_orb_hartree, "vhartree", "", PARAM.globalv.global_out_dir); - } - - // 5. xc (including exx) - if (!PARAM.inp.out_mat_xc) // avoid duplicate output - { - bool cal_exx = exx_info.info_global.cal_exx; - write_Vxc(nspin, - nbasis, - drank, - pv, - psi, - ucell, - sf, - solvent, - rho_basis, - rhod_basis, - vloc, - chg, - kv, - orb_cutoff, - wg, - gd, - PARAM.inp.dft_plus_u, - PARAM.globalv.gamma_only_local, - PARAM.globalv.global_out_dir, - PARAM.inp.out_ndigits, - PARAM.inp.ks_solver, - cal_exx, - exx_info -#ifdef __EXX - , - Hexxd, - Hexxc -#endif - ); + write_term(e_orb_hartree, "eband_hart.txt", "V_H"); } + // NOTE: the XC term (eband_vxc.txt etc.) is written by write_Vxc from + // ctrl_runner_lcao.cpp for both out_mat_xc and out_eband, + // so it is intentionally not handled here. } // Explicit template instantiations template void write_eband_terms( - const int, const int, const int, const Parallel_Orbitals*, const psi::Psi&, + const int, const int, const double, const int, const int, const Parallel_Orbitals*, const psi::Psi&, const UnitCell&, Structure_Factor&, surchem&, const ModulePW::PW_Basis&, const ModulePW::PW_Basis&, const ModuleBase::matrix&, const Charge&, const K_Vectors&, const ModuleBase::matrix&, Grid_Driver&, - const std::vector&, const TwoCenterBundle&, const Exx_Info& -#ifdef __EXX - , std::vector>>>*, - std::vector>>>>* -#endif + const std::vector&, const TwoCenterBundle& ); template void write_eband_terms, double>( - const int, const int, const int, const Parallel_Orbitals*, const psi::Psi>&, + const int, const int, const double, const int, const int, const Parallel_Orbitals*, const psi::Psi>&, const UnitCell&, Structure_Factor&, surchem&, const ModulePW::PW_Basis&, const ModulePW::PW_Basis&, const ModuleBase::matrix&, const Charge&, const K_Vectors&, const ModuleBase::matrix&, Grid_Driver&, - const std::vector&, const TwoCenterBundle&, const Exx_Info& -#ifdef __EXX - , std::vector>>>*, - std::vector>>>>* -#endif + const std::vector&, const TwoCenterBundle& ); template void write_eband_terms, std::complex>( - const int, const int, const int, const Parallel_Orbitals*, const psi::Psi>&, + const int, const int, const double, const int, const int, const Parallel_Orbitals*, const psi::Psi>&, const UnitCell&, Structure_Factor&, surchem&, const ModulePW::PW_Basis&, const ModulePW::PW_Basis&, const ModuleBase::matrix&, const Charge&, const K_Vectors&, const ModuleBase::matrix&, Grid_Driver&, - const std::vector&, const TwoCenterBundle&, const Exx_Info& -#ifdef __EXX - , std::vector>>>*, - std::vector>>>>* -#endif + const std::vector&, const TwoCenterBundle& ); } // namespace ModuleIO diff --git a/source/source_io/module_energy/write_eband_terms.h b/source/source_io/module_energy/write_eband_terms.h index 54d980a8701..067b2966b1f 100644 --- a/source/source_io/module_energy/write_eband_terms.h +++ b/source/source_io/module_energy/write_eband_terms.h @@ -4,10 +4,6 @@ #include "source_base/matrix.h" #include "source_psi/psi.h" -#include -#include -#include -#include #include class Grid_Driver; @@ -18,36 +14,30 @@ class surchem; class Charge; class K_Vectors; class TwoCenterBundle; -struct Exx_Info; namespace ModulePW { class PW_Basis; } -#ifdef __EXX -namespace hamilt -{ -using TAC = std::pair>; -} -namespace RI -{ -template -class Tensor; -} -#endif - namespace ModuleIO { /// @brief Write the band-decomposed energy of each Hamiltonian term -/// (kinetic, local/nonlocal pp, Hartree, XC) in KS orbital representation. +/// (kinetic, local/nonlocal pp, Hartree) in KS orbital representation. +/// The XC term is handled by write_Vxc in ctrl_runner_lcao.cpp instead. +/// @param nspin Number of spin components (1, 2 or 4) +/// @param istep Zero-based ionic step index; written one-based in the header +/// @param etot Total energy of the current frame in Ry +/// @param my_rank World rank; only rank 0 writes the files /// @tparam TK K-point data type (double or std::complex) /// @tparam TR Real-space data type (double or std::complex) template void write_eband_terms(const int nspin, + const int istep, + const double etot, + const int my_rank, const int nbasis, - const int drank, const Parallel_Orbitals* pv, const psi::Psi& psi, const UnitCell& ucell, @@ -61,13 +51,7 @@ void write_eband_terms(const int nspin, const ModuleBase::matrix& wg, Grid_Driver& gd, const std::vector& orb_cutoff, - const TwoCenterBundle& two_center_bundle, - const Exx_Info& exx_info -#ifdef __EXX - , - std::vector>>>* Hexxd, - std::vector>>>>* Hexxc -#endif + const TwoCenterBundle& two_center_bundle ); } // namespace ModuleIO diff --git a/source/source_io/module_hs/angmom_op_mat.cpp b/source/source_io/module_hs/angmom_op_mat.cpp index 16f8dc7e44d..eb094f0c147 100644 --- a/source/source_io/module_hs/angmom_op_mat.cpp +++ b/source/source_io/module_hs/angmom_op_mat.cpp @@ -380,7 +380,8 @@ void ModuleIO::Angmom_op::calculate( const UnitCell& ucell, const int precision, const int rank, - const int istep) + const int istep, + const int out_app_flag) { ModuleBase::TITLE("Angmom_op", "calculate"); ModuleBase::timer::start("Angmom_op", "calculate"); @@ -392,9 +393,12 @@ void ModuleIO::Angmom_op::calculate( } std::ofstream ofout; const std::string dir = "xyz"; - /// @brief Filename suffix: if istep >= 0, append "g{istep+1}" (e.g., "g1") - /// to follow the out_freq_ion convention; otherwise no suffix. - const std::string step_suffix = (istep >= 0) ? "g" + std::to_string(istep + 1) : ""; + /// @brief Filename suffix: modes 0 and 1 reuse the fixed name (mode 0 + /// overwrites, mode 1 appends each written step); only mode 2 + /// carries "g{istep+1}" (e.g., "g1") following the out_freq_ion + /// convention. + const std::string step_suffix = (istep >= 0 && out_app_flag == 2) ? "g" + std::to_string(istep + 1) : ""; + const bool append_step = (out_app_flag == 1) && (istep > 0); const std::string title = "# it ia il iz im iRx iRy iRz jt ja jl jz jm Re[] Im[]\n" "# it: atomtype index of the first atom\n" "# ia: atomic index of the first atom within the atomtype\n" @@ -413,7 +417,12 @@ void ModuleIO::Angmom_op::calculate( for (char d : dir) { std::string fn = outdir + "l" + d + step_suffix + "_nao.txt"; - ofout.open(fn, std::ios::out); + std::ios_base::openmode mode = std::ios::out; + if (append_step) + { + mode |= std::ios::app; + } + ofout.open(fn, mode); ofout << title; this->kernel(&ofout, ucell, d, precision); ofout.close(); diff --git a/source/source_io/module_hs/angmom_op_mat.h b/source/source_io/module_hs/angmom_op_mat.h index c5000d68f32..c79a059ff46 100644 --- a/source/source_io/module_hs/angmom_op_mat.h +++ b/source/source_io/module_hs/angmom_op_mat.h @@ -226,19 +226,21 @@ namespace ModuleIO * filenames are fixed as l{x,y,z}[g{step}]_nao.txt) * @param outdir the output directory * @param ucell the unit cell object - * @param precision the precision of the output, default is 10 - * @param rank the MPI rank, default is 0 - * @param istep the ionic step index. If >= 0, the filename includes - * `g{istep+1}` (e.g., lxg1_nao.txt); if -1, the filename - * has no step suffix (e.g., lx_nao.txt). Default is -1 - * (single-step overwrite mode). + * @param precision the precision of the output + * @param rank the MPI rank + * @param istep the ionic step index; -1 for single-point runs + * @param out_app_flag append mode: with 1 the fixed filename + * l{x,y,z}_nao.txt is reused and each step appends; + * with 0 or 2 the filename carries `g{istep+1}` + * (e.g., lxg1_nao.txt) */ void calculate(const std::string& prefix, const std::string& outdir, const UnitCell& ucell, - const int precision = 10, - const int rank = 0, - const int istep = -1); + const int precision, + const int rank, + const int istep, + const int out_app_flag); private: std::ofstream fallback_ofs_; diff --git a/source/source_io/module_hs/dhs_sparse_writer.cpp b/source/source_io/module_hs/dhs_sparse_writer.cpp index 5c4a049546f..4732734d1fa 100644 --- a/source/source_io/module_hs/dhs_sparse_writer.cpp +++ b/source/source_io/module_hs/dhs_sparse_writer.cpp @@ -21,10 +21,9 @@ void ModuleIO::save_dH_sparse(const int& istep, const bool& binary, const std::string& fileflag, const int precision, - const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const int nspin, const int nlocal, std::ofstream& ofs_running) { @@ -67,8 +66,9 @@ void ModuleIO::save_dH_sparse(const int& istep, const int total_R_num = static_cast(all_R_coor_ptr.size()); int output_R_number = 0; - // Use istep for filename/append decisions; clamp only the header STEP value. - const int header_step = std::max(istep, 0); + // Use istep for filename/append decisions; clamp only the header STEP + // value, printed one-based per governance rule 22. + const int header_step = std::max(istep, 0) + 1; int spin_loop = 1; if (nspin == 2) { @@ -112,20 +112,24 @@ void ModuleIO::save_dH_sparse(const int& istep, } } - const bool md_no_append = (calculation == "md") && !out_app_flag; + // Modes 0 and 1 keep the fixed filename (mode 0 overwrites, mode 1 appends + // every written ionic step); only mode 2 embeds the one-based ionic step + // before "_nao". + const bool include_step = (istep >= 0) && (out_app_flag == 2); + for (auto& comp: comps) { for (int ispin = 0; ispin < 2; ++ispin) { - if (istep >= 0) + if (include_step) { - comp.fname[ispin] << (md_no_append ? global_matrix_dir : global_out_dir) + comp.fname[ispin] << global_matrix_dir << "d" << fileflag << "r" << comp.axis << "s" << (ispin + 1) << "g" << (istep + 1) << "_nao.csr"; } else { - comp.fname[ispin] << global_out_dir + comp.fname[ispin] << global_matrix_dir << "d" << fileflag << "r" << comp.axis << "s" << (ispin + 1) << "_nao.csr"; } @@ -134,7 +138,10 @@ void ModuleIO::save_dH_sparse(const int& istep, if (GlobalV::DRANK == 0) { - const bool open_in_append = (calculation == "md") && out_app_flag && istep >= 0; + // The first written ionic step (istep == 0) truncates the shared file; + // later steps append one complete record each. Single-point runs pass + // istep <= 0 and therefore always start a fresh file. + const bool open_in_append = (out_app_flag == 1) && istep > 0; for (auto& comp: comps) { const std::string label = std::string("dH") + comp.axis; @@ -149,13 +156,14 @@ void ModuleIO::save_dH_sparse(const int& istep, { mode |= std::ios::app; } - else if (!binary) - { - const char* matrix_name = (fileflag == "s") ? "dS/dR" : "dH/dR"; - const std::string spin_tag = ModuleIO::make_spin_tag(ispin, nspin); - ofs_running << " Write " << matrix_name << " (" << comp.axis << " component)" << spin_tag - << " matrix in NAO basis to file: " << comp.fname[ispin].str() << std::endl; - } + + // Announce every written file, including appended multi-step + // records and binary output, so the running log tracks each write. + const char* matrix_name = (fileflag == "s") ? "dS/dR" : "dH/dR"; + const std::string spin_tag = ModuleIO::make_spin_tag(ispin, nspin); + ofs_running << " Write " << matrix_name << " (" << comp.axis << " component)" << spin_tag + << " matrix in NAO basis to file: " << comp.fname[ispin].str() << std::endl; + comp.ofs[ispin].open(comp.fname[ispin].str().c_str(), mode); detail::check_output_file_open(comp.ofs[ispin], comp.fname[ispin].str(), "ModuleIO::save_dH_sparse"); diff --git a/source/source_io/module_hs/dhs_sparse_writer.h b/source/source_io/module_hs/dhs_sparse_writer.h index 8aab9c8007e..59677d8f6ce 100644 --- a/source/source_io/module_hs/dhs_sparse_writer.h +++ b/source/source_io/module_hs/dhs_sparse_writer.h @@ -17,10 +17,9 @@ void save_dH_sparse(const int& istep, const bool& binary, const std::string& fileflag, const int precision, - const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const int nspin, const int nlocal, std::ofstream& ofs_running); diff --git a/source/source_io/module_hs/hs_sparse_io.cpp b/source/source_io/module_hs/hs_sparse_io.cpp index f645893164e..1b4d3ff6bbb 100644 --- a/source/source_io/module_hs/hs_sparse_io.cpp +++ b/source/source_io/module_hs/hs_sparse_io.cpp @@ -33,7 +33,10 @@ void open_sparse_file(std::ofstream& ofs, const ModuleIO::SparseWriteOptions& op { mode |= std::ios::binary; } - const bool append_on_restart = (options.calculation == "md") && options.out_app_flag && options.istep >= 0; + // Append from the second ionic step onward in append mode; the first + // step (istep == 0) truncates so a rerun does not retain prior frames. + // Single-point SCF passes istep = -1 and also starts a fresh file. + const bool append_on_restart = (options.out_app_flag == 1) && (options.istep > 0); if (append_on_restart) { mode |= std::ios::app; @@ -51,7 +54,9 @@ void write_sparse_header(std::ofstream& ofs, const int nlocal, const int output_R_number) { - const int step = std::max(options.istep, 0); + // ionic step printed one-based per governance rule 22, consistent + // with the g{N} filename suffix (g1 holds the first ionic step) + const int step = std::max(options.istep, 0) + 1; if (options.binary) { ofs.write(reinterpret_cast(&step), sizeof(int)); diff --git a/source/source_io/module_hs/hs_sparse_io.h b/source/source_io/module_hs/hs_sparse_io.h index d8bdb089989..3371e78628f 100644 --- a/source/source_io/module_hs/hs_sparse_io.h +++ b/source/source_io/module_hs/hs_sparse_io.h @@ -31,7 +31,7 @@ struct SparseWriteOptions // Runtime flags that decide file-open mode (append on md restart). // Must be provided explicitly by the caller instead of reading PARAM. std::string calculation; - bool out_app_flag = false; + int out_app_flag = 0; }; template diff --git a/source/source_io/module_hs/hsk_writer.cpp b/source/source_io/module_hs/hsk_writer.cpp index 263635b573b..38e8ec7ec5f 100644 --- a/source/source_io/module_hs/hsk_writer.cpp +++ b/source/source_io/module_hs/hsk_writer.cpp @@ -14,7 +14,7 @@ void ModuleIO::write_hsk( hamilt::Hamilt* p_hamilt, const Parallel_Orbitals &pv, const bool gamma_only, - const bool out_app_flag, + const int out_app_flag, const int istep, const int out_type, const int precision, @@ -40,6 +40,10 @@ void ModuleIO::write_hsk( { p_hamilt->updateHk(ik); const bool binary = (out_type == 2); + // save_mat takes a Boolean append flag: only mode 1 appends, and only + // from the second ionic step onward (istep > 0); modes 0 and 2 overwrite + // their per-step files. + const bool append_mat = (out_app_flag == 1) && (istep > 0); hamilt::MatrixBlock h_mat; hamilt::MatrixBlock s_mat; @@ -56,7 +60,7 @@ void ModuleIO::write_hsk( binary, precision, 1, - out_app_flag, + append_mat, h_fn, pv, drank, @@ -83,7 +87,7 @@ void ModuleIO::write_hsk( binary, precision, 1, - out_app_flag, + append_mat, s_fn, pv, drank, @@ -96,11 +100,11 @@ template void ModuleIO::write_hsk( const std::string&, const int, const int, const int, const std::vector&, const std::vector&, hamilt::Hamilt*, const Parallel_Orbitals&, - const bool, const bool, const int, const int, const int, + const bool, const int, const int, const int, const int, const int, const std::string&, const int, std::ofstream&); template void ModuleIO::write_hsk>( const std::string&, const int, const int, const int, const std::vector&, const std::vector&, hamilt::Hamilt>*, const Parallel_Orbitals&, - const bool, const bool, const int, const int, const int, + const bool, const int, const int, const int, const int, const int, const std::string&, const int, std::ofstream&); diff --git a/source/source_io/module_hs/hsk_writer.h b/source/source_io/module_hs/hsk_writer.h index af2bb6a8540..d80a5eb8839 100644 --- a/source/source_io/module_hs/hsk_writer.h +++ b/source/source_io/module_hs/hsk_writer.h @@ -25,7 +25,7 @@ void write_hsk(const std::string& global_out_dir, hamilt::Hamilt* p_hamilt, const Parallel_Orbitals& pv, const bool gamma_only, - const bool out_app_flag, + const int out_app_flag, const int istep, const int out_type, const int precision, diff --git a/source/source_io/module_hs/hsr_legacy.cpp b/source/source_io/module_hs/hsr_legacy.cpp index 0f1e37daa3e..229a7b49cf3 100644 --- a/source/source_io/module_hs/hsr_legacy.cpp +++ b/source/source_io/module_hs/hsr_legacy.cpp @@ -40,7 +40,7 @@ void ModuleIO::output_dSR(const int& istep, // mohan update 2024-04-01 const std::string fileflag_s = "s"; ModuleIO::save_dH_sparse(istep, pv, HS_Arrays, options.sparse_threshold, options.binary, - fileflag_s, options.precision, options.global_out_dir, + fileflag_s, options.precision, options.global_matrix_dir, options.calculation, options.out_app_flag, options.nspin, nlocal, ofs_running); @@ -86,7 +86,7 @@ void ModuleIO::output_dHR(const int& istep, // mohan update 2024-04-01 const std::string fileflag_h = "h"; ModuleIO::save_dH_sparse(istep, pv, HS_Arrays, options.sparse_threshold, options.binary, - fileflag_h, options.precision, options.global_out_dir, + fileflag_h, options.precision, options.global_matrix_dir, options.calculation, options.out_app_flag, options.nspin, nlocal, ofs_running); @@ -107,7 +107,7 @@ void ModuleIO::output_SR(Parallel_Orbitals& pv, const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const int nspin, std::ofstream& ofs_running) { @@ -174,17 +174,18 @@ void ModuleIO::output_TR(const int istep, ModuleBase::timer::start("ModuleIO", "output_TR"); std::stringstream sst; - const bool md_no_append = (options.calculation == "md") && !options.out_app_flag; - if (istep >= 0) + // Modes 0 and 1 keep the fixed name (mode 0 overwrites, mode 1 appends all + // ionic steps); only mode 2 embeds the one-based ionic step. + if (istep >= 0 && options.out_app_flag == 2) { // insert the 1-indexed ionic-step suffix right after the "tr" prefix, // i.e. tr_nao.csr -> trg{step+1}_nao.csr (aligned with lxg/rrg naming) const std::string base = TR_filename.substr(0, TR_filename.size() - std::string("_nao.csr").size()); - sst << (md_no_append ? options.global_matrix_dir : options.global_out_dir) << base << "g" << (istep + 1) << "_nao.csr"; + sst << options.global_matrix_dir << base << "g" << (istep + 1) << "_nao.csr"; } else { - sst << options.global_out_dir << TR_filename; + sst << options.global_matrix_dir << TR_filename; } ofs_running << " Write T(R) matrix in NAO basis to file: " << sst.str() << std::endl; sparse_format::cal_TR(ucell, pv, HS_Arrays, grid, two_center_bundle, orb, options.sparse_threshold); @@ -220,7 +221,7 @@ template void ModuleIO::output_SR(Parallel_Orbitals& pv, const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const int nspin, std::ofstream& ofs_running); template void ModuleIO::output_SR>(Parallel_Orbitals& pv, @@ -233,6 +234,6 @@ template void ModuleIO::output_SR>(Parallel_Orbitals& pv, const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const int nspin, std::ofstream& ofs_running); diff --git a/source/source_io/module_hs/hsr_legacy.h b/source/source_io/module_hs/hsr_legacy.h index 334374125e1..cddd93aed3f 100644 --- a/source/source_io/module_hs/hsr_legacy.h +++ b/source/source_io/module_hs/hsr_legacy.h @@ -29,7 +29,7 @@ struct MatROutputOptions std::string global_out_dir; std::string global_matrix_dir; std::string calculation; - bool out_app_flag = false; + int out_app_flag = 0; int nspin = 1; }; @@ -82,7 +82,7 @@ void output_SR(Parallel_Orbitals& pv, const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const int nspin, std::ofstream& ofs_running); diff --git a/source/source_io/module_hs/hsr_writer.cpp b/source/source_io/module_hs/hsr_writer.cpp index 10b5218fa69..dc09e5e1fdd 100644 --- a/source/source_io/module_hs/hsr_writer.cpp +++ b/source/source_io/module_hs/hsr_writer.cpp @@ -22,20 +22,20 @@ std::string ModuleIO::hsr_gen_fname(const std::string& prefix, const int ispin, - const bool append, + const bool numbered, const int istep) { - return hsr_gen_fname(prefix, ispin, append, istep, 1); + return hsr_gen_fname(prefix, ispin, numbered, istep, 1); } std::string ModuleIO::hsr_gen_fname(const std::string& prefix, const int ispin, - const bool append, + const bool numbered, const int istep, const int out_type) { const std::string extension = out_type == 2 ? ".dat" : ".csr"; - if (!append && istep >= 0) + if (numbered && istep >= 0) { return prefix + std::to_string(ispin + 1) + "g" + std::to_string(istep + 1) + "_nao" + extension; } @@ -45,15 +45,15 @@ std::string ModuleIO::hsr_gen_fname(const std::string& prefix, } } -std::string ModuleIO::sr_gen_fname(const bool append, const int istep) +std::string ModuleIO::sr_gen_fname(const bool numbered, const int istep) { - return sr_gen_fname(append, istep, 1); + return sr_gen_fname(numbered, istep, 1); } -std::string ModuleIO::sr_gen_fname(const bool append, const int istep, const int out_type) +std::string ModuleIO::sr_gen_fname(const bool numbered, const int istep, const int out_type) { const std::string extension = out_type == 2 ? ".dat" : ".csr"; - if (!append && istep >= 0) + if (numbered && istep >= 0) { return "srg" + std::to_string(istep + 1) + "_nao" + extension; } @@ -62,11 +62,11 @@ std::string ModuleIO::sr_gen_fname(const bool append, const int istep, const int std::string ModuleIO::dhr_gen_fname(const std::string& prefix, const int ispin, - const bool append, + const bool numbered, const int istep) { std::string fname = prefix + "rs" + std::to_string(ispin + 1); - if (!append && istep >= 0) + if (numbered && istep >= 0) { fname += "g" + std::to_string(istep + 1); } @@ -80,6 +80,7 @@ void ModuleIO::write_hcontainer_csr(const std::string& fname, const int precision, hamilt::HContainer* mat_serial, const int istep, + const bool append, const int ispin, const int nspin, const std::string& label, @@ -88,13 +89,17 @@ void ModuleIO::write_hcontainer_csr(const std::string& fname, const bool has_efermi) { std::ofstream ofs; - if (istep <= 0) + // The first written step of a run (or a single-point SCF) truncates the + // fixed file so a rerun does not keep stale records; later steps append + // only when the caller asks for it (mode 1). + const bool append_now = append && (istep > 0); + if (append_now) { - ofs.open(fname); + ofs.open(fname, std::ios::app); } else { - ofs.open(fname, std::ios::app); + ofs.open(fname); } if (!ofs.is_open()) { @@ -130,6 +135,17 @@ void ModuleIO::write_hcontainer_csr(const std::string& fname, namespace { +// Magic value ('H', 'S', 'R', 'B') that starts every versioned native binary +// CSR record. Unversioned records written before this magic was introduced +// start with a small ionic-step index instead, so the two layouts can be told +// apart from the first int alone. +const int hsr_binary_magic = 0x48535242; +// Version of the native binary CSR record layout. Version 2 stores the +// one-based ionic step after the magic and version fields; the unversioned +// layout that shipped in v3.11.0-beta9/beta10 had no magic and stored a +// zero-based step. +const int hsr_binary_format_version = 2; + template void write_native_value(std::ofstream& ofs, const T& value) { @@ -219,9 +235,12 @@ void ModuleIO::write_hcontainer_csr_binary(const std::string& fname, } std::sort(R_coordinates.begin(), R_coordinates.end()); - const int step = std::max(istep, 0); + // ionic step written one-based per governance rule 22 + const int step = std::max(istep, 0) + 1; const int nbasis = mat_serial->get_nbasis(); const int nR = static_cast(R_coordinates.size()); + write_native_value(ofs, hsr_binary_magic); + write_native_value(ofs, hsr_binary_format_version); write_native_value(ofs, step); write_native_value(ofs, nbasis); write_native_value(ofs, nR); @@ -283,7 +302,7 @@ void ModuleIO::write_hsr(const std::vector*>& hr_vec, const int out_type, const int precision, const Parallel_2D& paraV, - const bool append, + const int out_app_flag, const bool gamma_only, const int* iat2iwt, const int nat, @@ -298,6 +317,9 @@ void ModuleIO::write_hsr(const std::vector*>& hr_vec, } const int nspin = hr_vec.size(); assert(nspin > 0); + // Mode 1 appends to the fixed file; only mode 2 embeds the step in the name. + const bool append = (out_app_flag == 1); + const bool numbered = (out_app_flag == 2); const std::string representation_note = gamma_only ? "gamma-only folded matrix; stored R-space contributions are summed into R = (0, 0, 0)" @@ -321,7 +343,7 @@ void ModuleIO::write_hsr(const std::vector*>& hr_vec, if (GlobalV::MY_RANK == 0) { std::string fname = global_out_dir - + hsr_gen_fname("hrs", ispin, append, istep, out_type); + + hsr_gen_fname("hrs", ispin, numbered, istep, out_type); if (out_type == 2) { write_hcontainer_csr_binary(fname, &hr_serial, istep, append); @@ -330,7 +352,7 @@ void ModuleIO::write_hsr(const std::vector*>& hr_vec, { const double efermi_eV = eferm.get_efval(ispin) * ModuleBase::Ry_to_eV; write_hcontainer_csr( - fname, ucell, precision, &hr_serial, istep, ispin, nspin, "H", representation_note, efermi_eV, true); + fname, ucell, precision, &hr_serial, istep, append, ispin, nspin, "H", representation_note, efermi_eV, true); } const std::string spin_tag = ModuleIO::make_spin_tag(ispin, nspin); ofs_running << " Write H(R)" << spin_tag << " matrix in NAO basis to file: " << fname << std::endl; @@ -354,7 +376,7 @@ void ModuleIO::write_hsr(const std::vector*>& hr_vec, if (GlobalV::MY_RANK == 0) { std::string fname = global_out_dir - + sr_gen_fname(append, istep, out_type); + + sr_gen_fname(numbered, istep, out_type); if (out_type == 2) { write_hcontainer_csr_binary(fname, &sr_serial, istep, append); @@ -362,7 +384,7 @@ void ModuleIO::write_hsr(const std::vector*>& hr_vec, else { write_hcontainer_csr( - fname, ucell, precision, &sr_serial, istep, 0, 1, "S", representation_note, 0.0, false); + fname, ucell, precision, &sr_serial, istep, append, 0, 1, "S", representation_note, 0.0, false); } ofs_running << " Write S(R) matrix in NAO basis to file: " << fname << std::endl; } @@ -372,10 +394,10 @@ void ModuleIO::write_hsr(const std::vector*>& hr_vec, // Explicit instantiations template void ModuleIO::write_hcontainer_csr( const std::string&, const UnitCell*, const int, - hamilt::HContainer*, const int, const int, const int, const std::string&, const std::string&, const double, const bool); + hamilt::HContainer*, const int, const bool, const int, const int, const std::string&, const std::string&, const double, const bool); template void ModuleIO::write_hcontainer_csr>( const std::string&, const UnitCell*, const int, - hamilt::HContainer>*, const int, const int, const int, const std::string&, const std::string&, const double, const bool); + hamilt::HContainer>*, const int, const bool, const int, const int, const std::string&, const std::string&, const double, const bool); template void ModuleIO::write_hcontainer_csr_binary( const std::string&, hamilt::HContainer*, const int, const bool); @@ -386,11 +408,11 @@ template void ModuleIO::write_hsr( const std::vector*>&, const hamilt::HContainer*, const UnitCell*, const int, const int, const Parallel_2D&, - const bool, const bool, const int*, const int, const int, + const int, const bool, const int*, const int, const int, const std::string&, const elecstate::Efermi&, std::ofstream&); template void ModuleIO::write_hsr>( const std::vector>*>&, const hamilt::HContainer>*, const UnitCell*, const int, const int, const Parallel_2D&, - const bool, const bool, const int*, const int, const int, + const int, const bool, const int*, const int, const int, const std::string&, const elecstate::Efermi&, std::ofstream&); diff --git a/source/source_io/module_hs/hsr_writer.h b/source/source_io/module_hs/hsr_writer.h index 3fb8e6bc5a3..e7e56918f7b 100644 --- a/source/source_io/module_hs/hsr_writer.h +++ b/source/source_io/module_hs/hsr_writer.h @@ -24,31 +24,33 @@ namespace ModuleIO { /// Generate filename for spin-dependent HR output. +/// @param numbered whether the ionic step is embedded as a g{step} suffix (out_app_flag == 2) std::string hsr_gen_fname(const std::string& prefix, const int ispin, - const bool append, + const bool numbered, const int istep); /// Generate filename for spin-dependent HR output in the selected format. std::string hsr_gen_fname(const std::string& prefix, const int ispin, - const bool append, + const bool numbered, const int istep, const int out_type); /// Generate filename for spin-independent SR output. -std::string sr_gen_fname(const bool append, const int istep); +std::string sr_gen_fname(const bool numbered, const int istep); /// Generate filename for spin-independent SR output in the selected format. -std::string sr_gen_fname(const bool append, const int istep, const int out_type); +std::string sr_gen_fname(const bool numbered, const int istep, const int out_type); /// Generate filename for derivative matrices (dH/dR, dS/dR). std::string dhr_gen_fname(const std::string& prefix, const int ispin, - const bool append, + const bool numbered, const int istep); /// Write a single HContainer to CSR file with header. +/// @param append whether to append to an existing file; the first written step (istep <= 0) truncates /// @param efermi_eV the Fermi energy in eV for this spin channel /// @param has_efermi whether to append the Fermi energy to the spin-index line template @@ -57,6 +59,7 @@ void write_hcontainer_csr(const std::string& fname, const int precision, hamilt::HContainer* mat_serial, const int istep, + const bool append, const int ispin, const int nspin, const std::string& label, @@ -72,6 +75,8 @@ void write_hcontainer_csr_binary(const std::string& fname, const bool append); /// Write H(R) and S(R) in CSR format, unified with write_dmr interface. +/// @param out_app_flag output mode: 0 = fixed filename, overwritten; 1 = fixed filename, appended; +/// 2 = per-step g{step} filename. template void write_hsr(const std::vector*>& hr_vec, const hamilt::HContainer* sr, @@ -79,7 +84,7 @@ void write_hsr(const std::vector*>& hr_vec, const int out_type, const int precision, const Parallel_2D& paraV, - const bool append, + const int out_app_flag, const bool gamma_only, const int* iat2iwt, const int nat, diff --git a/source/source_io/module_hs/hterm_writer.cpp b/source/source_io/module_hs/hterm_writer.cpp index a4be8399b56..c8a6d6070f2 100644 --- a/source/source_io/module_hs/hterm_writer.cpp +++ b/source/source_io/module_hs/hterm_writer.cpp @@ -12,7 +12,6 @@ #include "source_hamilt/module_gint/gint_interface.h" #include "source_hamilt/module_hcontainer/hcontainer_funcs.h" #include "source_hamilt/module_hcontainer/output_hcontainer.h" -#include "source_lcao/module_operator_lcao/ekinetic.h" #include "source_lcao/module_operator_lcao/nonlocal.h" #include "source_lcao/module_operator_lcao/operator_fs_utils.h" #ifdef __EXX @@ -75,15 +74,17 @@ static void gather_and_write(const std::string& prefix, const int nspin, const int ispin, const int istep, - const bool append, + const int out_app_flag, const int* iat2iwt, const int nat, - const std::string& calculation, - const bool out_app_flag, - const std::string& global_out_dir, const std::string& global_matrix_dir, std::ofstream* ofs_running) { + // Mode 1 appends one frame per ionic step; the first step (istep == 0) + // truncates the fixed file so a rerun does not keep stale records. Only + // mode 2 embeds the step in the name. + const bool append = (out_app_flag == 1) && (istep > 0); + const bool numbered = (out_app_flag == 2); const int nbasis = hR.get_nbasis(); #ifdef __MPI Parallel_Orbitals serialV; @@ -94,16 +95,14 @@ static void gather_and_write(const std::string& prefix, if (GlobalV::MY_RANK == 0) #endif { - const bool md_no_append = (calculation == "md") && !out_app_flag; - const std::string& out_dir = md_no_append ? global_matrix_dir : global_out_dir; - const std::string fname = out_dir + hsr_gen_fname(prefix, ispin, append, istep); + const std::string fname = global_matrix_dir + hsr_gen_fname(prefix, ispin, numbered, istep); // H(R) component terms (T, V^NL, V^L, V^H, V^XC, V^EXX) are parts of // the Hamiltonian, not the full H(R): they do not carry the Fermi annotation const double no_efermi = 0.0; #ifdef __MPI - write_hcontainer_csr(fname, &ucell, 8, &hr_serial, istep, ispin, nspin, label, "", no_efermi, false); + write_hcontainer_csr(fname, &ucell, 8, &hr_serial, istep, append, ispin, nspin, label, "", no_efermi, false); #else - write_hcontainer_csr(fname, &ucell, 8, &hR, istep, ispin, nspin, label, "", no_efermi, false); + write_hcontainer_csr(fname, &ucell, 8, &hR, istep, append, ispin, nspin, label, "", no_efermi, false); #endif if (ofs_running != nullptr) { @@ -125,9 +124,10 @@ static void write_hk_common(hamilt::HContainer& hR, const int* iat2iwt, const int nat, const int nlocal, + const int precision, const bool gamma_only, const std::string& global_out_dir, - const bool out_app_flag, + const int out_app_flag, const std::string& ks_solver, const int drank) { @@ -157,9 +157,9 @@ static void write_hk_common(hamilt::HContainer& hR, hk_global.data(), nlocal, false, - 8, + precision, false, - out_app_flag, + append, fname, pv, drank, @@ -200,7 +200,7 @@ static void write_h_term(WriteHParams& params, const HTermSpec& spec) const int nlocal = params.nlocal; const bool gamma_only = params.gamma_only_local; const std::string& global_out_dir = params.global_out_dir; - const bool out_app_flag = params.out_app_flag; + const int out_app_flag = params.out_app_flag; const int nspin_out = (nspin == 2 ? 2 : 1); @@ -210,44 +210,20 @@ static void write_h_term(WriteHParams& params, const HTermSpec& spec) spec.build(hR_tmp, ispin); write_hk_common(hR_tmp, spec.k_prefix, ucell, pv, kv, nspin, istep, append, iat2iwt, nat, - nlocal, gamma_only, global_out_dir, out_app_flag, params.ks_solver, params.drank); + nlocal, params.precision, gamma_only, global_out_dir, out_app_flag, + params.ks_solver, params.drank); if (also_hR) { gather_and_write(spec.r_prefix, spec.r_label, spec.term_name, hR_tmp, ucell, pv, nspin, ispin, - istep, append, iat2iwt, nat, params.calculation, params.out_app_flag, - params.global_out_dir, params.global_matrix_dir, params.ofs_running); + istep, out_app_flag, iat2iwt, nat, + params.global_matrix_dir, params.ofs_running); } } ModuleBase::timer::end("ModuleIO", spec.timer_label); } -void write_h_t(WriteHParams& params) -{ - const UnitCell& ucell = *params.ucell; - const Grid_Driver& gd = *params.gd; - const Parallel_Orbitals& pv = *params.pv; - const TwoCenterBundle& two_center_bundle = *params.two_center_bundle; - const LCAO_Orbitals& orb = *params.orb; - const K_Vectors& kv = *params.kv; - const std::vector& orb_cutoff = orb.cutoffs(); - - HTermSpec spec; - spec.timer_label = "write_h_t"; - spec.k_prefix = "tk"; - spec.r_prefix = "t"; - spec.r_label = "T"; - spec.term_name = "kinetic"; - spec.build = [&](hamilt::HContainer& hR_tmp, int /*ispin*/) { - hamilt::EKinetic> - tmp_ekinetic(nullptr, kv.kvec_d, &hR_tmp, &ucell, orb_cutoff, &gd, - two_center_bundle.kinetic_orb.get()); - tmp_ekinetic.contributeHR(); - }; - write_h_term(params, spec); -} - void write_h_vnl(WriteHParams& params) { const UnitCell& ucell = *params.ucell; @@ -291,6 +267,10 @@ void write_h_vl(WriteHParams& params) spec.term_name = "local"; spec.build = [&](hamilt::HContainer& hR_tmp, int /*ispin*/) { setup_veff_hcontainer(hR_tmp, ucell, gd, pv, orb_cutoff); + // All ranks must enter cal_gint_vl so the Alltoall inside + // hr_gint_to_hR stays matched. A grid-parallel rank with no local + // real-space grid points (nrxx == 0) holds v_local == nullptr; + // cal_gint_vl then has no local biggrid and never dereferences it. ModuleGint::cal_gint_vl(v_local, &hR_tmp); }; write_h_term(params, spec); @@ -317,7 +297,14 @@ void write_h_vh(WriteHParams& params) spec.term_name = "hartree"; spec.build = [&](hamilt::HContainer& hR_tmp, int ispin) { setup_veff_hcontainer(hR_tmp, ucell, gd, pv, orb_cutoff); - ModuleGint::cal_gint_vl(&v_h(ispin, 0), &hR_tmp); + // All ranks must enter cal_gint_vl so the Alltoall inside + // hr_gint_to_hR stays matched. Under grid-parallel distribution a + // rank may own no real-space grid points (nrxx == 0), leaving v_h an + // empty matrix; pass nullptr there (cal_gint_vl then has no local + // biggrid and never dereferences it) instead of &v_h(ispin, 0), whose + // matrix::operator() would trip the ic < nc assertion. + const double* vh_ptr = (v_h.c != nullptr) ? &v_h(ispin, 0) : nullptr; + ModuleGint::cal_gint_vl(vh_ptr, &hR_tmp); }; write_h_term(params, spec); } @@ -354,7 +341,13 @@ void write_h_vxc(WriteHParams& params) spec.term_name = "xc"; spec.build = [&](hamilt::HContainer& hR_tmp, int ispin) { setup_veff_hcontainer(hR_tmp, ucell, gd, pv, orb_cutoff); - ModuleGint::cal_gint_vl(&v_xc(ispin, 0), &hR_tmp); + // All ranks must enter cal_gint_vl so the Alltoall inside + // hr_gint_to_hR stays matched. A grid-parallel rank with no local + // real-space grid points (nrxx == 0) holds an empty v_xc matrix; pass + // nullptr there (never dereferenced) instead of &v_xc(ispin, 0), + // whose matrix::operator() would trip the ic < nc assertion. + const double* vxc_ptr = (v_xc.c != nullptr) ? &v_xc(ispin, 0) : nullptr; + ModuleGint::cal_gint_vl(vxc_ptr, &hR_tmp); }; write_h_term(params, spec); } @@ -374,11 +367,12 @@ static void write_h_exx_impl(const UnitCell& ucell, const bool also_hR, const int npol, const int nlocal, + const int precision, const bool gamma_only, const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const std::string& ks_solver, const int drank, const Exx_Info& exx_info, @@ -397,12 +391,13 @@ static void write_h_exx_impl(const UnitCell& ucell, RI_2D_Comm::add_HexxR(ispin, alpha, Hexxs, pv, npol, hR_tmp, nullptr); write_hk_common(hR_tmp, "vexxk", ucell, pv, kv, nspin, istep, append, iat2iwt, nat, - nlocal, gamma_only, global_out_dir, out_app_flag, ks_solver, drank); + nlocal, precision, gamma_only, global_out_dir, out_app_flag, ks_solver, + drank); if (also_hR) { - gather_and_write("vexx", "V^EXX", "exx", hR_tmp, ucell, pv, nspin, ispin, istep, append, - iat2iwt, nat, calculation, out_app_flag, global_out_dir, global_matrix_dir, + gather_and_write("vexx", "V^EXX", "exx", hR_tmp, ucell, pv, nspin, ispin, istep, out_app_flag, + iat2iwt, nat, global_matrix_dir, ofs_running); } } @@ -413,7 +408,7 @@ void write_h_exx(WriteHParams& params, const Exx_Info& exx_info) ModuleBase::TITLE("ModuleIO", "write_h_exx"); ModuleBase::timer::start("ModuleIO", "write_h_exx"); - // Multi-k out_mat_h_exx is rejected upstream at the call site (setup_exx_h_params in + // Multi-k out_mat_exxk is rejected upstream at the call site (setup_exx_h_params in // ctrl_scf_lcao.cpp); this function is only reached on the gamma-only path. const UnitCell& ucell = *params.ucell; const Parallel_Orbitals& pv = *params.pv; @@ -431,7 +426,7 @@ void write_h_exx(WriteHParams& params, const Exx_Info& exx_info) if (params.exd != nullptr) { write_h_exx_impl(ucell, pv, params.exd, kv, nspin, istep, append, iat2iwt, nat, also_hR, - params.npol, params.nlocal, params.gamma_only_local, + params.npol, params.nlocal, params.precision, params.gamma_only_local, params.global_out_dir, params.global_matrix_dir, params.calculation, params.out_app_flag, params.ks_solver, params.drank, exx_info, params.ofs_running); @@ -442,7 +437,7 @@ void write_h_exx(WriteHParams& params, const Exx_Info& exx_info) if (params.exc != nullptr) { write_h_exx_impl(ucell, pv, params.exc, kv, nspin, istep, append, iat2iwt, nat, also_hR, - params.npol, params.nlocal, params.gamma_only_local, + params.npol, params.nlocal, params.precision, params.gamma_only_local, params.global_out_dir, params.global_matrix_dir, params.calculation, params.out_app_flag, params.ks_solver, params.drank, exx_info, params.ofs_running); diff --git a/source/source_io/module_hs/hterm_writer.h b/source/source_io/module_hs/hterm_writer.h index 7aa2f721fc6..6218fb96eb1 100644 --- a/source/source_io/module_hs/hterm_writer.h +++ b/source/source_io/module_hs/hterm_writer.h @@ -41,12 +41,13 @@ struct WriteHParams int nat = 0; bool also_hR = false; // H(k) is always written; H(R) (CSR) only when this is true int nlocal = 0; + int precision = 8; // text precision of the H(k) component matrices (second INPUT value) bool gamma_only_local = false; int npol = 1; bool domag = false; bool domag_z = false; int gga_grad = 0; // used by the XC term - bool out_app_flag = false; + int out_app_flag = 0; std::string calculation; std::string global_out_dir; std::string global_matrix_dir; @@ -65,11 +66,11 @@ struct WriteHParams #endif }; -// Individual Hamiltonian-term writers. write_h_t/vnl/vl/vh/vxc share a common +// Individual Hamiltonian-term writers. write_h_vnl/vl/vh/vxc share a common // build -> write H(k) -> optionally write H(R) skeleton (see hterm_writer.cpp); // the term-specific part is only how hR_tmp is filled. -void write_h_t(WriteHParams& params); - +// The kinetic T(k) output is handled by the out_mat_tk path in ctrl_scf_lcao, +// so there is no write_h_t here. void write_h_vnl(WriteHParams& params); void write_h_vl(WriteHParams& params); diff --git a/source/source_io/module_hs/output_mat_sparse.cpp b/source/source_io/module_hs/output_mat_sparse.cpp index 5179bd5477e..328056ee720 100644 --- a/source/source_io/module_hs/output_mat_sparse.cpp +++ b/source/source_io/module_hs/output_mat_sparse.cpp @@ -1,6 +1,7 @@ #include "output_mat_sparse.h" #include "pos_op_mat.h" +#include "source_base/fs_compat.h" #include "source_io/module_hs/hsr_legacy.h" #include "source_io/module_parameter/parameter.h" @@ -18,18 +19,25 @@ void output_mat_sparse(const MatSparseOutputOptions& options, const K_Vectors& kv, hamilt::Hamilt* p_ham, Plus_U_Base* p_dftu, + const std::string& global_out_dir, + const std::string& global_matrix_dir, + const std::string& calculation, + const int out_app_flag, + const int nspin, + const bool gamma_only_local, + const int npol, + const int nlocal, + const bool cal_force, std::ofstream& ofs_running) { - LCAO_HS_Arrays HS_Arrays; // store sparse arrays + // Mode-2 MAT/ subdirectory is created lazily at write time, only when at + // least one matrix output is actually requested. + if (options.out_mat_t || options.out_mat_dh || options.out_mat_ds || options.out_mat_r) + { + ModuleBase::ensure_directory(global_matrix_dir); + } - const std::string& global_out_dir = PARAM.globalv.global_out_dir; - const std::string& global_matrix_dir = PARAM.globalv.global_matrix_dir; - const std::string& calculation = PARAM.inp.calculation; - const bool out_app_flag = PARAM.inp.out_app_flag; - const int nspin = PARAM.inp.nspin; - const bool gamma_only_local = PARAM.globalv.gamma_only_local; - const int npol = PARAM.globalv.npol; - const int nlocal = PARAM.globalv.nlocal; + LCAO_HS_Arrays HS_Arrays; // store sparse arrays MatROutputOptions mat_R_options; mat_R_options.binary = options.binary; @@ -99,7 +107,6 @@ void output_mat_sparse(const MatSparseOutputOptions& options, Position_op r_matrix; r_matrix.binary = options.binary; r_matrix.sparse_threshold = options.sparse_threshold; - const bool cal_force = PARAM.inp.cal_force; r_matrix.init(ucell, pv, orb, cal_force, nlocal); r_matrix.out_rR(ucell, grid, @@ -128,6 +135,15 @@ template void output_mat_sparse(const MatSparseOutputOptions& options, const K_Vectors& kv, hamilt::Hamilt* p_ham, Plus_U_Base* p_dftu, + const std::string& global_out_dir, + const std::string& global_matrix_dir, + const std::string& calculation, + const int out_app_flag, + const int nspin, + const bool gamma_only_local, + const int npol, + const int nlocal, + const bool cal_force, std::ofstream& ofs_running); template void output_mat_sparse>(const MatSparseOutputOptions& options, @@ -141,6 +157,15 @@ template void output_mat_sparse>(const MatSparseOutputOptio const K_Vectors& kv, hamilt::Hamilt>* p_ham, Plus_U_Base* p_dftu, + const std::string& global_out_dir, + const std::string& global_matrix_dir, + const std::string& calculation, + const int out_app_flag, + const int nspin, + const bool gamma_only_local, + const int npol, + const int nlocal, + const bool cal_force, std::ofstream& ofs_running); } // namespace ModuleIO diff --git a/source/source_io/module_hs/output_mat_sparse.h b/source/source_io/module_hs/output_mat_sparse.h index 86e780758cc..ca25720902c 100644 --- a/source/source_io/module_hs/output_mat_sparse.h +++ b/source/source_io/module_hs/output_mat_sparse.h @@ -39,6 +39,15 @@ void output_mat_sparse(const MatSparseOutputOptions& options, const K_Vectors& kv, hamilt::Hamilt* p_ham, Plus_U_Base* p_dftu, + const std::string& global_out_dir, + const std::string& global_matrix_dir, + const std::string& calculation, + const int out_app_flag, + const int nspin, + const bool gamma_only_local, + const int npol, + const int nlocal, + const bool cal_force, std::ofstream& ofs_running); } // namespace ModuleIO diff --git a/source/source_io/module_hs/pos_op_mat.cpp b/source/source_io/module_hs/pos_op_mat.cpp index 04ec8d45e85..e37c7f1245e 100644 --- a/source/source_io/module_hs/pos_op_mat.cpp +++ b/source/source_io/module_hs/pos_op_mat.cpp @@ -89,7 +89,7 @@ void Position_op::out_rR(const UnitCell& ucell, const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const int nlocal, const int npol, std::ofstream& ofs_running) diff --git a/source/source_io/module_hs/pos_op_mat.h b/source/source_io/module_hs/pos_op_mat.h index 96a6513c23e..d76d71e0c3e 100644 --- a/source/source_io/module_hs/pos_op_mat.h +++ b/source/source_io/module_hs/pos_op_mat.h @@ -76,7 +76,7 @@ class Position_op const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - const bool out_app_flag, + const int out_app_flag, const int nlocal, const int npol, std::ofstream& ofs_running); diff --git a/source/source_io/module_hs/pos_op_writer.cpp b/source/source_io/module_hs/pos_op_writer.cpp index 15e1fa201ea..31a8da8d526 100644 --- a/source/source_io/module_hs/pos_op_writer.cpp +++ b/source/source_io/module_hs/pos_op_writer.cpp @@ -22,7 +22,7 @@ void PosOpWriter::out_lat_r(const UnitCell& ucell, const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - bool out_app_flag, + int out_app_flag, int nlocal, int npol, double sparse_threshold, @@ -216,19 +216,24 @@ void PosOpWriter::out_lat_r(const UnitCell& ucell, if (GlobalV::DRANK == 0) { std::stringstream ssr; - const bool md_no_append = (calculation == "md") && !out_app_flag; - if (step >= 0) + // Modes 0 and 1 keep the fixed filename (mode 0 overwrites, mode 1 + // appends all ionic steps); only mode 2 embeds the one-based step. + if (step >= 0 && out_app_flag == 2) { - ssr << (md_no_append ? global_matrix_dir : global_out_dir) << "rrg" << (step + 1) << "_nao.txt"; + ssr << global_matrix_dir << "rrg" << (step + 1) << "_nao.txt"; } else { - ssr << global_out_dir << "rr_nao.txt"; + ssr << global_matrix_dir << "rr_nao.txt"; } ofs_tem1.close(); - const bool open_in_append = (calculation == "md") && out_app_flag && step >= 0; - const int header_step = std::max(step, 0); + // Append from the second ionic step onward in append mode; the first + // step (step == 0) truncates so a rerun does not retain prior frames. + // Single-point SCF passes step = -1 and also starts a fresh file. + const bool open_in_append = (out_app_flag == 1) && (step > 0); + // ionic step printed one-based per governance rule 22 + const int header_step = std::max(step, 0) + 1; ModuleIO::detail::assemble_csr(ssr.str(), tem1.str(), header_step, diff --git a/source/source_io/module_hs/pos_op_writer.h b/source/source_io/module_hs/pos_op_writer.h index 1fd22bcfc81..d2996e43890 100644 --- a/source/source_io/module_hs/pos_op_writer.h +++ b/source/source_io/module_hs/pos_op_writer.h @@ -35,7 +35,7 @@ class PosOpWriter const std::string& global_out_dir, const std::string& global_matrix_dir, const std::string& calculation, - bool out_app_flag, + int out_app_flag, int nlocal, int npol, double sparse_threshold, diff --git a/source/source_io/module_hs/unittests/csr_test_helpers.h b/source/source_io/module_hs/unittests/csr_test_helpers.h index 5676d3bb5ef..2e97848a6e1 100644 --- a/source/source_io/module_hs/unittests/csr_test_helpers.h +++ b/source/source_io/module_hs/unittests/csr_test_helpers.h @@ -69,6 +69,8 @@ struct NativeDoubleRBlock struct NativeDoubleRecord { + int magic = 0; + int format_version = 0; int step = 0; int nbasis = 0; std::vector blocks; @@ -77,6 +79,8 @@ struct NativeDoubleRecord NativeDoubleRecord read_native_double_record(std::ifstream& ifs) { NativeDoubleRecord record; + record.magic = read_binary_value(ifs); + record.format_version = read_binary_value(ifs); record.step = read_binary_value(ifs); record.nbasis = read_binary_value(ifs); const int nR = read_binary_value(ifs); diff --git a/source/source_io/module_hs/unittests/test_dhs_sparse_writer.cpp b/source/source_io/module_hs/unittests/test_dhs_sparse_writer.cpp index 3885c7c9472..aa2b9df9772 100644 --- a/source/source_io/module_hs/unittests/test_dhs_sparse_writer.cpp +++ b/source/source_io/module_hs/unittests/test_dhs_sparse_writer.cpp @@ -27,7 +27,7 @@ static std::ofstream ofs_running; TEST(DhsSparseWriter, TextCountsOnlyValuesAboveThreshold) { - remove_derivative_files("h", 5); + remove_derivative_files("h"); init_sparse_output_globals(); Parallel_Orbitals pv; @@ -40,11 +40,12 @@ TEST(DhsSparseWriter, TextCountsOnlyValuesAboveThreshold) arrays.dHRx_sparse[0][r_vector][1][0] = 0.0; arrays.dHRx_sparse[0][r_vector][1][1] = -2.0; - ModuleIO::save_dH_sparse(5, pv, arrays, 1e-10, false, "h", 8, "./", "./", "scf", false, 1, 2, ofs_running); + ModuleIO::save_dH_sparse(5, pv, arrays, 1e-10, false, "h", 8, "./", "scf", false, 1, 2, ofs_running); - const std::vector lines = read_lines("dhrxs1g6_nao.csr"); + const std::vector lines = read_lines("dhrxs1_nao.csr"); ASSERT_GE(lines.size(), 7); - EXPECT_EQ(lines[0], "STEP: 5"); + // istep = 5 is the sixth ionic step, printed one-based as 6 + EXPECT_EQ(lines[0], "STEP: 6"); EXPECT_EQ(lines[1], "Matrix Dimension of dHx(R): 2"); EXPECT_EQ(lines[2], "Matrix number of dHx(R): 1"); EXPECT_EQ(lines[3], "0 0 0 2"); @@ -69,16 +70,16 @@ TEST(DhsSparseWriter, TextCountsOnlyValuesAboveThreshold) } EXPECT_THAT(indptr, testing::ElementsAre(0, 1, 2)); - const std::vector y_lines = read_lines("dhrys1g6_nao.csr"); + const std::vector y_lines = read_lines("dhrys1_nao.csr"); ASSERT_GE(y_lines.size(), 4); EXPECT_EQ(y_lines[3], "0 0 0 0"); - remove_derivative_files("h", 5); + remove_derivative_files("h"); } TEST(DhsSparseWriter, BinaryCountsOnlyValuesAboveThreshold) { - remove_derivative_files("h", 6); + remove_derivative_files("h"); init_sparse_output_globals(); Parallel_Orbitals pv; @@ -91,11 +92,12 @@ TEST(DhsSparseWriter, BinaryCountsOnlyValuesAboveThreshold) arrays.dHRx_sparse[0][r_vector][1][0] = 0.0; arrays.dHRx_sparse[0][r_vector][1][1] = -2.0; - ModuleIO::save_dH_sparse(6, pv, arrays, 1e-10, true, "h", 8, "./", "./", "scf", false, 1, 2, ofs_running); + ModuleIO::save_dH_sparse(6, pv, arrays, 1e-10, true, "h", 8, "./", "scf", false, 1, 2, ofs_running); - std::ifstream ifs("dhrxs1g7_nao.csr", std::ios::binary); + std::ifstream ifs("dhrxs1_nao.csr", std::ios::binary); ASSERT_TRUE(ifs.is_open()); - EXPECT_EQ(read_binary_value(ifs), 6); + // istep = 6 -> one-based step 7 in the binary header + EXPECT_EQ(read_binary_value(ifs), 7); EXPECT_EQ(read_binary_value(ifs), 2); EXPECT_EQ(read_binary_value(ifs), 1); EXPECT_EQ(read_binary_value(ifs), 0); @@ -110,12 +112,12 @@ TEST(DhsSparseWriter, BinaryCountsOnlyValuesAboveThreshold) EXPECT_EQ(read_binary_value(ifs), 1); EXPECT_EQ(read_binary_value(ifs), 2); - remove_derivative_files("h", 6); + remove_derivative_files("h"); } TEST(DhsSparseWriter, SocWritesAllDirections) { - remove_derivative_files("s", 7); + remove_derivative_files("s"); init_sparse_output_globals(); Parallel_Orbitals pv; @@ -127,11 +129,11 @@ TEST(DhsSparseWriter, SocWritesAllDirections) arrays.dHRy_soc_sparse[r_vector][0][1] = std::complex(2.0, -1.0); arrays.dHRz_soc_sparse[r_vector][1][1] = std::complex(-3.0, 0.5); - ModuleIO::save_dH_sparse(7, pv, arrays, 1e-10, false, "s", 8, "./", "./", "scf", false, 4, 2, ofs_running); + ModuleIO::save_dH_sparse(7, pv, arrays, 1e-10, false, "s", 8, "./", "scf", false, 4, 2, ofs_running); - const std::string x_output = read_file("dsrxs1g8_nao.csr"); - const std::string y_output = read_file("dsrys1g8_nao.csr"); - const std::string z_output = read_file("dsrzs1g8_nao.csr"); + const std::string x_output = read_file("dsrxs1_nao.csr"); + const std::string y_output = read_file("dsrys1_nao.csr"); + const std::string z_output = read_file("dsrzs1_nao.csr"); EXPECT_THAT(x_output, testing::HasSubstr("Matrix number of dHx(R): 1\n0 0 0 1\n")); EXPECT_THAT(y_output, testing::HasSubstr("Matrix number of dHy(R): 1\n0 0 0 1\n")); EXPECT_THAT(z_output, testing::HasSubstr("Matrix number of dHz(R): 1\n0 0 0 1\n")); @@ -139,7 +141,45 @@ TEST(DhsSparseWriter, SocWritesAllDirections) EXPECT_THAT(y_output, testing::HasSubstr("(2.00000000e+00,-1.00000000e+00)")); EXPECT_THAT(z_output, testing::HasSubstr("(-3.00000000e+00,5.00000000e-01)")); - remove_derivative_files("s", 7); + remove_derivative_files("s"); +} + +TEST(DhsSparseWriter, AppendModeReusesFixedFilename) +{ + remove_derivative_files("s"); + init_sparse_output_globals(); + + Parallel_Orbitals pv; + init_serial_orbitals(pv); + LCAO_HS_Arrays arrays; + const Abfs::Vector3_Order r_vector(0, 0, 0); + arrays.all_R_coor.insert(r_vector); + arrays.dHRx_sparse[0][r_vector][0][0] = 1.0; + + // out_app_flag = 1 reuses the fixed filename without a g{N} suffix; the + // first written ionic step (istep == 0) truncates it. + ModuleIO::save_dH_sparse(0, pv, arrays, 1e-10, false, "s", 8, "./", "md", 1, 1, 2, ofs_running); + const std::string first_frame = read_file("dsrxs1_nao.csr"); + ASSERT_FALSE(first_frame.empty()); + EXPECT_EQ(count_substr(first_frame, "STEP: "), 1); + EXPECT_EQ(count_substr(first_frame, "STEP: 1"), 1); + + // The next ionic step appends a second frame to the same file. + ModuleIO::save_dH_sparse(1, pv, arrays, 1e-10, false, "s", 8, "./", "md", 1, 1, 2, ofs_running); + const std::string two_frames = read_file("dsrxs1_nao.csr"); + EXPECT_EQ(count_substr(two_frames, "STEP: "), 2); + EXPECT_EQ(count_substr(two_frames, "STEP: 1"), 1); + EXPECT_EQ(count_substr(two_frames, "STEP: 2"), 1); + + // Mode 2 keeps one suffixed file per ionic step and never appends; mode 0 + // overwrites the fixed filename instead. + ModuleIO::save_dH_sparse(0, pv, arrays, 1e-10, false, "s", 8, "./", "md", 2, 1, 2, ofs_running); + const std::vector mode2_lines = read_lines("dsrxs1g1_nao.csr"); + ASSERT_FALSE(mode2_lines.empty()); + EXPECT_EQ(mode2_lines[0], "STEP: 1"); + + remove_derivative_files("s"); + remove_derivative_files("s", 0); } int main(int argc, char** argv) diff --git a/source/source_io/module_hs/unittests/test_hs_dense_io.cpp b/source/source_io/module_hs/unittests/test_hs_dense_io.cpp index 31410679def..6eb6263e030 100644 --- a/source/source_io/module_hs/unittests/test_hs_dense_io.cpp +++ b/source/source_io/module_hs/unittests/test_hs_dense_io.cpp @@ -378,6 +378,60 @@ TEST(HsDenseIoSaveMat, BinaryAppendCompleteRecordsAndOverwrite) remove_test_file(filename); } +TEST(HsDenseIoSaveMat, AppendFalseOverwritesSameIstep) +{ + /// Protects against regression where an int-mode value (out_app_flag) is + /// passed to a bool append parameter. The older BinaryAppend test covers + /// cross-stepping; this one exercises the same-istep overwrite path. + const int dim = 2; + const std::string filename = "save_mat_overwrite_same_step.dat"; + remove_test_file(filename); + + Parallel_Orbitals pv; + initialize_distribution(pv, dim); + const std::vector first = {1.0, 2.0, 3.0, 4.0}; + const std::vector replacement = {9.0, 10.0, 11.0, 12.0}; + const std::vector first_local = distribute_matrix(pv, first, dim); + const std::vector replacement_local + = distribute_matrix(pv, replacement, dim); + + // Step 1: append=true writes the full record for istep=0. + ModuleIO::save_mat(0, first_local.data(), dim, true, 8, true, true, + filename, pv, test_rank, "cg"); + + // Step 2: append=false on the same istep must truncate the file, not + // append a second copy. + ModuleIO::save_mat(0, replacement_local.data(), dim, true, 8, true, false, + filename, pv, test_rank, "cg"); + + if (test_rank == 0) + { + std::ifstream ifs(filename.c_str(), std::ios::binary); + ASSERT_TRUE(ifs.is_open()); + // Exactly one record, containing the replacement values. + EXPECT_EQ(read_record(ifs, dim), upper_triangle(replacement, dim)); + EXPECT_EQ(ifs.peek(), std::ifstream::traits_type::eof()); + } + + remove_test_file(filename); +} + +TEST(HsDenseIoSaveMat, TextPrecisionFollowsArgument) +{ + // The H(k) component writers forward the second INPUT value to this + // precision argument; a non-default value must change the printed mantissa. + const std::string filename = testing::TempDir() + "/save_mat_precision.dat"; + const int dim = 2; + std::vector mat(dim * dim, 0.0); + mat[0] = 1.23456789; + const Parallel_2D pv = make_serial_pv(dim); + ModuleIO::save_mat(0, mat.data(), dim, false, 5, false, false, + filename, pv, 0, "genelpa", false); + const std::string content = read_text_file(filename); + EXPECT_NE(content.find("1.23457"), std::string::npos); + std::remove(filename.c_str()); +} + // --------------------------------------------------------------------------- // Main: initialize MPI before running tests (required by save_mat's MPI path) // --------------------------------------------------------------------------- diff --git a/source/source_io/module_hs/unittests/test_hs_sparse_io.cpp b/source/source_io/module_hs/unittests/test_hs_sparse_io.cpp index d3bd9fb8495..41544d286d2 100644 --- a/source/source_io/module_hs/unittests/test_hs_sparse_io.cpp +++ b/source/source_io/module_hs/unittests/test_hs_sparse_io.cpp @@ -41,7 +41,8 @@ TEST(HsSparseIo, SparseHeaderKeepsStepStyle) ModuleIO::save_sparse(sparse_matrix, all_R_coor, pv, options); const std::string output = read_file(filename); - EXPECT_TRUE(starts_with(output, "STEP: 0\n")); + // istep = 0 is the first ionic step, printed one-based as 1 + EXPECT_TRUE(starts_with(output, "STEP: 1\n")); EXPECT_THAT(output, testing::HasSubstr("Matrix Dimension of S(R): 2\n")); EXPECT_THAT(output, testing::HasSubstr("Matrix number of S(R): 1\n")); EXPECT_THAT(output, testing::HasSubstr("0 0 0 2\n")); @@ -77,7 +78,8 @@ TEST(HsSparseIo, SparseTextCountsOnlyValuesAboveThreshold) const std::vector lines = read_lines(filename); ASSERT_GE(lines.size(), 6); - EXPECT_EQ(lines[0], "STEP: 2"); + // istep = 2 is the third ionic step, printed one-based as 3 + EXPECT_EQ(lines[0], "STEP: 3"); EXPECT_EQ(lines[1], "Matrix Dimension of S(R): 2"); EXPECT_EQ(lines[2], "Matrix number of S(R): 1"); EXPECT_EQ(lines[3], "0 0 0 2"); @@ -137,8 +139,9 @@ TEST(HsSparseIo, SparseBinaryHeaderWritesConcreteStep) options.reduce = false; ModuleIO::save_sparse(sparse_matrix, all_R_coor, pv, options); + // istep = 3 -> one-based step 4 in the binary header const std::vector header_and_r = read_binary_ints(filename, 7); - EXPECT_THAT(header_and_r, testing::ElementsAre(3, 2, 1, 0, 0, 0, 2)); + EXPECT_THAT(header_and_r, testing::ElementsAre(4, 2, 1, 0, 0, 0, 2)); std::remove(filename.c_str()); } @@ -171,7 +174,8 @@ TEST(HsSparseIo, SparseBinaryCountsOnlyValuesAboveThreshold) std::ifstream ifs(filename.c_str(), std::ios::binary); ASSERT_TRUE(ifs.is_open()); - EXPECT_EQ(read_binary_value(ifs), 4); + // istep = 4 -> one-based step 5 in the binary header + EXPECT_EQ(read_binary_value(ifs), 5); EXPECT_EQ(read_binary_value(ifs), 2); EXPECT_EQ(read_binary_value(ifs), 1); EXPECT_EQ(read_binary_value(ifs), 0); diff --git a/source/source_io/module_hs/unittests/test_hsk_writer.cpp b/source/source_io/module_hs/unittests/test_hsk_writer.cpp index a812efb77f4..6c59b9af7e7 100644 --- a/source/source_io/module_hs/unittests/test_hsk_writer.cpp +++ b/source/source_io/module_hs/unittests/test_hsk_writer.cpp @@ -17,6 +17,8 @@ #include "source_base/matrix_block.h" #include "source_base/module_out/filename.h" +#include "source_base/module_parallel/para_mpi_func.h" +#include "source_base/module_parallel/para_world.h" #include "source_base/parallel_comm.h" #include "source_basis/module_ao/parallel_orbitals.h" #include "source_hamilt/hamilt.h" @@ -38,6 +40,16 @@ namespace /// Rank in DIAG_WORLD; 0 without MPI. int test_rank = 0; +/// Collectively synchronize DIAG_WORLD through the guarded Parallel wrappers. +/// Governance rule 9 forbids direct MPI calls in tests; no-op in serial builds. +void diag_world_barrier() +{ +#ifdef __MPI + const Parallel::ParaWorld world = Parallel::ParaWorld::make_mpi("test_hsk_writer", DIAG_WORLD); + Parallel::barrier(world); +#endif +} + /// Output options shared by the tests (binary, no append, step 0). const int k_out_type = 2; const bool k_out_app = false; @@ -436,6 +448,91 @@ TEST(WriteHsk, WritesComplexKMatrix) remove_test_file("write_hsk_running_" + std::to_string(test_rank) + ".log"); } +// --------------------------------------------------------------------------- +// Regression guard: out_app_flag = 2 must write non-append records. +// The old code passed the integer out_app_flag directly to save_mat's +// boolean append argument, so a rerun in mode 2 would silently grow the +// file instead of replacing the snapshot. This test sets out_app_flag=2, +// writes one snapshot, swaps the FakeHamilt buffers, writes the same step +// again, and verifies the file contains exactly one record holding the +// second payload. +// --------------------------------------------------------------------------- + +TEST(WriteHsk, Mode2OverwritesSameIstep) +{ + const int dim = 2; + const int nspin = 1; + const int nks = 1; + const int nkstot = 1; + const std::vector ik2iktot = {0}; + const std::vector isk = {0}; + const bool gamma_only = false; + const int istep = 0; + const int out_app_flag = 2; // mode 2 => overwrite semantics + const int out_type = 2; // binary + const int precision = 8; + + Parallel_Orbitals pv; + initialize_distribution(pv, dim); + + FakeHamilt fake; + fake.dim = dim; + + // Round 1 payload: H = all 1.0, S = all -1.0 + std::vector hk_first(dim * dim, 1.0); + std::vector sk_first(dim * dim, -1.0); + fake.set_k_matrix(0, + distribute_matrix(pv, hk_first, dim), + distribute_matrix(pv, sk_first, dim)); + + // filename_output with out_app_flag=2 + istep=0 yields hk{ik}g{step+1}_nao.dat + const std::string h_fn = ModuleIO::filename_output( + "", "hk", "nao", 0, ik2iktot, nspin, nkstot, out_type, + out_app_flag, gamma_only, istep); + const std::string s_fn = ModuleIO::filename_output( + "", "sk", "nao", 0, ik2iktot, nspin, nkstot, out_type, + out_app_flag, gamma_only, istep); + remove_test_file(h_fn); + remove_test_file(s_fn); + + std::ofstream ofs = open_running_log(); + ModuleIO::write_hsk("", nspin, nks, nkstot, ik2iktot, isk, &fake, pv, + gamma_only, out_app_flag, istep, out_type, precision, dim, + "cg", test_rank, ofs); + + // Round 2 payload: different data, same out_app_flag and istep. + std::vector hk_second(dim * dim, 9.0); + std::vector sk_second(dim * dim, -9.0); + fake.set_k_matrix(0, + distribute_matrix(pv, hk_second, dim), + distribute_matrix(pv, sk_second, dim)); + + diag_world_barrier(); + ModuleIO::write_hsk("", nspin, nks, nkstot, ik2iktot, isk, &fake, pv, + gamma_only, out_app_flag, istep, out_type, precision, dim, + "cg", test_rank, ofs); + + if (test_rank == 0) + { + std::ifstream hfs(h_fn.c_str(), std::ios::binary); + ASSERT_TRUE(hfs.is_open()) << h_fn; + // Exactly one record — second payload — not two records stacked. + EXPECT_EQ(read_record(hfs, dim), upper_triangle(hk_second, dim)); + EXPECT_EQ(hfs.peek(), std::ifstream::traits_type::eof()) + << "hk file should contain one overwrite record, not two"; + + std::ifstream sfs(s_fn.c_str(), std::ios::binary); + ASSERT_TRUE(sfs.is_open()) << s_fn; + EXPECT_EQ(read_record(sfs, dim), upper_triangle(sk_second, dim)); + EXPECT_EQ(sfs.peek(), std::ifstream::traits_type::eof()); + } + + remove_test_file(h_fn); + remove_test_file(s_fn); + ofs.close(); + remove_test_file("write_hsk_running_" + std::to_string(test_rank) + ".log"); +} + int main(int argc, char** argv) { #ifdef __MPI diff --git a/source/source_io/module_hs/unittests/test_hsr_writer.cpp b/source/source_io/module_hs/unittests/test_hsr_writer.cpp index a5f86e7c74e..9dee9f3acd0 100644 --- a/source/source_io/module_hs/unittests/test_hsr_writer.cpp +++ b/source/source_io/module_hs/unittests/test_hsr_writer.cpp @@ -43,46 +43,46 @@ void expect_fname_eq(const std::string& expected, const std::string& actual) // hsr_gen_fname: H(R) spin-dependent filename // --------------------------------------------------------------------------- -TEST(HsrWriterFname, HsrOverwriteWithStep) +TEST(HsrWriterFname, HsrNumberedWithStep) { - // prefix="data-", ispin=0, append=false, istep=0 + // prefix="data-", ispin=0, numbered=true, istep=0 // -> "data-1g1_nao.csr" - const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, false, 0); + const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, true, 0); expect_fname_eq("data-1g1_nao.csr", fname); } -TEST(HsrWriterFname, HsrOverwriteWithStepTwo) +TEST(HsrWriterFname, HsrNumberedWithStepTwo) { // istep=2 -> g3 - const std::string fname = ModuleIO::hsr_gen_fname("data-", 1, false, 2); + const std::string fname = ModuleIO::hsr_gen_fname("data-", 1, true, 2); expect_fname_eq("data-2g3_nao.csr", fname); } -TEST(HsrWriterFname, HsrAppendSuppressesStep) +TEST(HsrWriterFname, HsrFixedNameSuppressesStep) { - // append=true -> step number is omitted - const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, true, 5); + // numbered=false -> step number is omitted (modes 0/1) + const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, false, 5); expect_fname_eq("data-1_nao.csr", fname); } TEST(HsrWriterFname, HsrNegativeStepSuppressesStep) { // istep=-1 -> step number is omitted (no "g" suffix) - const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, false, -1); + const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, true, -1); expect_fname_eq("data-1_nao.csr", fname); } TEST(HsrWriterFname, HsrDatExtension) { // out_type=2 -> ".dat" extension - const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, false, 0, 2); + const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, true, 0, 2); expect_fname_eq("data-1g1_nao.dat", fname); } TEST(HsrWriterFname, HsrCsrExtensionDefault) { // out_type=1 (default) -> ".csr" extension - const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, false, 0, 1); + const std::string fname = ModuleIO::hsr_gen_fname("data-", 0, true, 0, 1); expect_fname_eq("data-1g1_nao.csr", fname); } @@ -90,31 +90,31 @@ TEST(HsrWriterFname, HsrCsrExtensionDefault) // sr_gen_fname: S(R) spin-independent filename // --------------------------------------------------------------------------- -TEST(HsrWriterFname, SrOverwriteWithStep) +TEST(HsrWriterFname, SrNumberedWithStep) { - // append=false, istep=0 -> "srg1_nao.csr" - const std::string fname = ModuleIO::sr_gen_fname(false, 0); + // numbered=true, istep=0 -> "srg1_nao.csr" + const std::string fname = ModuleIO::sr_gen_fname(true, 0); expect_fname_eq("srg1_nao.csr", fname); } -TEST(HsrWriterFname, SrAppendSuppressesStep) +TEST(HsrWriterFname, SrFixedNameSuppressesStep) { - // append=true -> "sr_nao.csr" (no step number) - const std::string fname = ModuleIO::sr_gen_fname(true, 5); + // numbered=false -> "sr_nao.csr" (no step number) + const std::string fname = ModuleIO::sr_gen_fname(false, 5); expect_fname_eq("sr_nao.csr", fname); } TEST(HsrWriterFname, SrNegativeStepSuppressesStep) { // istep=-1 -> "sr_nao.csr" - const std::string fname = ModuleIO::sr_gen_fname(false, -1); + const std::string fname = ModuleIO::sr_gen_fname(true, -1); expect_fname_eq("sr_nao.csr", fname); } TEST(HsrWriterFname, SrDatExtension) { // out_type=2 -> ".dat" - const std::string fname = ModuleIO::sr_gen_fname(false, 0, 2); + const std::string fname = ModuleIO::sr_gen_fname(true, 0, 2); expect_fname_eq("srg1_nao.dat", fname); } @@ -122,25 +122,25 @@ TEST(HsrWriterFname, SrDatExtension) // dhr_gen_fname: dH/dR spin-dependent filename // --------------------------------------------------------------------------- -TEST(HsrWriterFname, DhrOverwriteWithStep) +TEST(HsrWriterFname, DhrNumberedWithStep) { - // prefix="H", ispin=0, append=false, istep=0 + // prefix="H", ispin=0, numbered=true, istep=0 // -> "Hrs1g1_nao.csr" - const std::string fname = ModuleIO::dhr_gen_fname("H", 0, false, 0); + const std::string fname = ModuleIO::dhr_gen_fname("H", 0, true, 0); expect_fname_eq("Hrs1g1_nao.csr", fname); } -TEST(HsrWriterFname, DhrAppendSuppressesStep) +TEST(HsrWriterFname, DhrFixedNameSuppressesStep) { - // append=true -> step omitted - const std::string fname = ModuleIO::dhr_gen_fname("H", 1, true, 3); + // numbered=false -> step omitted (modes 0/1) + const std::string fname = ModuleIO::dhr_gen_fname("H", 1, false, 3); expect_fname_eq("Hrs2_nao.csr", fname); } TEST(HsrWriterFname, DhrNegativeStepSuppressesStep) { // istep=-1 -> step omitted - const std::string fname = ModuleIO::dhr_gen_fname("dH", 0, false, -1); + const std::string fname = ModuleIO::dhr_gen_fname("dH", 0, true, -1); expect_fname_eq("dHrs1_nao.csr", fname); } @@ -163,7 +163,7 @@ TEST(HsrWriterIo, HContainerCsrHeaderKeepsCurrentFormat) // label "H": header carries the Fermi energy of this spin channel const double efermi_eV = 5.4321; - ModuleIO::write_hcontainer_csr(filename, &ucell, 5, &matrix, 0, 0, 1, "H", "", efermi_eV, true); + ModuleIO::write_hcontainer_csr(filename, &ucell, 5, &matrix, 0, false, 0, 1, "H", "", efermi_eV, true); const std::string output = read_file(filename); EXPECT_THAT(output, testing::HasSubstr(" --- Ionic Step 1 ---\n")); @@ -203,7 +203,7 @@ TEST(HsrWriterIo, GammaFoldedHeaderKeepsCsrReadable) // label "H" without Fermi energy available: pass has_efermi = false const double no_efermi = 0.0; ModuleIO::write_hcontainer_csr( - filename, &ucell, 5, &matrix, 0, 0, 1, "H", representation_note, no_efermi, false); + filename, &ucell, 5, &matrix, 0, false, 0, 1, "H", representation_note, no_efermi, false); const std::string output = read_file(filename); EXPECT_THAT(output, testing::HasSubstr("# representation: " + representation_note + "\n")); @@ -235,8 +235,8 @@ TEST(HsrWriterIo, HContainerCsrAppendKeepsCurrentStepSections) // label "S": header carries no Fermi energy (has_efermi = false) const double ignored_efermi = 0.0; - ModuleIO::write_hcontainer_csr(filename, &ucell, 4, &matrix, 0, 0, 1, "S", "", ignored_efermi, false); - ModuleIO::write_hcontainer_csr(filename, &ucell, 4, &matrix, 1, 0, 1, "S", "", ignored_efermi, false); + ModuleIO::write_hcontainer_csr(filename, &ucell, 4, &matrix, 0, false, 0, 1, "S", "", ignored_efermi, false); + ModuleIO::write_hcontainer_csr(filename, &ucell, 4, &matrix, 1, true, 0, 1, "S", "", ignored_efermi, false); const std::string output = read_file(filename); EXPECT_EQ(count_substr(output, " --- Ionic Step "), 2); @@ -266,8 +266,8 @@ TEST(HsrWriterIo, HContainerCsrHeaderCarriesPerChannelFermi) const std::string filename_dw = "write_hs_r_header_fermi_dw.csr"; std::remove(filename_up.c_str()); std::remove(filename_dw.c_str()); - ModuleIO::write_hcontainer_csr(filename_up, &ucell, 5, &matrix, 0, 0, 2, "H", "", efermi_up_eV, true); - ModuleIO::write_hcontainer_csr(filename_dw, &ucell, 5, &matrix, 0, 1, 2, "H", "", efermi_dw_eV, true); + ModuleIO::write_hcontainer_csr(filename_up, &ucell, 5, &matrix, 0, false, 0, 2, "H", "", efermi_up_eV, true); + ModuleIO::write_hcontainer_csr(filename_dw, &ucell, 5, &matrix, 0, false, 1, 2, "H", "", efermi_dw_eV, true); const std::string output_up = read_file(filename_up); const std::string output_dw = read_file(filename_dw); @@ -300,7 +300,11 @@ TEST(HsrWriterIo, HContainerBinaryWritesSortedAndEmptyRBlocks) std::ifstream ifs(filename.c_str(), std::ios::binary); ASSERT_TRUE(ifs.is_open()); const NativeDoubleRecord record = read_native_double_record(ifs); - EXPECT_EQ(record.step, 0); + // versioned record header: magic 'HSRB' followed by format version 2 + EXPECT_EQ(record.magic, 0x48535242); + EXPECT_EQ(record.format_version, 2); + // istep = -1 (single-point SCF) maps to one-based step 1 + EXPECT_EQ(record.step, 1); EXPECT_EQ(record.nbasis, 2); ASSERT_EQ(record.blocks.size(), 2); EXPECT_EQ(record.blocks[0].rx, -1); @@ -340,7 +344,11 @@ TEST(HsrWriterIo, HContainerBinaryWritesComplexValuesAsDoublePairs) std::ifstream ifs(filename.c_str(), std::ios::binary); ASSERT_TRUE(ifs.is_open()); - EXPECT_EQ(read_binary_value(ifs), 4); + // versioned record header: magic 'HSRB', format version 2 + EXPECT_EQ(read_binary_value(ifs), 0x48535242); + EXPECT_EQ(read_binary_value(ifs), 2); + // istep = 4 -> one-based step 5 in the binary header + EXPECT_EQ(read_binary_value(ifs), 5); EXPECT_EQ(read_binary_value(ifs), 2); EXPECT_EQ(read_binary_value(ifs), 1); EXPECT_EQ(read_binary_value(ifs), 0); @@ -377,15 +385,16 @@ TEST(HsrWriterIo, HContainerBinaryAppendsAndCanOverwrite) std::ifstream appended(filename.c_str(), std::ios::binary); ASSERT_TRUE(appended.is_open()); - EXPECT_EQ(read_native_double_record(appended).step, 0); + // istep 0/1/2 -> one-based steps 1/2/3 EXPECT_EQ(read_native_double_record(appended).step, 1); + EXPECT_EQ(read_native_double_record(appended).step, 2); EXPECT_EQ(appended.peek(), std::ifstream::traits_type::eof()); appended.close(); ModuleIO::write_hcontainer_csr_binary(filename, &matrix, 2, false); std::ifstream replaced(filename.c_str(), std::ios::binary); ASSERT_TRUE(replaced.is_open()); - EXPECT_EQ(read_native_double_record(replaced).step, 2); + EXPECT_EQ(read_native_double_record(replaced).step, 3); EXPECT_EQ(replaced.peek(), std::ifstream::traits_type::eof()); std::remove(filename.c_str()); @@ -454,7 +463,7 @@ TEST(HsrWriterIo, HContainerBinaryMpiGatherWritesCompleteFiles) eferm.ef = 0.4; // Ry; used only for the H(R) header std::ofstream ofs_running_null; // not opened; test does not inspect the running log ModuleIO::write_hsr( - hr_vec, &sr_parallel, &ucell, 2, 8, parallel_pv, true, true, iat2iwt, 1, 0, "./", eferm, + hr_vec, &sr_parallel, &ucell, 2, 8, parallel_pv, 1, true, iat2iwt, 1, 0, "./", eferm, ofs_running_null); MPI_Barrier(MPI_COMM_WORLD); @@ -494,7 +503,7 @@ TEST(HsrWriterIo, WriteHsrTextCsrCarriesPerSpinFermiFromEfermi) // Exercise the fix path inside write_hsr: eferm.get_efval(ispin) * Ry_to_eV // for a two-Fermi (nspin=2) case. The only existing write_hsr test uses // out_type=2 (binary), which skips the Fermi branch entirely. - // append=true suppresses the ionic-step suffix in the generated names. + // out_app_flag=1 keeps a fixed filename (no g{step} suffix) and appends. const std::string hr_up_filename = "hrs1_nao.csr"; const std::string hr_dw_filename = "hrs2_nao.csr"; const std::string sr_filename = "sr_nao.csr"; @@ -542,7 +551,7 @@ TEST(HsrWriterIo, WriteHsrTextCsrCarriesPerSpinFermiFromEfermi) std::ofstream ofs_running_null; // not opened; test does not inspect the running log ModuleIO::write_hsr( - hr_vec, &sr, &ucell, 1, 8, pv, true, true, iat2iwt_ptr, 1, 0, "./", eferm, + hr_vec, &sr, &ucell, 1, 8, pv, 1, true, iat2iwt_ptr, 1, 0, "./", eferm, ofs_running_null); const std::string output_up = read_file(hr_up_filename); diff --git a/source/source_io/module_hs/vxc_op_lip.cpp b/source/source_io/module_hs/vxc_op_lip.cpp index fcee73b6cd4..c170357addc 100644 --- a/source/source_io/module_hs/vxc_op_lip.cpp +++ b/source/source_io/module_hs/vxc_op_lip.cpp @@ -4,6 +4,8 @@ #include "source_base/parallel_reduce.h" #include "source_base/module_container/base/third_party/blas.h" #include "source_io/module_hs/hs_dense_io.h" +#include "source_io/module_hs/vxc_op_tools.h" +#include "source_io/module_output/provenance_header.h" #include #include @@ -104,6 +106,8 @@ FPTYPE all_band_energy_lip(const int ik, const std::vector& orbital_ener template void write_Vxc_LIP(int nspin, + const int istep, + const double etot, int naos, int drank, const psi::Psi>& psi_pw, @@ -195,15 +199,15 @@ void write_Vxc_LIP(int nspin, #endif // add-up and write - const int istep = -1; + const int istep_mat = -1; const int out_label = 1; // 1 means .txt while 2 means .dat - const bool out_app_flag = 0; + const int out_app_flag = 0; std::string vxc_file = ModuleIO::filename_output( global_out_dir, "vxc", "nao", ik, kv.ik2iktot, nspin, kv.get_nkstot(), - out_label, out_app_flag, gamma_only, istep); + out_label, out_app_flag, gamma_only, istep_mat); - ModuleIO::save_mat(istep, vxc_tot_k_mo.data(), nbands, + ModuleIO::save_mat(istep_mat, vxc_tot_k_mo.data(), nbands, false, out_ndigits, true, out_app_flag, vxc_file, p2d_serial, drank, ks_solver, false); @@ -215,13 +219,18 @@ void write_Vxc_LIP(int nspin, const int nspin0 = (nspin == 2) ? 2 : 1; auto write_orb_energy_lip = [&kv, &nspin0, &nbands, &global_out_dir](const std::vector>& e_orb, const std::string& label, + const std::string& header, const bool app = false) { assert(e_orb.size() == kv.get_nks()); const int nk = kv.get_nks() / nspin0; std::ofstream ofs; const std::string out_name = (label == "") ? ".txt" : "_" + label + ".txt"; - ofs.open(global_out_dir + "vxc" + out_name, + ofs.open(global_out_dir + "eband_vxc" + out_name, app ? std::ios::app : std::ios::out); + if (!header.empty()) + { + ofs << header; + } ofs << nk << "\n" << nspin0 << "\n" << nbands << "\n"; ofs << std::scientific << std::setprecision(16); for (int ik = 0; ik < nk; ++ik) @@ -238,12 +247,17 @@ void write_Vxc_LIP(int nspin, if (GlobalV::MY_RANK == 0) { - write_orb_energy_lip(e_orb_tot, ""); + const double etot_eV = etot * ModuleBase::Ry_to_eV; + const std::string time_stamp = local_timestamp(); + const std::string header_tot = build_eband_header(istep, etot_eV, "V_xc", time_stamp); + write_orb_energy_lip(e_orb_tot, "", header_tot); #if((defined __LCAO)&&(defined __EXX) && !(defined __CUDA)&& !(defined __ROCM)) if (cal_exx) { - write_orb_energy_lip(e_orb_locxc, "local"); - write_orb_energy_lip(e_orb_exx, "exx"); + const std::string header_loc = build_eband_header(istep, etot_eV, "V_xc_local", time_stamp); + const std::string header_exx = build_eband_header(istep, etot_eV, "V_EXX", time_stamp); + write_orb_energy_lip(e_orb_locxc, "local", header_loc); + write_orb_energy_lip(e_orb_exx, "exx", header_exx); } #endif } @@ -251,7 +265,7 @@ void write_Vxc_LIP(int nspin, // Explicit template instantiations template void write_Vxc_LIP( - int, int, int, const psi::Psi>&, const UnitCell&, Structure_Factor&, surchem&, + int, const int, const double, int, int, const psi::Psi>&, const UnitCell&, Structure_Factor&, surchem&, const ModulePW::PW_Basis_K&, const ModulePW::PW_Basis&, const ModulePW::PW_Basis&, const ModuleBase::matrix&, const Charge&, const K_Vectors&, const ModuleBase::matrix&, const bool, const std::string&, const int, const std::string&, bool, double @@ -261,7 +275,7 @@ template void write_Vxc_LIP( ); template void write_Vxc_LIP( - int, int, int, const psi::Psi>&, const UnitCell&, Structure_Factor&, surchem&, + int, const int, const double, int, int, const psi::Psi>&, const UnitCell&, Structure_Factor&, surchem&, const ModulePW::PW_Basis_K&, const ModulePW::PW_Basis&, const ModulePW::PW_Basis&, const ModuleBase::matrix&, const Charge&, const K_Vectors&, const ModuleBase::matrix&, const bool, const std::string&, const int, const std::string&, bool, double diff --git a/source/source_io/module_hs/vxc_op_lip.h b/source/source_io/module_hs/vxc_op_lip.h index a613077f6dd..232edb286a5 100644 --- a/source/source_io/module_hs/vxc_op_lip.h +++ b/source/source_io/module_hs/vxc_op_lip.h @@ -19,10 +19,16 @@ namespace ModuleIO { /// @brief Write the Vxc matrix in KS orbital representation for LIP (LCAO-in-PW), useful for GW calculation -/// including terms: local/semi-local XC and EXX +/// including terms: local/semi-local XC and EXX. +/// The band-decomposed diagonal energies (eband_vxc.txt and, with EXX, +/// eband_vxc_local.txt and eband_vxc_exx.txt) are written to global_out_dir. +/// @param istep Zero-based ionic step index; written one-based in the header +/// @param etot Total energy of the current frame in Ry /// @tparam FPTYPE Floating point type (float or double) template void write_Vxc_LIP(int nspin, + const int istep, + const double etot, int naos, int drank, const psi::Psi>& psi_pw, diff --git a/source/source_io/module_hs/vxc_op_mat.cpp b/source/source_io/module_hs/vxc_op_mat.cpp index c7a0b8fe2b4..b8925238a1a 100644 --- a/source/source_io/module_hs/vxc_op_mat.cpp +++ b/source/source_io/module_hs/vxc_op_mat.cpp @@ -1,14 +1,18 @@ #include "vxc_op_mat.h" +#include "source_base/constants.h" #include "source_base/module_out/filename.h" #include "source_base/parallel_reduce.h" #include "source_io/module_hs/hs_dense_io.h" +#include "source_io/module_output/provenance_header.h" namespace ModuleIO { template void write_Vxc(const int nspin, + const int istep, + const double etot, const int nbasis, const int drank, const Parallel_Orbitals* pv, @@ -26,6 +30,8 @@ void write_Vxc(const int nspin, Grid_Driver& gd, const bool dft_plus_u, const bool gamma_only, + const bool out_mat, + const std::string& mat_dir, const std::string& global_out_dir, const int out_ndigits, const std::string& ks_solver, @@ -112,36 +118,45 @@ void write_Vxc(const int nspin, const std::vector& vxc_tot_k_mo = cVc(vxc_k_ao.get_hk(), &psi(ik, 0, 0), nbasis, nbands, *pv, p2d); e_orb_tot.emplace_back(orbital_energy(ik, nbands, vxc_tot_k_mo, p2d)); - // write - const int istep = -1; - const int out_label = 1; // 1 means .txt while 2 means .dat - const bool out_app_flag = 0; - std::string vxc_file = ModuleIO::filename_output( - global_out_dir, - "vxc", "nao", ik, kv.ik2iktot, nspin, kv.get_nkstot(), - out_label, out_app_flag, gamma_only, istep); - - ModuleIO::save_mat(istep, - vxc_tot_k_mo.data(), - nbands, - false /*binary*/, - out_ndigits, - true /*triangle*/, - out_app_flag /*append*/, - vxc_file, - p2d, - drank, - ks_solver); + // write the full MO-basis matrix only when requested + if (out_mat) + { + const int istep = -1; + const int out_label = 1; // 1 means .txt while 2 means .dat + const int out_app_flag = 0; + const bool append_vxc = (out_app_flag == 1); + std::string vxc_file = ModuleIO::filename_output( + mat_dir, + "vxc", "nao", ik, kv.ik2iktot, nspin, kv.get_nkstot(), + out_label, out_app_flag, gamma_only, istep); + + ModuleIO::save_mat(istep, + vxc_tot_k_mo.data(), + nbands, + false /*binary*/, + out_ndigits, + true /*triangle*/, + append_vxc, + vxc_file, + p2d, + drank, + ks_solver); + } } if (GlobalV::MY_RANK == 0) { - write_orb_energy(kv, nspin0, nbands, e_orb_tot, "vxc", "", global_out_dir); + const double etot_eV = etot * ModuleBase::Ry_to_eV; + const std::string time_stamp = local_timestamp(); + const std::string header_tot = build_eband_header(istep, etot_eV, "V_xc", time_stamp); + write_orb_energy(kv, nspin0, nbands, e_orb_tot, "eband_vxc.txt", header_tot, global_out_dir); #ifdef __EXX if (cal_exx) { - write_orb_energy(kv, nspin0, nbands, e_orb_locxc, "vxc", "local", global_out_dir); - write_orb_energy(kv, nspin0, nbands, e_orb_exx, "vxc", "exx", global_out_dir); + const std::string header_loc = build_eband_header(istep, etot_eV, "V_xc_local", time_stamp); + const std::string header_exx = build_eband_header(istep, etot_eV, "V_EXX", time_stamp); + write_orb_energy(kv, nspin0, nbands, e_orb_locxc, "eband_vxc_local.txt", header_loc, global_out_dir); + write_orb_energy(kv, nspin0, nbands, e_orb_exx, "eband_vxc_exx.txt", header_exx, global_out_dir); } #endif } @@ -149,11 +164,11 @@ void write_Vxc(const int nspin, // Explicit template instantiations template void write_Vxc( - const int, const int, const int, const Parallel_Orbitals*, const psi::Psi&, + const int, const int, const double, const int, const int, const Parallel_Orbitals*, const psi::Psi&, const UnitCell&, Structure_Factor&, surchem&, const ModulePW::PW_Basis&, const ModulePW::PW_Basis&, const ModuleBase::matrix&, const Charge&, const K_Vectors&, const std::vector&, - const ModuleBase::matrix&, Grid_Driver&, const bool, const bool, const std::string&, - const int, const std::string&, bool, const Exx_Info& + const ModuleBase::matrix&, Grid_Driver&, const bool, const bool, const bool, const std::string&, + const std::string&, const int, const std::string&, bool, const Exx_Info& #ifdef __EXX , std::vector>>>*, std::vector>>>>* @@ -161,11 +176,11 @@ template void write_Vxc( ); template void write_Vxc, double>( - const int, const int, const int, const Parallel_Orbitals*, const psi::Psi>&, + const int, const int, const double, const int, const int, const Parallel_Orbitals*, const psi::Psi>&, const UnitCell&, Structure_Factor&, surchem&, const ModulePW::PW_Basis&, const ModulePW::PW_Basis&, const ModuleBase::matrix&, const Charge&, const K_Vectors&, const std::vector&, - const ModuleBase::matrix&, Grid_Driver&, const bool, const bool, const std::string&, - const int, const std::string&, bool, const Exx_Info& + const ModuleBase::matrix&, Grid_Driver&, const bool, const bool, const bool, const std::string&, + const std::string&, const int, const std::string&, bool, const Exx_Info& #ifdef __EXX , std::vector>>>*, std::vector>>>>* @@ -173,11 +188,11 @@ template void write_Vxc, double>( ); template void write_Vxc, std::complex>( - const int, const int, const int, const Parallel_Orbitals*, const psi::Psi>&, + const int, const int, const double, const int, const int, const Parallel_Orbitals*, const psi::Psi>&, const UnitCell&, Structure_Factor&, surchem&, const ModulePW::PW_Basis&, const ModulePW::PW_Basis&, const ModuleBase::matrix&, const Charge&, const K_Vectors&, const std::vector&, - const ModuleBase::matrix&, Grid_Driver&, const bool, const bool, const std::string&, - const int, const std::string&, bool, const Exx_Info& + const ModuleBase::matrix&, Grid_Driver&, const bool, const bool, const bool, const std::string&, + const std::string&, const int, const std::string&, bool, const Exx_Info& #ifdef __EXX , std::vector>>>*, std::vector>>>>* diff --git a/source/source_io/module_hs/vxc_op_mat.h b/source/source_io/module_hs/vxc_op_mat.h index 7e17a17cfb2..44860b52ca9 100644 --- a/source/source_io/module_hs/vxc_op_mat.h +++ b/source/source_io/module_hs/vxc_op_mat.h @@ -29,11 +29,19 @@ namespace ModuleIO { /// @brief Write the Vxc matrix in KS orbital representation, useful for GW calculation -/// including terms: local/semi-local XC, EXX, DFTU +/// including terms: local/semi-local XC, EXX, DFTU. +/// The band-decomposed diagonal energies (eband_vxc.txt and, with EXX, +/// eband_vxc_local.txt and eband_vxc_exx.txt) are always written to +/// global_out_dir; the full MO-basis matrices (vxc_nao.txt / vxck*_nao.txt) +/// are written to mat_dir only when out_mat is true. +/// @param istep Zero-based ionic step index; written one-based in the header +/// @param etot Total energy of the current frame in Ry /// @tparam TK K-point data type (double or std::complex) /// @tparam TR Real-space data type (double or std::complex) template void write_Vxc(const int nspin, + const int istep, + const double etot, const int nbasis, const int drank, const Parallel_Orbitals* pv, @@ -51,6 +59,8 @@ void write_Vxc(const int nspin, Grid_Driver& gd, const bool dft_plus_u, const bool gamma_only, + const bool out_mat, + const std::string& mat_dir, const std::string& global_out_dir, const int out_ndigits, const std::string& ks_solver, diff --git a/source/source_io/module_hs/vxc_op_tools.cpp b/source/source_io/module_hs/vxc_op_tools.cpp index 0862ae1557d..865f56e5435 100644 --- a/source/source_io/module_hs/vxc_op_tools.cpp +++ b/source/source_io/module_hs/vxc_op_tools.cpp @@ -1,7 +1,9 @@ #include "vxc_op_tools.h" +#include "source_base/formatter.h" #include "source_base/module_external/scalapack_connector.h" #include "source_base/module_out/filename.h" +#include "source_io/module_output/provenance_header.h" #include #include @@ -102,19 +104,21 @@ void write_orb_energy(const K_Vectors& kv, const int nspin0, const int nbands, const std::vector>& e_orb, - const std::string& term, - const std::string& label, + const std::string& out_name, + const std::string& header, const std::string& global_out_dir, const bool app) { assert(e_orb.size() == kv.get_nks()); + assert(!out_name.empty()); const int nk = kv.get_nks() / nspin0; std::ofstream ofs; - const std::string out_name = term == "vxc" - ? (label.empty() ? "vxc.txt" : "vxc_" + label + ".txt") - : term + "_" + (label.empty() ? "out.dat" : label + "_out.dat"); ofs.open(global_out_dir + out_name, app ? std::ios::app : std::ios::out); + if (!header.empty()) + { + ofs << header; + } ofs << nk << "\n" << nspin0 << "\n" << nbands << "\n"; ofs << std::scientific << std::setprecision(16); for (int ik = 0; ik < nk; ++ik) @@ -129,6 +133,22 @@ void write_orb_energy(const K_Vectors& kv, } } +std::string build_eband_header(const int istep, + const double etot_eV, + const std::string& term_symbol, + const std::string& time_stamp) +{ + const int istep_one_based = istep + 1; + const std::string step_lines = FmtCore::format( + "# RELAX STEP %d, Energy: %.8f eV\n" + "# e_{ikn} = in the KS orbital basis\n" + "# units: columns are e/2 in Hartree and e in eV; data rows follow nk, nspin0, nbands\n", + istep_one_based, + etot_eV, + term_symbol.c_str()); + return provenance_header(time_stamp) + step_lines; +} + // Explicit template instantiations template std::vector cVc(const double*, const double*, const int, const int, const Parallel_Orbitals&, const Parallel_2D&); template std::vector> cVc>(const std::complex*, const std::complex*, const int, const int, const Parallel_Orbitals&, const Parallel_2D&); diff --git a/source/source_io/module_hs/vxc_op_tools.h b/source/source_io/module_hs/vxc_op_tools.h index 4e66d632fb0..0a988d3b1da 100644 --- a/source/source_io/module_hs/vxc_op_tools.h +++ b/source/source_io/module_hs/vxc_op_tools.h @@ -80,19 +80,31 @@ double all_band_energy(const int ik, /// @param nspin0 Number of independent spin channels (1 or 2) /// @param nbands Number of bands /// @param e_orb Orbital energies per k-point -/// @param term Term label (e.g., "vxc", "kinetic") -/// @param label Additional label (e.g., "local", "exx") +/// @param out_name Output file name relative to the output directory +/// (e.g., "eband_vxc.txt", "eband_vl.txt") +/// @param header Comment block written before the numeric data; empty to omit /// @param global_out_dir Output directory /// @param app Append mode flag void write_orb_energy(const K_Vectors& kv, const int nspin0, const int nbands, const std::vector>& e_orb, - const std::string& term, - const std::string& label, + const std::string& out_name, + const std::string& header, const std::string& global_out_dir, const bool app = false); +/// @brief Build the five comment lines preceding band-decomposed energy data. +/// @param istep Zero-based ionic step index, printed one-based. +/// @param etot_eV Total energy of the written frame in eV. +/// @param term_symbol Operator symbol used in the expectation value expression. +/// @param time_stamp Timestamp shared by all files of the same run. +/// @return Comment block terminated by a newline. +std::string build_eband_header(const int istep, + const double etot_eV, + const std::string& term_symbol, + const std::string& time_stamp); + } // namespace ModuleIO #endif // VXC_OP_TOOLS_H diff --git a/source/source_io/module_output/cube_io.h b/source/source_io/module_output/cube_io.h index 6f554fef949..26496b8407c 100644 --- a/source/source_io/module_output/cube_io.h +++ b/source/source_io/module_output/cube_io.h @@ -52,6 +52,9 @@ bool read_vdata_palgrid(const Parallel_Grid& pgrid, const int nat); /// write volumetric data on the parallized grid into a .cube file +/// @param append append a complete frame to an existing cube file instead of +/// truncating it. Used by out_app_flag == 1, where successive ionic steps +/// reuse a fixed filename and each written step contributes one frame. void write_vdata_palgrid(const Parallel_Grid& pgrid, const double* const data, const int is, @@ -64,6 +67,7 @@ void write_vdata_palgrid(const Parallel_Grid& pgrid, const int out_fermi, const bool two_fermi, const bool reduce_all_pool, + const bool append, std::ofstream& ofs_running, const std::string& data_desc); @@ -84,6 +88,8 @@ bool read_cube(const std::string& file, std::vector& data); /// write a cube file +/// @param append append a complete frame to an existing file instead of +/// truncating it (out_app_flag == 1 multi-step trajectories). void write_cube(const std::string& file, const std::vector& comment, const int& natom, @@ -99,6 +105,7 @@ void write_cube(const std::string& file, const std::vector>& atom_pos, const std::vector& data, const int precision, + const bool append, const int ndata_line = 6); /** diff --git a/source/source_io/module_output/provenance_header.h b/source/source_io/module_output/provenance_header.h new file mode 100644 index 00000000000..654cbe03860 --- /dev/null +++ b/source/source_io/module_output/provenance_header.h @@ -0,0 +1,39 @@ +#ifndef MODULEIO_PROVENANCE_HEADER_H +#define MODULEIO_PROVENANCE_HEADER_H + +#include "source_base/formatter.h" +#include "source_main/version.h" + +#include +#include + +namespace ModuleIO +{ + +/// Local wall-clock timestamp "YYYY-MM-DD HH:MM:SS" for the provenance +/// header of output files. Call once per frame and pass the result to +/// provenance_header() so all files of the same frame carry one timestamp. +inline std::string local_timestamp() +{ + const std::time_t now = std::time(nullptr); + std::tm tm_local; + localtime_r(&now, &tm_local); + char time_buf[64]; + std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_local); + return std::string(time_buf); +} + +/// First two comment lines shared by per-step output files +/// (band*.txt, wfk*_nao.txt, eband_*.txt, ...): +/// "# ABACUS version: ..." and "# Written at ...". +/// Callers append their own step/energy lines after this block. +inline std::string provenance_header(const std::string& time_stamp) +{ + return FmtCore::format("# ABACUS version: %s\n# Written at %s\n", + VERSION, + time_stamp.c_str()); +} + +} // namespace ModuleIO + +#endif // MODULEIO_PROVENANCE_HEADER_H diff --git a/source/source_io/module_output/spin_tag.h b/source/source_io/module_output/spin_tag.h index 19562a55fe7..b48afdcf46e 100644 --- a/source/source_io/module_output/spin_tag.h +++ b/source/source_io/module_output/spin_tag.h @@ -22,6 +22,24 @@ inline std::string make_spin_tag(const int is, const int nspin) return " (spin down)"; } +/// Compact spin-channel label used in per-spin output files +/// (bands1.txt, wfk1s1_nao.txt, ...). Unlike make_spin_tag, which pads "up" +/// so both channels share one width in the running log, this form is +/// unpadded because it heads a file that holds a single channel. +/// Returns " (spin up)", " (spin down)" or an empty string. +inline std::string make_compact_spin_tag(const int is, const int nspin) +{ + if (nspin != 2 || is < 0) + { + return ""; + } + if (is == 0) + { + return " (spin up)"; + } + return " (spin down)"; +} + } // namespace ModuleIO #endif diff --git a/source/source_io/module_output/write_cube.cpp b/source/source_io/module_output/write_cube.cpp index 5c3d8e9af54..6fc55dc7954 100644 --- a/source/source_io/module_output/write_cube.cpp +++ b/source/source_io/module_output/write_cube.cpp @@ -23,6 +23,7 @@ void ModuleIO::write_vdata_palgrid(const Parallel_Grid& pgrid, const int out_fermi, const bool two_fermi, const bool reduce_all_pool, + const bool append, std::ofstream& ofs_running, const std::string& data_desc) { @@ -173,7 +174,8 @@ void ModuleIO::write_vdata_palgrid(const Parallel_Grid& pgrid, atom_charge, atom_pos, data_xyz_full, - precision); + precision, + append); ofs_running << " Write " << data_desc << " to file: " << fn << std::endl; @@ -199,6 +201,7 @@ void ModuleIO::write_cube(const std::string& file, const std::vector>& atom_pos, const std::vector& data, const int precision, + const bool append, const int ndata_line) { assert(comment.size() >= 2); @@ -222,7 +225,16 @@ void ModuleIO::write_cube(const std::string& file, assert(data.size() >= nx * ny * nz); - std::ofstream ofs(file); + // In append mode (out_app_flag == 1) each written ionic step contributes + // one complete cube frame; the previous frame always ends with a newline, + // so the appended frame starts on a fresh line. Otherwise the file is + // truncated and holds a single frame. + std::ios_base::openmode open_mode = std::ios::out | std::ios::trunc; + if (append) + { + open_mode = std::ios::out | std::ios::app; + } + std::ofstream ofs(file, open_mode); for (int i = 0; i < 2; ++i) { diff --git a/source/source_io/module_output/write_orb_info.cpp b/source/source_io/module_output/write_orb_info.cpp index 665e12e9f86..8f6117a6a9c 100644 --- a/source/source_io/module_output/write_orb_info.cpp +++ b/source/source_io/module_output/write_orb_info.cpp @@ -7,7 +7,7 @@ void ModuleIO::write_orb_info(const UnitCell* ucell) { std::stringstream os; - os << PARAM.globalv.global_out_dir << "Orbital"; + os << PARAM.globalv.global_out_dir << "nao.txt"; std::ofstream out(os.str().c_str()); out << std::setw(5) << "#io" << std::setw(8) << "spec" diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index 1fa8f482855..6afac3a9d3d 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -484,12 +484,17 @@ struct Input_para std::vector out_mat_tk = {0, 8}; ///< output T(k) matrix in local basis. std::vector out_mat_l = {0, 8}; ///< output L matrix in local basis. std::vector out_mat_hs2 = {0, 8}; ///< legacy alias for text H(R) and S(R) output - std::vector out_mat_h_t = {0, 8}; ///< output kinetic energy T(R) matrix - std::vector out_mat_h_vnl = {0, 8}; ///< output nonlocal pseudopotential Vnl(R) matrix - std::vector out_mat_h_vl = {0, 8}; ///< output local pseudopotential Vl(R) matrix - std::vector out_mat_h_vh = {0, 8}; ///< output Hartree Vh(R) matrix - std::vector out_mat_h_vxc = {0, 8}; ///< output XC Vxc(R) matrix - std::vector out_mat_h_exx = {0, 8}; ///< output exact-exchange Vexx(R) matrix + std::vector out_mat_h_t = {0, 8}; ///< legacy alias for out_mat_tk (kinetic T(k) matrix) + std::vector out_mat_vnlk = {0, 8}; ///< output nonlocal pseudopotential Vnl(k) matrix + std::vector out_mat_vlk = {0, 8}; ///< output local pseudopotential Vl(k) matrix + std::vector out_mat_vhk = {0, 8}; ///< output Hartree Vh(k) matrix + std::vector out_mat_vxck = {0, 8}; ///< output XC Vxc(k) matrix + std::vector out_mat_exxk = {0, 8}; ///< output exact-exchange Vexx(k) matrix + std::vector out_mat_h_vnl = {0, 8}; ///< legacy alias for out_mat_vnlk + std::vector out_mat_h_vl = {0, 8}; ///< legacy alias for out_mat_vlk + std::vector out_mat_h_vh = {0, 8}; ///< legacy alias for out_mat_vhk + std::vector out_mat_h_vxc = {0, 8}; ///< legacy alias for out_mat_vxck + std::vector out_mat_h_exx = {0, 8}; ///< legacy alias for out_mat_exxk std::vector out_mat_dh = {0, 8}; ///< output dH/dR matrices with precision std::vector out_mat_dh_t = {0, 8}; ///< output kinetic dH/dR (dT/dR) matrices std::vector out_mat_dh_vl = {0, 8}; ///< output local pseudopotential dH/dR (dV^L/dR) matrices @@ -500,12 +505,15 @@ struct Input_para bool out_mat_xc = false; ///< output exchange-correlation matrix in ///< KS-orbital representation. std::vector out_mat_xc2 = {0, 8}; ///< output Vxc(R) matrix with precision - bool out_eband_terms = false; ///< output the band energy terms separately + bool out_eband = false; ///< output the band energy terms separately + bool out_eband_terms_compat = false; ///< legacy alias out_eband_terms for out_eband bool out_hr_npz = false; ///< output H(R) matrix in npz format bool out_hsr_npz = false; ///< output H(R) and S(R) matrices in npz format bool out_dm_npz = false; ///< output DM(R) matrix in npz format - bool out_app_flag = true; ///< whether output r(R), H(R), S(R), T(R), and dH(R) matrices - ///< in an append manner during MD liuyu 2023-03-20 + int out_app_flag = 0; ///< 0: fixed filename, truncated and overwritten at every write; + ///< 1: append to the same fixed file; 2: write to subdirectory + ///< (PSI/, CHG/, POT/, ELF/, DOS/, DIP/, MAT/) with a g{step} suffix + ///< liuyu 2023-03-20, modified 2025-10-16 int out_ndigits = 8; ///< Assuming 8 digits precision is needed for matrices output std::vector out_mat_t = {0, 8}; ///< output T(R) matrix with precision bool out_element_info = false; ///< output information of all elements diff --git a/source/source_io/module_parameter/read_inp_out.cpp b/source/source_io/module_parameter/read_inp_out.cpp index 9830989fa2b..6ced6b5a295 100644 --- a/source/source_io/module_parameter/read_inp_out.cpp +++ b/source/source_io/module_parameter/read_inp_out.cpp @@ -16,6 +16,8 @@ void ReadInput::item_output() item.category = "Output information"; item.type = "Integer"; item.description = "Controls the output interval in ionic steps. When set to a positive integer, information such as charge density, local potential, electrostatic potential, Hamiltonian matrix, overlap matrix, density matrix, Mulliken population analysis, and structure files (STRU{istep} or STRU{istep}.cif, when out_stru is 1 or 2) is printed every n ionic steps." + "\n\nThis parameter only controls how often a write happens, not how many files are kept: with out_app_flag = 0 each write overwrites the same fixed filename, with out_app_flag = 1 records are appended to one fixed filename, and with out_app_flag = 2 each written step gets its own g{step} file." + "\n\nIn molecular dynamics calculations, out_freq_ion controls the cadence of the structure frames written when out_stru is positive; when out_freq_ion is 0, md_restartfreq is used as the structure-output frequency. The MD restart file Restart_md.txt always follows md_restartfreq and is independent of out_freq_ion." "\n\nFor DFT+U calculations with out_occ_mat = 1, a separate occupation-matrix file occ_matg{geom_step}.txt is created for every output ionic step. The converged (or final, if not converged) electronic step is always recorded in that file; when out_freq_elec is set smaller than scf_nmax, additional electronic steps are appended as separate sections, each labelled only with the electronic-step number." "\n\n[NOTE] In RT-TDDFT calculations, this parameter is inactive; output frequency is instead controlled by out_freq_td."; item.default_value = "0"; @@ -81,17 +83,11 @@ void ReadInput::item_output() - nspin = 2: `taus1.cube`, and `taus2.cube`; - nspin = 4: `taus1.cube`, `taus2.cube`, `taus3.cube`, and `taus4.cube`; - 2: On top of 1, also output the initial charge density files. The files are named as: - - out_freq_ion = 0: - - nspin = 1: `chg_ini.cube`; - - nspin = 2: `chgs1_ini.cube` and `chgs2_ini.cube`; - - nspin = 4: `chgs1_ini.cube`, `chgs2_ini.cube`, `chgs3_ini.cube`, and `chgs4_ini.cube`; - - output at every step (overwrite same file) - - out_freq_ion > 0: - - nspin = 1: `chgg{geom_step}_ini.cube` (e.g., `chgg1_ini.cube`); - - nspin = 2: `chgs1g{geom_step}_ini.cube` and `chgs2g{geom_step}_ini.cube`; - - nspin = 4: `chgs1g{geom_step}_ini.cube`, `chgs2g{geom_step}_ini.cube`, `chgs3g{geom_step}_ini.cube`, and `chgs4g{geom_step}_ini.cube`. - - output every out_freq_ion steps - Here, {geom_step} denotes the geometry step index, starting from 1 (geom_step = istep + 1). + - nspin = 1: `chg_ini.cube`; + - nspin = 2: `chgs1_ini.cube` and `chgs2_ini.cube`; + - nspin = 4: `chgs1_ini.cube`, `chgs2_ini.cube`, `chgs3_ini.cube`, and `chgs4_ini.cube`; + - out_freq_ion = 0 writes them at every step; out_freq_ion > 0 writes them every out_freq_ion steps. + Only out_app_flag = 2 inserts a one-based geometry-step index before `_ini` (e.g., `chgg1_ini.cube`, `chgs1g1_ini.cube`); with out_app_flag = 0 the fixed file is truncated and overwritten at every write, and with out_app_flag = 1 each written step appends one cube frame. - -1: Disable the charge density auto-back-up file `{suffix}-CHARGE-DENSITY.restart`, useful for large systems. The second integer controls the precision of the charge density output. If not given, `3` is used as default. For restarting from this file and other high-precision calculations, `10` is recommended. @@ -142,17 +138,11 @@ In molecular dynamics simulations, the output frequency is controlled by out_fre * nspin = 4: pots1.cube, pots2.cube, pots3.cube, and pots4.cube * 2: Output the electrostatic potential on real space grids into OUT.{suffix}/pot_es.cube. The Python script named tools/02_postprocessing/average_pot/aveElecStatPot.py can be used to calculate the average electrostatic potential along the z-axis and outputs it into ElecStaticPot_AVE. Please note that the total local potential refers to the local component of the self-consistent potential, excluding the non-local pseudopotential. The distinction between the local potential and the electrostatic potential is as follows: local potential = electrostatic potential + XC potential. * 3: Apart from 1, also output the total local potential of the initial charge density. The files are named as: - * out_freq_ion = 0: * nspin = 1: `pot_ini.cube`; * nspin = 2: `pots1_ini.cube` and `pots2_ini.cube`; * nspin = 4: `pots1_ini.cube`, `pots2_ini.cube`, `pots3_ini.cube`, and `pots4_ini.cube`; - * output at every step (overwrite same file) - * out_freq_ion > 0: - * nspin = 1: `potg{geom_step}_ini.cube` (e.g., `potg1_ini.cube`); - * nspin = 2: `pots1g{geom_step}_ini.cube` and `pots2g{geom_step}_ini.cube`; - * nspin = 4: `pots1g{geom_step}_ini.cube`, `pots2g{geom_step}_ini.cube`, `pots3g{geom_step}_ini.cube`, and `pots4g{geom_step}_ini.cube`. - * output every out_freq_ion steps - Here, {geom_step} denotes the geometry step index, starting from 1 (geom_step = istep + 1). + * out_freq_ion = 0 writes them at every step; out_freq_ion > 0 writes them every out_freq_ion steps. + Only out_app_flag = 2 inserts a one-based geometry-step index before `_ini` (e.g., `potg1_ini.cube`, `pots1g1_ini.cube`); with out_app_flag = 0 the fixed file is truncated and overwritten at every write, and with out_app_flag = 1 each written step appends one cube frame. The optional second integer controls the output precision. If not provided, the default precision is 8. @@ -189,7 +179,7 @@ In molecular dynamics calculations, the output frequency is controlled by out_fr * 1: Output the implicit-solvent fields of the last solvent correction on real space grids into files in the folder OUT.{suffix}, for imp_sol 1 (the original implicit solvation model) and imp_sol 2 (SCCS): * `sol_eps.cube`: the dielectric function epsilon(r), 1 in the solute and the bulk permittivity in the solvent. * `sol_cavity.cube`: the solute boundary s(r), 1 in the solute and 0 in the solvent; for imp_sol 1 it is 1 minus the solvent shape function (written only when eb_k > 1). - With out_freq_ion > 0 the files are written every out_freq_ion ionic steps with the geometry index in the name, e.g. `sol_epsg1.cube`; with out_freq_ion = 0 they are overwritten at every step. Nothing is written while SCCS waits for its delayed start (sccs_start_drho). + With out_freq_ion > 0 the files are written every out_freq_ion ionic steps; with out_freq_ion = 0 they are written at every step. In out_app_flag 1, fixed names such as `sol_eps.cube` hold one complete frame per written step: ionic step 0 truncates the file and later steps append. In modes 0 and 2, positive out_freq_ion adds the geometry index, e.g. `sol_epsg1.cube`; frequency zero overwrites the fixed name at each step. Solvent files stay in OUT.{suffix} in all modes. Nothing is written while SCCS waits for its delayed start (sccs_start_drho). The optional second integer controls the output precision. If not provided, the default precision is 8.)"; item.default_value = "0"; @@ -247,15 +237,15 @@ The optional second integer controls the output precision. If not provided, the item.annotation = ">0 output density matrix DM(k) for each k-point"; item.category = "Output information"; item.type = R"(Boolean \[Integer\](optional))"; - item.description = R"(Whether to output the density matrix for each k-point into files in the folder OUT.${suffix}. For current develop versions, out_dmk writes *_nao.txt files and includes a g{istep} index in the file name: + item.description = R"(Whether to output the density matrix for each k-point into files in the folder OUT.${suffix}. For current develop versions, out_dmk writes *_nao.txt files: * For gamma only case: - * nspin = 1 and 4: dmg1_nao.txt; - * nspin = 2: dms1g1_nao.txt and dms2g1_nao.txt for the two spin channels. + * nspin = 1 and 4: dm_nao.txt; + * nspin = 2: dms1_nao.txt and dms2_nao.txt for the two spin channels. * For multi-k points case: - * nspin = 1 and 4: dmk1g1_nao.txt, dmk2g1_nao.txt, ...; - * nspin = 2: dmk1s1g1_nao.txt... and dmk1s2g1_nao.txt... for the two spin channels. + * nspin = 1 and 4: dmk1_nao.txt, dmk2_nao.txt, ...; + * nspin = 2: dmk1s1_nao.txt... and dmk1s2_nao.txt... for the two spin channels. - Here, g{istep} denotes the geometry/step index in the output file name. + The file names are fixed for out_app_flag = 0 (overwritten at every write) and out_app_flag = 1 (records appended). Only out_app_flag = 2 inserts a one-based geometry-step suffix before _nao (for example dmk1g1_nao.txt). [NOTE] Version difference (develop vs 3.10-LTS): * In develop, out_dmk supports both gamma-only and multi-k-point density-matrix output. @@ -286,7 +276,7 @@ The optional second integer controls the output precision. If not provided, the item.annotation = "output density matrix DM(R) with respect to lattice vector R (with precision 8)"; item.category = "Output information"; item.type = R"(Boolean \[Integer\](optional))"; - item.description = R"(Whether to output the density matrix with Bravias lattice vector R index into files in the folder OUT.${suffix}. The files are named as dmr{s}{spin index}{g}{geometry index}{_nao} + {".csr"}. Here, 's' refers to spin, where s1 means spin up channel while s2 means spin down channel, and the sparse matrix format 'csr' is mentioned in out_hsr. Finally, if out_app_flag is set to false, the file name contains the optional 'g' index for each ionic step that may have different geometries, and if out_app_flag is set to true, the density matrix with respect to Bravias lattice vector R accumulates during ionic steps: + item.description = R"(Whether to output the density matrix with Bravias lattice vector R index into files in the folder OUT.${suffix}. The files are named as dmr{s}{spin index}{g}{geometry index}{_nao} + {".csr"}. Here, 's' refers to spin, where s1 means spin up channel while s2 means spin down channel, and the sparse matrix format 'csr' is mentioned in out_hsr. Finally, the file name is fixed for out_app_flag = 0 (truncated and overwritten at every write) and out_app_flag = 1 (the first written step truncates it and later steps append complete records); only out_app_flag = 2 inserts a one-based geometry-step index for each ionic step that may have different geometries: * nspin = 1: dmrs1_nao.csr; * nspin = 2: dmrs1_nao.csr and dmrs2_nao.csr for the two spin channels. @@ -335,9 +325,9 @@ Available values are: The file-name pattern is `wfk{k}[s{spin}][g{geometry step}][e{electronic iteration}]_pw.txt` for `out_wfc_pw=1` and `wfk{k}[s{spin}][g{geometry step}][e{electronic iteration}]_pw.dat` for `out_wfc_pw=2`. All PW output files include a `k*` label, including Gamma-only calculations. Without geometry-step or electronic-iteration indices, representative names are `wfk1_pw.txt` or `wfk1_pw.dat` for `nspin=1`, `wfk1s1_pw.txt` and `wfk1s2_pw.txt` or their `.dat` equivalents for `nspin=2`, and `wfk1s4_pw.txt` or `wfk1s4_pw.dat` for `nspin=4`. -With `out_freq_ion=0`, files are written only when the electronic calculation converges or reaches `scf_nmax`; no `g*` or `e*` index is added. During structural relaxation or molecular dynamics, later ionic steps overwrite the same unindexed files. With `out_freq_ion` > 0, output is restricted to the ionic steps selected by `out_freq_ion` and is written when the electronic iteration is a multiple of `out_freq_elec`, when the calculation converges, or when it reaches `scf_nmax`. Both `g*` and `e*` indices are then added, including for a static `calculation=scf` or `calculation=nscf` run. +With `out_freq_ion=0`, files are written only when the electronic calculation converges or reaches `scf_nmax`; no `g*` or `e*` index is added. During structural relaxation or molecular dynamics with `out_app_flag=1`, the first written step truncates the unindexed files and later ionic steps append one frame per step (mirroring `out_wfc_lcao`); with `out_app_flag=0` the unindexed files are overwritten each step. With `out_freq_ion` > 0, output is restricted to the ionic steps selected by `out_freq_ion`. For `out_app_flag=0` or `2`, output is written when the electronic iteration is a multiple of `out_freq_elec`, when the calculation converges, or when it reaches `scf_nmax`; for `out_app_flag=1` only the last electronic iteration of each selected ionic step is written (out_freq_elec is ignored), so the appended file holds exactly one frame per step. Both `g*` and `e*` indices are then added only when `out_app_flag=2`; with `out_app_flag=0` the fixed files are truncated and overwritten at every write, and with `out_app_flag=1` they accumulate one frame per written step. -With `init_wfc file binary`, ABACUS reads only unindexed binary `wf*_pw.dat` files from `read_file_dir`. Such directly reusable files are normally generated with `out_wfc_pw=2` and `out_freq_ion=0`. Text `wf*_pw.txt` files and files containing `g*` or `e*` indices are not matched automatically. +With `init_wfc file binary`, ABACUS reads only unindexed binary `wf*_pw.dat` files from `read_file_dir`, starting from the first frame. Such directly reusable files are normally generated with `out_wfc_pw=2` and `out_freq_ion=0`; multi-frame files accumulated under `out_app_flag=1` are trajectories and only their first frame is visible to the reader. Text `wf*_pw.txt` files and files containing `g*` or `e*` indices are not matched automatically. [NOTE] In the 3.10-LTS version, the binary files are named `WAVEFUNC1.dat`, `WAVEFUNC2.dat`, etc.)"; item.default_value = "0"; @@ -353,7 +343,7 @@ With `init_wfc file binary`, ABACUS reads only unindexed binary `wf*_pw.dat` fil item.annotation = "ouput LCAO wave functions, 0, no output 1: text, 2: binary"; item.category = "Output information"; item.type = "Integer"; - item.description = R"(Whether to output the electronic wavefunction coefficients into files and store them in the folder OUT.${suffix}. The files are named as wf{s}{spin index}{k(optional)}{k-point index}{g(optional)}{geometry index1}{_nao} + {".txt"/".dat"}. Here, 's' refers to spin, where s1 means spin up channel while s2 means spin down channel, and 's12' refer to spinor wave functions that contains both spin channels with spin-orbital coupling or noncollinear calculations enabled. In addition, if 'gamma_only' is set to 0, then the optinoal k-point sampling index appears with the k-point index attached to the electronic wave function file names. Finally, if out_app_flag is set to false, the file name contains the optional 'g' index for each ionic step that may have different geometries, and if out_app_flag is set to true, the wave functions accumulate during ionic steps. If the out_app_flag is set to false, a new folder named WFC will be created, and the wave function files will be saved into it. + item.description = R"(Whether to output the electronic wavefunction coefficients into files and store them in the folder OUT.${suffix} (or its PSI/ subdirectory when out_app_flag is 2). The files are named as wf{s}{spin index}{k(optional)}{k-point index}{g(optional)}{geometry index1}{_nao} + {".txt"/".dat"}. Here, 's' refers to spin, where s1 means spin up channel while s2 means spin down channel, and 's12' refer to spinor wave functions that contains both spin channels with spin-orbital coupling or noncollinear calculations enabled. In addition, if 'gamma_only' is set to 0, then the optinoal k-point sampling index appears with the k-point index attached to the electronic wave function file names. Finally, only out_app_flag = 2 adds the optional 'g' index for each ionic step that may have different geometries, and the wave function files are saved into the PSI/ subdirectory of OUT.${suffix}; with out_app_flag = 0 the fixed file is truncated and overwritten at every write, and with out_app_flag = 1 the wave functions accumulate during ionic steps. * 0: no output * 1: (txt format) * gamma-only: wfs1_nao.txt or wfs2_nao.txt, ...; @@ -387,8 +377,10 @@ Also controled by out_freq_ion and out_app_flag. item.description = R"(Whether to output the density of states (DOS) and projected density of states (PDOS). For more information, refer to the dos.md. * 0: no output * 1: output the density of states (DOS) and projected density of states (PDOS, LCAO only) - * nspin=1 or 4: doss1g{geom}.txt and pdoss1g{geom}.txt (PDOS is LCAO only), where geom is the geometry index when cell changes or ions move. PW and LCAO calculations use the same naming. - * nspin=2: doss1/doss2 and pdoss1/pdoss2 files for two spin channels. + * With out_app_flag = 0: fixed-name files (`doss{s}.txt`, `pdoss{s}.txt`) are truncated and overwritten at every write. + * With out_app_flag = 1: the same fixed-name files are reused; the first written step truncates them and later steps append one record per step (prefixed with a `# ionic step N` header, one-based). + * With out_app_flag = 2: per-step files are named `doss{s}g{geom}.txt` and `pdoss{s}g{geom}.txt` where `{geom}` is the one-based geometry index (e.g. `doss1g3.txt` for the third ionic step). + * nspin=1 or 4: one spin channel (`s1`). nspin=2: two channels (`s1`, `s2`). PW and LCAO use the same naming. Note: values 2 and 3 are no longer supported. Setting out_dos to 2 or 3 will raise an error.)"; item.default_value = "0"; item.unit = ""; @@ -502,10 +494,11 @@ Note: values 2 and 3 are no longer supported. Setting out_dos to 2 or 3 will rai item.annotation = "output the structure files per ion step"; item.category = "Output information"; item.type = "Integer"; - item.description = "Controls the output of structure files per ionic step. The files are written to the OUT.{suffix}/ directory. Each file corresponds to the structure at RELAX STEP ${istep} (for scf/nscf this is the single step), i.e., the structure for which that step's energy was computed (before the relax move), and includes a header comment with the ABACUS version, timestamp, energy, and stress tensor. When out_freq_ion is positive, the numbered files STRU{istep} (or STRU{istep}.cif) are written every out_freq_ion steps during geometry relaxation; when out_freq_ion is 0, no numbered files are output. This parameter is effective for scf/nscf/relax/cell-relax; for scf/nscf only STRU_FINAL (or STRU_FINAL.cif) is written, and structure output is disabled by default unless out_stru is set explicitly. Molecular dynamics structure output is instead controlled by md_restartfreq (STRU_MD_*).\n" + item.description = "Controls the output of structure files per ionic step. The files are written to the OUT.{suffix}/ directory (or the OUT.{suffix}/STRUC/ subdirectory when out_app_flag is 2). Each file corresponds to the structure at the current ionic step; in the relaxation path the header comment records the ABACUS version, timestamp, energy, and stress tensor, while MD frames written to a cumulative file are prefixed with a one-based \"# MD step N\" marker. When out_freq_ion is positive and out_app_flag is not 0, the numbered files STRU{istep} (or STRU{istep}.cif) are written every out_freq_ion steps; when out_freq_ion is 0 or out_app_flag is 0, no numbered files are output (with out_app_flag = 0 the latest structure is still kept in STRU_NOW / STRU_NOW.cif). This parameter is effective for scf/nscf/relax/cell-relax and md; for md only values 0 and 1 are supported, because MD structure frames are written in ABACUS STRU format and read back by the restart logic, so value 2 (CIF) is rejected. For scf/nscf only STRU_FINAL (or STRU_FINAL.cif) is written.\n" " - 0: No structure files are output.\n" - " - 1: ABACUS STRU format files are output. The latest structure is written to STRU_NOW (overwritten each step), the numbered file STRU{istep} (e.g., STRU1, STRU2) is written every out_freq_ion steps (when out_freq_ion is positive), and the final converged structure is written to STRU_FINAL. No CIF files are output.\n" - " - 2: CIF format files are output. The latest structure is written to STRU_NOW.cif (overwritten each step), the numbered file STRU{istep}.cif (e.g., STRU1.cif, STRU2.cif) is written every out_freq_ion steps (when out_freq_ion is positive), and the final converged structure is written to STRU_FINAL.cif. No non-CIF files are output.\n" + " - 1: ABACUS STRU format files are output. In scf/nscf/relax/cell-relax the latest structure is written to STRU_NOW (overwritten each step), the numbered file STRU{istep} (e.g., STRU1, STRU2) is written every out_freq_ion steps (when out_freq_ion is positive), and the final converged structure is written to STRU_FINAL. No CIF files are output. When out_app_flag is 1, the numbered STRU{istep} files are replaced by a single fixed-name STRU file that appends one structure block (with a one-based \"# Ionic Step N\" or \"# MD step N\" comment) per written step.\n" + " - 2: CIF format files are output. The latest structure is written to STRU_NOW.cif (overwritten each step), the numbered file STRU{istep}.cif (e.g., STRU1.cif, STRU2.cif) is written every out_freq_ion steps (when out_freq_ion is positive), and the final converged structure is written to STRU_FINAL.cif. No non-CIF files are output. When out_app_flag is 1, the numbered files are replaced by a fixed-name STRU.cif overwritten each written step. Not supported for calculation = md, which always writes ABACUS STRU frames.\n" + "[NOTE] MD structure output differs from the ionic-relaxation path: frames are written every out_freq_ion steps, falling back to md_restartfreq when out_freq_ion is 0; out_app_flag = 0 keeps the numbered STRU{istep} files (STRU1 is the initial geometry) and no STRU_NOW is written; out_app_flag = 2 writes the numbered files under STRUC/; out_app_flag = 1 writes one cumulative STRU file that is truncated on the first frame of a fresh run and appended with a \"# MD step N\" marker afterwards. MD never writes STRU_NOW or STRU_FINAL. When a legacy STRU/ directory exists in the output directory, MD falls back to numbered STRU{istep} files inside that directory even if out_app_flag = 1.\n" "[NOTE] For backward compatibility, true/false (case insensitive) are accepted and converted to 1/0."; item.default_value = "1"; item.unit = ""; @@ -608,8 +601,9 @@ The output is also controlled by out_freq_ion and out_app_flag. For more informa * Multi-k, nspin = 2: hk1s1_nao.txt and hk1s2_nao.txt for the two spin channels of the Hamiltonian matrix, and sk1_nao.txt for the overlap matrix at the first k-point. Only one overlap matrix is written because it is identical for both spin channels. * Multi-k, nspin = 4: hk1s4_nao.txt for the spinor Hamiltonian matrix and sk1_nao.txt for the spinor overlap matrix at the first k-point. For binary output, the same names use the .dat suffix. Each native binary record contains the matrix dimension as an int followed by the row-major upper triangle. Gamma-only elements are doubles; multi-k and spinor elements are pairs of doubles containing the real and imaginary parts. Native integer representation and byte order are used. -When out_app_flag is true, the first ionic step truncates the file and later steps append complete records. -When out_app_flag is false, g followed by the one-based ionic-step index is inserted before _nao, for example hk1s1g1_nao.txt. +When out_app_flag is 0, the fixed file is truncated and overwritten at every write. +When out_app_flag is 1, the first ionic step truncates the file and later steps append complete records. +When out_app_flag is 2, g followed by the one-based ionic-step index is inserted before _nao, for example hk1s1g1_nao.txt. [NOTE] In the 3.10-LTS version, the file names are data-0-H and data-0-S, etc.)"; item.default_value = "0 8"; @@ -682,7 +676,7 @@ When out_app_flag is false, g followed by the one-based ionic-step index is inse For multi-k calculations, the output contains the individual real-space blocks stored for the Bravais lattice vectors R. For gamma-only calculations, the internal real-space contributions are folded into a single R = (0, 0, 0) block. This folded result cannot recover the original R-resolved contributions or interpolate arbitrary k points. Terms added only while constructing H(k) are not guaranteed to be present. -For binary output, each file uses the same basename as text output with a .dat suffix. Every native record contains the zero-based ionic step, matrix dimension, and number of R blocks as ints. Each R block contains three int coordinates, an int nonzero count, native double values (real/imaginary double pairs for complex matrices), int column indices, and long long row pointers. Native integer representation and byte order are used. When out_app_flag is true, the first ionic step truncates the file and later steps append complete records. +For binary output, each file uses the same basename as text output with a .dat suffix. Every native record starts with a five-int versioned header: the magic 0x48535242, the format version (currently 2), the one-based ionic step, the matrix dimension, and the number of R blocks. Files written before this versioned header was introduced (v3.11.0-beta9/beta10 stored a zero-based step, and development builds after that stored a one-based step without any version marker) cannot be interpreted from their content alone, so readers must not assume the current layout for old .dat files. Each R block contains three int coordinates, an int nonzero count, native double values (real/imaginary double pairs for complex matrices), int column indices, and long long row pointers. Native integer representation and byte order are used. When out_app_flag is 0, the fixed file is truncated and overwritten at every write; when out_app_flag is 1, the first ionic step truncates the file and later steps append complete records. [NOTE] In the 3.10-LTS version, the file names are data-HR-sparse_SPIN0.csr and data-SR-sparse_SPIN0.csr, etc.)"; item.default_value = "0 8"; @@ -1053,11 +1047,10 @@ For binary output, each file uses the same basename as text output with a .dat s } { Input_Item item("out_mat_h_t"); - item.annotation = "output kinetic energy T(R) matrix"; + item.annotation = "legacy alias for out_mat_tk"; item.category = "Output information"; item.type = "Integer"; - item.description = "Whether to print files containing the kinetic energy matrix T(R) in CSR format." - "\n\nSee out_hsr for format details."; + item.description = "Legacy alias for out_mat_tk, which outputs the kinetic energy matrix T(k) for each k-point in the NAO basis. The optional second integer controls text precision. If both out_mat_tk and out_mat_h_t are present, out_mat_tk takes precedence."; item.default_value = "0 8"; item.unit = "Ry"; item.read_value = [](const Input_Item& item, Parameter& para) { @@ -1068,21 +1061,47 @@ For binary output, each file uses the same basename as text output with a .dat s catch (const std::invalid_argument& e) { ModuleBase::WARNING("Input", "out_mat_h_t enable flag must be 0/1, using default 0"); } + if (count >= 2) try { para.input.out_mat_h_t[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } + }; + sync_intvec(input.out_mat_h_t, 2, 0); + this->add_item(item); + } + { + Input_Item item("out_mat_vnlk"); + item.annotation = "output nonlocal pseudopotential Vnl(k) matrix"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Whether to print the nonlocal pseudopotential matrix Vnl(k) for each k-point in the NAO basis, into text files OUT.${suffix}/vnlk*_nao.txt." + "\n\nThe optional second integer controls text precision (default 8)."; + item.default_value = "0 8"; + item.unit = "Ry"; + item.read_value = [](const Input_Item& item, Parameter& para) { + const size_t count = item.get_size(); + try { + para.input.out_mat_vnlk[0] = assume_as_boolean(item.str_values[0]); + } + catch (const std::invalid_argument& e) { + ModuleBase::WARNING("Input", "out_mat_vnlk enable flag must be 0/1, using default 0"); + } + if (count >= 2) try { para.input.out_mat_vnlk[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } }; item.check_value = [](const Input_Item& item, const Parameter& para) { - if (para.input.out_mat_h_t[0] && para.input.nspin == 4) - ModuleBase::WARNING_QUIT("ReadInput", "out_mat_h_t is not available for nspin = 4"); + if (para.input.out_mat_vnlk[0] && para.input.nspin == 4) + ModuleBase::WARNING_QUIT("ReadInput", "out_mat_vnlk is not available for nspin = 4"); }; - sync_intvec(input.out_mat_h_t, 2, 0); + sync_intvec(input.out_mat_vnlk, 2, 0); this->add_item(item); } { Input_Item item("out_mat_h_vnl"); - item.annotation = "output nonlocal pseudopotential Vnl(R) matrix"; + item.annotation = "legacy alias for out_mat_vnlk"; item.category = "Output information"; item.type = "Integer"; - item.description = "Whether to print files containing the nonlocal pseudopotential matrix Vnl(R) in CSR format." - "\n\nSee out_hsr for format details."; + item.description = "Legacy alias for out_mat_vnlk, which outputs the nonlocal pseudopotential matrix Vnl(k) for each k-point in the NAO basis. The optional second integer controls text precision. If both out_mat_vnlk and out_mat_h_vnl are present, out_mat_vnlk takes precedence."; item.default_value = "0 8"; item.unit = "Ry"; item.read_value = [](const Input_Item& item, Parameter& para) { @@ -1093,21 +1112,47 @@ For binary output, each file uses the same basename as text output with a .dat s catch (const std::invalid_argument& e) { ModuleBase::WARNING("Input", "out_mat_h_vnl enable flag must be 0/1, using default 0"); } + if (count >= 2) try { para.input.out_mat_h_vnl[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } + }; + sync_intvec(input.out_mat_h_vnl, 2, 0); + this->add_item(item); + } + { + Input_Item item("out_mat_vlk"); + item.annotation = "output local pseudopotential Vl(k) matrix"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Whether to print the local pseudopotential matrix Vl(k) for each k-point in the NAO basis, into text files OUT.${suffix}/vlk*_nao.txt." + "\n\nThe optional second integer controls text precision (default 8)."; + item.default_value = "0 8"; + item.unit = "Ry"; + item.read_value = [](const Input_Item& item, Parameter& para) { + const size_t count = item.get_size(); + try { + para.input.out_mat_vlk[0] = assume_as_boolean(item.str_values[0]); + } + catch (const std::invalid_argument& e) { + ModuleBase::WARNING("Input", "out_mat_vlk enable flag must be 0/1, using default 0"); + } + if (count >= 2) try { para.input.out_mat_vlk[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } }; item.check_value = [](const Input_Item& item, const Parameter& para) { - if (para.input.out_mat_h_vnl[0] && para.input.nspin == 4) - ModuleBase::WARNING_QUIT("ReadInput", "out_mat_h_vnl is not available for nspin = 4"); + if (para.input.out_mat_vlk[0] && para.input.nspin == 4) + ModuleBase::WARNING_QUIT("ReadInput", "out_mat_vlk is not available for nspin = 4"); }; - sync_intvec(input.out_mat_h_vnl, 2, 0); + sync_intvec(input.out_mat_vlk, 2, 0); this->add_item(item); } { Input_Item item("out_mat_h_vl"); - item.annotation = "output local pseudopotential Vl(R) matrix"; + item.annotation = "legacy alias for out_mat_vlk"; item.category = "Output information"; item.type = "Integer"; - item.description = "Whether to print files containing the local pseudopotential matrix Vl(R) in CSR format." - "\n\nSee out_hsr for format details."; + item.description = "Legacy alias for out_mat_vlk, which outputs the local pseudopotential matrix Vl(k) for each k-point in the NAO basis. The optional second integer controls text precision. If both out_mat_vlk and out_mat_h_vl are present, out_mat_vlk takes precedence."; item.default_value = "0 8"; item.unit = "Ry"; item.read_value = [](const Input_Item& item, Parameter& para) { @@ -1118,21 +1163,47 @@ For binary output, each file uses the same basename as text output with a .dat s catch (const std::invalid_argument& e) { ModuleBase::WARNING("Input", "out_mat_h_vl enable flag must be 0/1, using default 0"); } + if (count >= 2) try { para.input.out_mat_h_vl[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } + }; + sync_intvec(input.out_mat_h_vl, 2, 0); + this->add_item(item); + } + { + Input_Item item("out_mat_vhk"); + item.annotation = "output Hartree Vh(k) matrix"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Whether to print the Hartree matrix Vh(k) for each k-point in the NAO basis, into text files OUT.${suffix}/vhk*_nao.txt." + "\n\nThe optional second integer controls text precision (default 8)."; + item.default_value = "0 8"; + item.unit = "Ry"; + item.read_value = [](const Input_Item& item, Parameter& para) { + const size_t count = item.get_size(); + try { + para.input.out_mat_vhk[0] = assume_as_boolean(item.str_values[0]); + } + catch (const std::invalid_argument& e) { + ModuleBase::WARNING("Input", "out_mat_vhk enable flag must be 0/1, using default 0"); + } + if (count >= 2) try { para.input.out_mat_vhk[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } }; item.check_value = [](const Input_Item& item, const Parameter& para) { - if (para.input.out_mat_h_vl[0] && para.input.nspin == 4) - ModuleBase::WARNING_QUIT("ReadInput", "out_mat_h_vl is not available for nspin = 4"); + if (para.input.out_mat_vhk[0] && para.input.nspin == 4) + ModuleBase::WARNING_QUIT("ReadInput", "out_mat_vhk is not available for nspin = 4"); }; - sync_intvec(input.out_mat_h_vl, 2, 0); + sync_intvec(input.out_mat_vhk, 2, 0); this->add_item(item); } { Input_Item item("out_mat_h_vh"); - item.annotation = "output Hartree Vh(R) matrix"; + item.annotation = "legacy alias for out_mat_vhk"; item.category = "Output information"; item.type = "Integer"; - item.description = "Whether to print files containing the Hartree matrix Vh(R) in CSR format." - "\n\nSee out_hsr for format details."; + item.description = "Legacy alias for out_mat_vhk, which outputs the Hartree matrix Vh(k) for each k-point in the NAO basis. The optional second integer controls text precision. If both out_mat_vhk and out_mat_h_vh are present, out_mat_vhk takes precedence."; item.default_value = "0 8"; item.unit = "Ry"; item.read_value = [](const Input_Item& item, Parameter& para) { @@ -1143,21 +1214,47 @@ For binary output, each file uses the same basename as text output with a .dat s catch (const std::invalid_argument& e) { ModuleBase::WARNING("Input", "out_mat_h_vh enable flag must be 0/1, using default 0"); } + if (count >= 2) try { para.input.out_mat_h_vh[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } + }; + sync_intvec(input.out_mat_h_vh, 2, 0); + this->add_item(item); + } + { + Input_Item item("out_mat_vxck"); + item.annotation = "output exchange-correlation Vxc(k) matrix"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Whether to print the exchange-correlation matrix Vxc(k) for each k-point in the NAO basis, into text files OUT.${suffix}/vxck*_nao.txt." + "\n\nThe optional second integer controls text precision (default 8)."; + item.default_value = "0 8"; + item.unit = "Ry"; + item.read_value = [](const Input_Item& item, Parameter& para) { + const size_t count = item.get_size(); + try { + para.input.out_mat_vxck[0] = assume_as_boolean(item.str_values[0]); + } + catch (const std::invalid_argument& e) { + ModuleBase::WARNING("Input", "out_mat_vxck enable flag must be 0/1, using default 0"); + } + if (count >= 2) try { para.input.out_mat_vxck[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } }; item.check_value = [](const Input_Item& item, const Parameter& para) { - if (para.input.out_mat_h_vh[0] && para.input.nspin == 4) - ModuleBase::WARNING_QUIT("ReadInput", "out_mat_h_vh is not available for nspin = 4"); + if (para.input.out_mat_vxck[0] && para.input.nspin == 4) + ModuleBase::WARNING_QUIT("ReadInput", "out_mat_vxck is not available for nspin = 4"); }; - sync_intvec(input.out_mat_h_vh, 2, 0); + sync_intvec(input.out_mat_vxck, 2, 0); this->add_item(item); } { Input_Item item("out_mat_h_vxc"); - item.annotation = "output exchange-correlation Vxc(R) matrix"; + item.annotation = "legacy alias for out_mat_vxck"; item.category = "Output information"; item.type = "Integer"; - item.description = "Whether to print files containing the XC matrix Vxc(R) in CSR format." - "\n\nSee out_hsr for format details."; + item.description = "Legacy alias for out_mat_vxck, which outputs the exchange-correlation matrix Vxc(k) for each k-point in the NAO basis. The optional second integer controls text precision. If both out_mat_vxck and out_mat_h_vxc are present, out_mat_vxck takes precedence."; item.default_value = "0 8"; item.unit = "Ry"; item.read_value = [](const Input_Item& item, Parameter& para) { @@ -1168,21 +1265,47 @@ For binary output, each file uses the same basename as text output with a .dat s catch (const std::invalid_argument& e) { ModuleBase::WARNING("Input", "out_mat_h_vxc enable flag must be 0/1, using default 0"); } + if (count >= 2) try { para.input.out_mat_h_vxc[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } + }; + sync_intvec(input.out_mat_h_vxc, 2, 0); + this->add_item(item); + } + { + Input_Item item("out_mat_exxk"); + item.annotation = "output exact-exchange Vexx(k) matrix"; + item.category = "Output information"; + item.type = "Integer"; + item.description = "Whether to print the exact-exchange matrix Vexx(k) for each k-point in the NAO basis, into text files OUT.${suffix}/vexxk*_nao.txt." + "\n\nThe optional second integer controls text precision (default 8). Only available for gamma-only calculations with exact exchange enabled."; + item.default_value = "0 8"; + item.unit = "Ry"; + item.read_value = [](const Input_Item& item, Parameter& para) { + const size_t count = item.get_size(); + try { + para.input.out_mat_exxk[0] = assume_as_boolean(item.str_values[0]); + } + catch (const std::invalid_argument& e) { + ModuleBase::WARNING("Input", "out_mat_exxk enable flag must be 0/1, using default 0"); + } + if (count >= 2) try { para.input.out_mat_exxk[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } }; item.check_value = [](const Input_Item& item, const Parameter& para) { - if (para.input.out_mat_h_vxc[0] && para.input.nspin == 4) - ModuleBase::WARNING_QUIT("ReadInput", "out_mat_h_vxc is not available for nspin = 4"); + if (para.input.out_mat_exxk[0] && para.input.nspin == 4) + ModuleBase::WARNING_QUIT("ReadInput", "out_mat_exxk is not available for nspin = 4"); }; - sync_intvec(input.out_mat_h_vxc, 2, 0); + sync_intvec(input.out_mat_exxk, 2, 0); this->add_item(item); } { Input_Item item("out_mat_h_exx"); - item.annotation = "output exact-exchange Vexx(R) matrix"; + item.annotation = "legacy alias for out_mat_exxk"; item.category = "Output information"; item.type = "Integer"; - item.description = "Whether to print files containing the exact-exchange matrix Vexx(R) in CSR format." - "\n\nSee out_hsr for format details."; + item.description = "Legacy alias for out_mat_exxk, which outputs the exact-exchange matrix Vexx(k) for each k-point in the NAO basis. The optional second integer controls text precision. If both out_mat_exxk and out_mat_h_exx are present, out_mat_exxk takes precedence."; item.default_value = "0 8"; item.unit = "Ry"; item.read_value = [](const Input_Item& item, Parameter& para) { @@ -1193,10 +1316,9 @@ For binary output, each file uses the same basename as text output with a .dat s catch (const std::invalid_argument& e) { ModuleBase::WARNING("Input", "out_mat_h_exx enable flag must be 0/1, using default 0"); } - }; - item.check_value = [](const Input_Item& item, const Parameter& para) { - if (para.input.out_mat_h_exx[0] && para.input.nspin == 4) - ModuleBase::WARNING_QUIT("ReadInput", "out_mat_h_exx is not available for nspin = 4"); + if (count >= 2) try { para.input.out_mat_h_exx[1] = std::stoi(item.str_values[1]); } + catch (const std::invalid_argument&) { /* do nothing */ } + catch (const std::out_of_range&) { /* do nothing */ } }; sync_intvec(input.out_mat_h_exx, 2, 0); this->add_item(item); @@ -1240,7 +1362,7 @@ For binary output, each file uses the same basename as text output with a .dat s item.annotation = "output exchange-correlation matrix in KS-orbital representation"; item.category = "Output information"; item.type = "Boolean"; - item.description = "Write exchange-correlation matrices in the Kohn-Sham orbital basis and their diagonal expectation values. The diagonal energies are saved in OUT.${suffix}/vxc.txt. When EXX is calculated, its local and EXX contributions are also saved as vxc_local.txt and vxc_exx.txt in the same directory. Each energy file begins with nk, nspin, and nbands, followed by nk*nspin*nbands rows of energies in Hartree and eV." + item.description = "Write exchange-correlation matrices in the Kohn-Sham orbital basis and their diagonal expectation values. The diagonal energies are saved in OUT.${suffix}/eband_vxc.txt. When EXX is calculated, its local and EXX contributions are also saved as eband_vxc_local.txt and eband_vxc_exx.txt in the same directory. Each energy file begins with nk, nspin, and nbands, followed by nk*nspin*nbands rows of energies in Hartree and eV." "\n\n[NOTE] In the 3.10-LTS version, the file name is k-$k-Vxc and so on."; item.default_value = "False"; item.unit = "Ry"; @@ -1281,7 +1403,7 @@ For binary output, each file uses the same basename as text output with a .dat s item.annotation = "output the expectation values of angular momentum operators"; item.category = "Output information"; item.type = R"(Boolean \[Integer\](optional))"; - item.description = "Whether to print the expectation value of the angular momentum operator , , and in the basis of the localized atomic orbitals. The files are named lx_nao.txt, ly_nao.txt, and lz_nao.txt (or lxg{step+1}_nao.txt etc. when out_freq_ion is set). The second integer controls the precision of the output."; + item.description = "Whether to print the expectation value of the angular momentum operator , , and in the basis of the localized atomic orbitals. The files are named lx_nao.txt, ly_nao.txt, and lz_nao.txt (or lxg{step+1}_nao.txt etc. when out_app_flag = 2). The second integer controls the precision of the output."; item.default_value = "False 8"; item.unit = ""; item.set_availability("basis_type==lcao"); @@ -1338,15 +1460,30 @@ The circle order of the charge density on real space grids is: x is the outer lo this->add_item(item); } { - Input_Item item("out_eband_terms"); + Input_Item item("out_eband"); item.annotation = "output the band energy terms separately"; item.category = "Output information"; item.type = "Boolean"; - item.description = "Whether to print the band energy terms separately in the file OUT.{term}_out.dat. The terms include the kinetic, pseudopotential (local + nonlocal), Hartree and exchange-correlation (including exact exchange if calculated)."; + item.description = "Whether to print the band-decomposed energy of each Hamiltonian term in the files eband_kin.txt, eband_vl.txt, eband_vnl.txt, eband_hart.txt and eband_vxc.txt under the output directory. Each file starts with comment lines giving the ABACUS version, the timestamp, the ionic step and total energy, the expectation-value formula, and the units. The terms include the kinetic, pseudopotential (local + nonlocal), Hartree and exchange-correlation (including exact exchange if calculated)."; item.default_value = "False"; item.unit = ""; item.set_availability("basis_type==lcao"); - read_sync_bool(input.out_eband_terms); + read_sync_bool(input.out_eband); + this->add_item(item); + } + { + Input_Item item("out_eband_terms"); + item.annotation = "legacy alias for out_eband"; + item.category = "Output information"; + item.type = "Boolean"; + item.description = "Legacy alias for out_eband, which outputs the band-decomposed energy of each Hamiltonian term. If both out_eband and out_eband_terms are present, out_eband takes precedence."; + item.default_value = "False"; + item.unit = ""; + item.set_availability("basis_type==lcao"); + item.read_value = [](const Input_Item& item, Parameter& para) { + para.input.out_eband_terms_compat = assume_as_boolean(item.str_values[0]); + }; + add_bool_bcast(input.out_eband_terms_compat); this->add_item(item); } { @@ -1437,15 +1574,49 @@ The circle order of the charge density on real space grids is: x is the outer lo } { Input_Item item("out_app_flag"); - item.annotation = "whether output r(R), H(R), S(R), T(R), and dH(R) " - "matrices in an append manner during MD"; + item.annotation = "output directory layout and append behavior " + "during multi-step calculations (0/1/2)"; item.category = "Output information"; - item.type = "Boolean"; - item.description = "Whether to output r(R), H(R), S(R), T(R), dH(R), dS(R), and wfc matrices in an append manner during molecular dynamics calculations. Check input parameters out_mat_r, out_hsr, out_mat_t, out_mat_dh, out_hsk and out_wfc_lcao for more information."; - item.default_value = "true"; + item.type = "Integer"; + item.description = R"(Controls output file organization during multi-step calculations (MD, relax, RT-TDDFT). The directory routing applies to both LCAO and PW basis calculations. +* 0: Write directly to the OUT directory under a fixed filename. Every write truncates the previous content, so only the most recently written step is kept on disk. out_freq_ion only controls how often a write happens; it does not change the number of files (default, disk-friendly). +* 1: Reuse fixed filenames across steps: text outputs append new records, cube outputs accumulate one frame per written step + In mode 1 the first output step of a fresh (non-restart) run truncates the fixed-name file so a rerun does not keep stale records from a previous run; later steps and restarted runs keep appending. +* 2: Write to subdirectory (STRUC/ for structure files, PSI/, CHG/, POT/, ELF/, DOS/, DIP/, MAT/), files carry a g{step} suffix +For MD structure frames specifically, mode 0 keeps the numbered STRU{istep} files (STRU1 is the initial geometry) and writes no STRU_NOW, mode 1 writes one cumulative STRU file, and mode 2 writes the numbered files under STRUC/; see out_stru. +The legacy Boolean spellings (true/false, yes/no, on/off, t/f, y/n, and the dotted forms) are still accepted and map to modes 1/0 as before. +For more information, refer to the documentation of out_mat_r, out_hsr, out_mat_t, out_mat_dh, out_hsk, out_wfc_lcao, out_chg, out_pot, out_elf, out_dos, out_dipole, out_stru.)"; + item.default_value = "0"; item.unit = ""; - item.set_availability("basis_type==lcao and gamma_only==0"); - read_sync_bool(input.out_app_flag); + // out_app_flag applies to both LCAO and PW bases, and gamma-only runs + // follow the same directory routing rules as multi-k runs. An empty + // availability expression means "always available". + item.set_availability(""); + // Keep the previously accepted Boolean spellings working: the legacy + // parser treated out_app_flag as a Boolean, so INPUT files containing + // "true"/"false"/"yes"/"no"/"on"/"off" (case-insensitive) must keep + // parsing instead of reaching std::stoi and throwing. + item.read_value = [](const Input_Item& item, Parameter& para) { + try + { + para.input.out_app_flag = std::stoi(item.str_values[0]); + } + catch (const std::invalid_argument&) + { + para.input.out_app_flag = assume_as_boolean(item.str_values[0]) ? 1 : 0; + } + catch (const std::out_of_range&) + { + para.input.out_app_flag = assume_as_boolean(item.str_values[0]) ? 1 : 0; + } + }; + sync_int(input.out_app_flag); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.out_app_flag < 0 || para.input.out_app_flag > 2) + { + ModuleBase::WARNING_QUIT("ReadInput", "out_app_flag should be 0, 1, or 2"); + } + }; this->add_item(item); } { @@ -1670,7 +1841,9 @@ Here $\tilde{\psi}$ is the pseudo-wavefunction, $\beta_i^I$ are the atomic proje * nspin = 4 (noncollinear): * elftot.cube: ELF for total charge density, ${\rm{ELF}} = \frac{1}{1+\chi^2}$, $\chi = \frac{\frac{1}{2}\sum_{i}{f_i |\nabla\psi_{i}|^2} - \frac{|\nabla\rho|^2}{8\rho}}{\frac{3}{10}(3\pi^2)^{2/3}\rho^{5/3}}$ -When `out_freq_ion > 0`, a geometry step suffix `g{#}` is appended to the file names (e.g., `elftotg1.cube`, `elfs1g1.cube`). +With `out_app_flag=0` the fixed-name files (`elftot.cube`, `elfs1.cube`) are truncated and overwritten at every write. +With `out_app_flag=1` the geometry step suffix is omitted and the same ELF files accumulate one complete cube frame per written ionic step (`Ionic_Step {#}` as the first comment line, one-based); the first written step truncates them. +With `out_app_flag=2`, every written step gets its own suffixed file (e.g., `elftotg1.cube`, `elfs1g1.cube`) routed to the `ELF/` subdirectory. The second integer controls the precision of the kinetic energy density output, if not given, will use 3 as default. For purpose restarting from this file and other high-precision involved calculation, recommend to use 10. diff --git a/source/source_io/module_parameter/read_input.cpp b/source/source_io/module_parameter/read_input.cpp index 66f1b41048a..2f87852aa05 100644 --- a/source/source_io/module_parameter/read_input.cpp +++ b/source/source_io/module_parameter/read_input.cpp @@ -25,7 +25,6 @@ #include #include #include -#include namespace ModuleIO { @@ -294,7 +293,7 @@ void ReadInput::create_directory(const Parameter& param) // NAME : Run::make_dir( dir name : OUT.suffix) //---------------------------------------------------------- bool out_dir = false; - if (!param.input.out_app_flag + if (param.input.out_app_flag == 0 && ((param.input.out_hsr[0] == 1 || param.input.out_hsr[0] == 2) || param.input.out_mat_r[0] || param.input.out_mat_t[0] || param.input.out_mat_dh[0] || param.input.out_mat_ds[0])) @@ -302,7 +301,7 @@ void ReadInput::create_directory(const Parameter& param) out_dir = true; } bool out_wfc_dir = false; - if (param.input.out_wfc_lcao && !param.input.out_app_flag) + if (param.input.out_wfc_lcao && param.input.out_app_flag == 0) { out_wfc_dir = true; } @@ -316,7 +315,6 @@ void ReadInput::create_directory(const Parameter& param) param.input.mdp.md_restart, param.input.out_alllog, param.globalv.global_out_dir, - param.globalv.global_stru_dir, param.globalv.global_matrix_dir, param.globalv.global_wfc_dir, param.globalv.global_mlkedf_descriptor_dir, @@ -324,6 +322,14 @@ void ReadInput::create_directory(const Parameter& param) param.globalv.log_file, param.input.of_ml_gene_data, param.input.deepks_out_freq_elec > 0); // xiaohui add 2013-09-01 + + // The unified subdirectories used when out_app_flag == 2 (STRUC/, PSI/, + // CHG/, POT/, ELF/, DOS/, DIP/, MAT/) are NOT created here. Each output + // writer creates its target directory lazily at write time via + // ModuleBase::ensure_directory, so this routine no longer needs to + // predict which outputs the user requested (a predicate that had to be + // fixed by hand for every new writer). + //const std::string ss = "test -d " + PARAM.inp.read_file_dir; struct stat st; if (stat(PARAM.inp.read_file_dir.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) @@ -478,6 +484,15 @@ std::string ReadInput::prepare_input(Parameter& param) this->normalize_hs_output_options(param); this->normalize_occ_mat_options(param); + this->normalize_out_eband_alias(param); + + // The MD driver writes ABACUS STRU frames only, and the restart logic reads + // them back, so CIF output (out_stru = 2) is not available for MD. Reject it + // instead of silently writing STRU content under a CIF setting. + if (param.input.calculation == "md" && param.input.out_stru == 2) + { + errors += "Invalid INPUT parameter 'out_stru': value 2 (CIF) is not supported for calculation = md; use 0 or 1.\n"; + } // Snapshot typed effective values once for shared domain and dependency checks. InputValues values; @@ -589,6 +604,65 @@ void ReadInput::normalize_hs_output_options(Parameter& param) { param.input.out_hsk[0] = 1; } + + // Merge the legacy out_mat_h_* aliases into their canonical k-space names. + // Each alias is accepted for backward compatibility; when both names are + // present the canonical new name takes precedence. out_mat_h_t is an alias + // for out_mat_tk because both produce the T(k) NAO text matrices (the + // former via the redundant write_h_t path, now removed). + const auto merge_alias = [&](const char* new_label, const char* old_label, + std::vector& new_val, const std::vector& old_val) { + const bool new_is_read = item_is_read(new_label); + const bool old_is_read = item_is_read(old_label); + if (new_is_read) + { + if (old_is_read) + { + ModuleBase::WARNING("ReadInput", + std::string("both ") + new_label + " and " + old_label + + " are set; " + new_label + " takes precedence"); + } + } + else if (old_is_read) + { + new_val = old_val; + } + }; + + merge_alias("out_mat_tk", "out_mat_h_t", param.input.out_mat_tk, param.input.out_mat_h_t); + merge_alias("out_mat_vnlk", "out_mat_h_vnl", param.input.out_mat_vnlk, param.input.out_mat_h_vnl); + merge_alias("out_mat_vlk", "out_mat_h_vl", param.input.out_mat_vlk, param.input.out_mat_h_vl); + merge_alias("out_mat_vhk", "out_mat_h_vh", param.input.out_mat_vhk, param.input.out_mat_h_vh); + merge_alias("out_mat_vxck", "out_mat_h_vxc", param.input.out_mat_vxck, param.input.out_mat_h_vxc); + merge_alias("out_mat_exxk", "out_mat_h_exx", param.input.out_mat_exxk, param.input.out_mat_h_exx); +} + +void ReadInput::normalize_out_eband_alias(Parameter& param) +{ + const auto item_is_read = [this](const std::string& label) { + const auto item = std::find_if( + this->input_lists.begin(), + this->input_lists.end(), + [&label](const std::pair& entry) { return entry.first == label; }); + return item != this->input_lists.end() && item->second.is_read(); + }; + + // out_eband_terms is the legacy spelling of out_eband. Accepting it keeps + // existing INPUT files working instead of aborting on an unknown keyword; + // the canonical out_eband wins when both are present. + const bool out_eband_is_read = item_is_read("out_eband"); + const bool out_eband_terms_is_read = item_is_read("out_eband_terms"); + if (out_eband_is_read) + { + if (out_eband_terms_is_read) + { + ModuleBase::WARNING("ReadInput", "both out_eband and out_eband_terms are set; out_eband takes precedence"); + } + } + else if (out_eband_terms_is_read) + { + param.input.out_eband = param.input.out_eband_terms_compat; + } } void ReadInput::write_txt_input(const Parameter& param, const std::string& filename) diff --git a/source/source_io/module_parameter/read_input.h b/source/source_io/module_parameter/read_input.h index d41b7252170..1f3f19965b0 100644 --- a/source/source_io/module_parameter/read_input.h +++ b/source/source_io/module_parameter/read_input.h @@ -87,6 +87,13 @@ class ReadInput * does not depend on the order of keywords in the INPUT file. */ void normalize_occ_mat_options(Parameter& param); + /** + * @brief Resolve out_eband and its legacy alias out_eband_terms. + * + * This is called after all INPUT values have been read so that the result + * does not depend on the order of keywords in the INPUT file. + */ + void normalize_out_eband_alias(Parameter& param); /** * @brief write INPUT file of txt format * diff --git a/source/source_io/module_parameter/read_input_item_md.cpp b/source/source_io/module_parameter/read_input_item_md.cpp index a41343fd261..8472a43cacb 100644 --- a/source/source_io/module_parameter/read_input_item_md.cpp +++ b/source/source_io/module_parameter/read_input_item_md.cpp @@ -159,7 +159,8 @@ Note that md_tlast is only used in NVT/NPT simulations. If md_tlast is unset or item.category = "Molecular dynamics"; item.type = "Boolean"; item.description = R"(Control whether to restart molecular dynamics calculations and time-dependent density functional theory calculations. -* True: ABACUS will read in {md_step}, then read in the corresponding STRU_MD_suffix/STRU/ automatically. For tddft, ABACUS will also read in WFC_NAO_K${kpoint} of the last step (You need to set out_wfc_lcao=1 and out_app_flag=0 to obtain this file). +* True: ABACUS will read in {md_step}, then read in the corresponding structure file automatically (STRU{istep} from the STRUC/ output directory when out_app_flag=2; older releases wrote STRU_MD_{istep} into a STRU/ subdirectory, which is still recognized). For tddft, ABACUS will also read in WFC_NAO_K${kpoint} of the last step (You need to set out_wfc_lcao=1 and out_app_flag=0 to obtain this file). +A structure frame is required for {istep}. Because out_freq_ion sets the structure-frame cadence independently of md_restartfreq, keep out_freq_ion unset (0) or equal to md_restartfreq when you intend to restart, and make sure out_stru is positive. * False: ABACUS will start molecular dynamics calculations normally from the first step.)"; item.default_value = "False"; item.unit = ""; @@ -171,7 +172,12 @@ Note that md_tlast is only used in NVT/NPT simulations. If md_tlast is unset or item.annotation = "The period to output MD restart information"; item.category = "Molecular dynamics"; item.type = "Integer"; - item.description = "The output frequency of OUT.{suffix}/STRU_MD_*, which are used to restart molecular dynamics calculations, see md_restart in detail. Set to 0 to disable MD restart output."; + item.description = "The period (in ionic steps) of writing the MD restart file " + "(Restart_md.txt). This cadence is independent of out_freq_ion: " + "restart files are written every md_restartfreq steps regardless of " + "the STRU output cadence. Set to 0 to disable restart-file output. " + "When out_freq_ion is not set (0), it also falls back to md_restartfreq " + "as the STRU output frequency for backward compatibility."; item.default_value = "5"; item.unit = ""; read_sync_int(input.mdp.md_restartfreq); diff --git a/source/source_io/module_parameter/read_set_globalv.cpp b/source/source_io/module_parameter/read_set_globalv.cpp index 3b395e6b8f5..0386ab5cc2c 100644 --- a/source/source_io/module_parameter/read_set_globalv.cpp +++ b/source/source_io/module_parameter/read_set_globalv.cpp @@ -2,7 +2,75 @@ #include "read_input_tool.h" #include "source_base/global_variable.h" #include "source_base/tool_quit.h" +#include "source_cell/stru_frame_io.h" #include "source_io/module_parameter/parameter.h" + +#include + +namespace +{ +/// @brief Find the structure file for restarting a molecular dynamics run. +/// @param stru_dir directory where the previous run wrote its structure files +/// @param istep number of completed ionic steps recorded in Restart_md.txt +/// @return path of the first existing structure file, empty if none exists +/// @details Three naming schemes are accepted, in order: +/// - "STRU": fixed-name cumulative file written in out_app_flag == 1 mode, +/// accepted when its last "# MD step N" frame matches step istep + 1; the +/// last frame is extracted to "STRU_restart_frame" and that path is +/// returned so the restart readers see a single-frame file; +/// - "STRU{istep + 1}": written by run_md since the ionic-step numbering +/// was unified with relax (1-based); +/// - "STRU_MD_{istep}": written by older ABACUS versions. +/// If none matches, the last frame of an arbitrary cumulative STRU is used as +/// a last resort, preserving restarts whose frame numbering cannot be +/// validated against Restart_md.txt. +std::string find_md_restart_stru(const std::string& stru_dir, const int istep) +{ + // Prefer the fixed-name cumulative STRU when its last frame belongs to the + // step this restart resumes from. A stale number-named file left behind by + // an earlier run must not shadow it, so the header is checked before the + // legacy names are consulted. + const std::string stru_cumulative = stru_dir + "STRU"; + std::ifstream file_cumulative(stru_cumulative.c_str()); + if (file_cumulative.good()) + { + file_cumulative.close(); + const int last_step = stru_frame_io::last_frame_step(stru_cumulative); + if (last_step == istep + 1) + { + const std::string frame_path = stru_dir + "STRU_restart_frame"; + if (stru_frame_io::extract_last_frame(stru_cumulative, frame_path)) + { + return frame_path; + } + } + } + const std::string stru_new = stru_dir + "STRU" + std::to_string(istep + 1); + std::ifstream file_new(stru_new.c_str()); + if (file_new.good()) + { + return stru_new; + } + const std::string stru_old = stru_dir + "STRU_MD_" + std::to_string(istep); + std::ifstream file_old(stru_old.c_str()); + if (file_old.good()) + { + return stru_old; + } + std::ifstream file_last_frame(stru_cumulative.c_str()); + if (file_last_frame.good()) + { + file_last_frame.close(); + const std::string frame_path = stru_dir + "STRU_restart_frame"; + if (stru_frame_io::extract_last_frame(stru_cumulative, frame_path)) + { + return frame_path; + } + } + return ""; +} +} // namespace + namespace ModuleIO { /// @note Here para.inp has been synchronized of all ranks. @@ -85,17 +153,38 @@ void ReadInput::set_global_dir(const Input_para& inp, System_para& sys) sys.global_out_dir = prefix + inp.suffix + "/"; sys.global_out_dir = to_dir(sys.global_out_dir); - /// get the global output directory - sys.global_stru_dir = sys.global_out_dir + "STRU/"; - sys.global_stru_dir = to_dir(sys.global_stru_dir); - - /// get the global output directory - sys.global_matrix_dir = sys.global_out_dir + "matrix/"; - sys.global_matrix_dir = to_dir(sys.global_matrix_dir); + /// Set subdirectories based on out_app_flag + /// out_app_flag == 2: use unified uppercase subdirectories + /// otherwise: keep backward-compatible behavior (all point to global_out_dir) + /// The structure subdirectory is named STRUC (not STRU) so it cannot be + /// confused with the STRU input file. + if (inp.out_app_flag == 2) + { + sys.global_stru_dir = sys.global_out_dir + "STRUC/"; + sys.global_psi_dir = sys.global_out_dir + "PSI/"; + sys.global_chg_dir = sys.global_out_dir + "CHG/"; + sys.global_pot_dir = sys.global_out_dir + "POT/"; + sys.global_elf_dir = sys.global_out_dir + "ELF/"; + sys.global_dos_dir = sys.global_out_dir + "DOS/"; + sys.global_dip_dir = sys.global_out_dir + "DIP/"; + sys.global_mat_dir = sys.global_out_dir + "MAT/"; + } + else + { + sys.global_stru_dir = sys.global_out_dir; + sys.global_psi_dir = sys.global_out_dir; + sys.global_chg_dir = sys.global_out_dir; + sys.global_pot_dir = sys.global_out_dir; + sys.global_elf_dir = sys.global_out_dir; + sys.global_dos_dir = sys.global_out_dir; + sys.global_dip_dir = sys.global_out_dir; + sys.global_mat_dir = sys.global_out_dir; + } - /// get the global output directory - sys.global_wfc_dir = sys.global_out_dir + "WFC/"; - sys.global_wfc_dir = to_dir(sys.global_wfc_dir); + /// Map legacy directory names to new unified names + /// WFC -> PSI, matrix -> MAT + sys.global_wfc_dir = sys.global_psi_dir; + sys.global_matrix_dir = sys.global_mat_dir; /// get the global ML KEDF descriptor directory sys.global_mlkedf_descriptor_dir = sys.global_out_dir + "MLKEDF_Descriptors/"; @@ -112,15 +201,37 @@ void ReadInput::set_global_dir(const Input_para& inp, System_para& sys) /// get the stru file for md restart case if (inp.calculation == "md" && inp.mdp.md_restart) { - int istep = current_md_step(sys.global_readin_dir); + const int istep = current_md_step(sys.global_readin_dir); if (inp.read_file_dir == to_dir("OUT." + inp.suffix)) { - sys.global_in_stru = sys.global_stru_dir + "STRU_MD_" + std::to_string(istep); + // Restart from the default output directory: search the current + // STRU output directory, which follows out_app_flag. + const std::string stru_dir = sys.global_stru_dir; + sys.global_in_stru = find_md_restart_stru(stru_dir, istep); + if (sys.global_in_stru.empty()) + { + // Releases before the out_app_flag rework always wrote + // STRU_MD_{istep} into a fixed STRU/ subdirectory of the + // output directory (the mode-2 directory has since been + // renamed to STRUC/); keep those outputs restartable. + const std::string legacy_dir = sys.global_out_dir + "STRU/"; + sys.global_in_stru = find_md_restart_stru(legacy_dir, istep); + } } else { - sys.global_in_stru = inp.read_file_dir + "STRU_MD_" + std::to_string(istep); + // Restart from a user-provided directory: the previous run may + // have written either naming scheme there. + const std::string stru_dir = inp.read_file_dir; + sys.global_in_stru = find_md_restart_stru(stru_dir, istep); + } + + if (sys.global_in_stru.empty()) + { + const std::string message = "no STRU file found for MD restart in " + sys.global_readin_dir + + "; expected STRU_MD_{step}, STRU{step+1} or cumulative STRU"; + ModuleBase::WARNING_QUIT("ReadInput::set_global_dir", message); } } else @@ -154,6 +265,13 @@ void ReadInput::set_global_dir(const Input_para& inp, System_para& sys) Parallel_Common::bcast_string(sys.global_stru_dir); Parallel_Common::bcast_string(sys.global_matrix_dir); Parallel_Common::bcast_string(sys.global_wfc_dir); + Parallel_Common::bcast_string(sys.global_psi_dir); + Parallel_Common::bcast_string(sys.global_chg_dir); + Parallel_Common::bcast_string(sys.global_pot_dir); + Parallel_Common::bcast_string(sys.global_elf_dir); + Parallel_Common::bcast_string(sys.global_dos_dir); + Parallel_Common::bcast_string(sys.global_dip_dir); + Parallel_Common::bcast_string(sys.global_mat_dir); Parallel_Common::bcast_string(sys.global_in_stru); #endif } diff --git a/source/source_io/module_parameter/system_parameter.h b/source/source_io/module_parameter/system_parameter.h index 438bd32c145..4705c33f4ea 100644 --- a/source/source_io/module_parameter/system_parameter.h +++ b/source/source_io/module_parameter/system_parameter.h @@ -53,8 +53,16 @@ struct System_para std::string global_stru_dir = ""; ///< global structure directory std::string global_matrix_dir = ""; ///< global matrix directory std::string global_wfc_dir = ""; ///< global wavefunction directory + // Subdirectories used when out_app_flag == 2 + std::string global_psi_dir = ""; ///< wavefunction directory (PSI/) + std::string global_chg_dir = ""; ///< charge density directory (CHG/) + std::string global_pot_dir = ""; ///< potential directory (POT/) + std::string global_elf_dir = ""; ///< ELF directory (ELF/) + std::string global_dos_dir = ""; ///< DOS directory (DOS/) + std::string global_dip_dir = ""; ///< dipole directory (DIP/) + std::string global_mat_dir = ""; ///< matrix directory (MAT/) std::string global_mlkedf_descriptor_dir = ""; ///< global ML KEDF descriptor directory - std::string global_deepks_label_elec_dir = ""; ///< global directory for DeePKS labels during electronic steps + std::string global_deepks_label_elec_dir = ""; ///< global directory for DeePKS labels during electronic steps std::string log_file = "log"; ///< log file name bool deepks_setorb = false; ///< true if "deepks" is set diff --git a/source/source_io/module_wf/get_wf_lcao.cpp b/source/source_io/module_wf/get_wf_lcao.cpp index 70b1e46784f..f982738b54b 100644 --- a/source/source_io/module_wf/get_wf_lcao.cpp +++ b/source/source_io/module_wf/get_wf_lcao.cpp @@ -76,7 +76,7 @@ void Get_wf_lcao::begin_gamma(const UnitCell& ucell, std::stringstream ss_out; ss_out << global_out_dir << ss_file.str(); - ModuleIO::write_vdata_palgrid(pgrid, wfc_norm.data(), is, nspin_, 0, ss_out.str(), 0.0, &ucell, precision, 0, false, false, ofs_running, desc_wfc_norm); + ModuleIO::write_vdata_palgrid(pgrid, wfc_norm.data(), is, nspin_, 0, ss_out.str(), 0.0, &ucell, precision, 0, false, false, false, ofs_running, desc_wfc_norm); } if (re_im_bands_picked[ib]) @@ -95,6 +95,7 @@ void Get_wf_lcao::begin_gamma(const UnitCell& ucell, 0, false, false, + false, ofs_running, desc_wfc_real); @@ -112,6 +113,7 @@ void Get_wf_lcao::begin_gamma(const UnitCell& ucell, 0, false, false, + false, ofs_running, desc_wfc_imag); } @@ -199,6 +201,7 @@ void Get_wf_lcao::begin_k(const UnitCell& ucell, 0, false, false, + false, ofs_running, desc_wfc_norm); } @@ -235,6 +238,7 @@ void Get_wf_lcao::begin_k(const UnitCell& ucell, 0, false, false, + false, ofs_running, desc_wfc_real); @@ -252,6 +256,7 @@ void Get_wf_lcao::begin_k(const UnitCell& ucell, 0, false, false, + false, ofs_running, desc_wfc_imag); } diff --git a/source/source_io/module_wf/get_wf_pw.cpp b/source/source_io/module_wf/get_wf_pw.cpp index c3d3401c2d9..39b17bc18f0 100644 --- a/source/source_io/module_wf/get_wf_pw.cpp +++ b/source/source_io/module_wf/get_wf_pw.cpp @@ -338,7 +338,7 @@ void Get_wf_pw::write_cube(const int band, } // collinear spin channel (nspin=2); spinor components (nspin=4) are not spin channels data_desc += ModuleIO::make_spin_tag(component, nspin_); - ModuleIO::write_vdata_palgrid(pgrid, values.data(), component, nspin_, 0, filename.str(), 0.0, &ucell, 11, 0, false, true, ofs_running, data_desc); + ModuleIO::write_vdata_palgrid(pgrid, values.data(), component, nspin_, 0, filename.str(), 0.0, &ucell, 11, 0, false, true, false, ofs_running, data_desc); } // Explicit instantiation emits both precisions for each supported device from this .cpp file. diff --git a/source/source_io/module_wf/read_wf2rho_pw.cpp b/source/source_io/module_wf/read_wf2rho_pw.cpp index 50e31e733a3..26120027492 100644 --- a/source/source_io/module_wf/read_wf2rho_pw.cpp +++ b/source/source_io/module_wf/read_wf2rho_pw.cpp @@ -128,8 +128,13 @@ void ModuleIO::read_wf2rho_pw_impl(const ModulePW::PW_Basis_K* pw_wfc, // mohan add 2025-05-17 // .dat file + // NOTE: reads the fixed-name binary file from its first frame. Files + // accumulated across ionic steps under out_app_flag == 1 hold one + // frame per step (MD trajectories); only the first frame is seen + // here, so use out_app_flag = 2 (per-step files) when the charge + // density of a specific step is to be reconstructed. const int out_type = 2; - const bool out_app_flag = false; + const int out_app_flag = 0; const bool gamma_only = false; const int istep = -1; diff --git a/source/source_io/module_wf/read_wfc_nao.cpp b/source/source_io/module_wf/read_wfc_nao.cpp index 2d26e9390ec..f98e25fb40b 100644 --- a/source/source_io/module_wf/read_wfc_nao.cpp +++ b/source/source_io/module_wf/read_wfc_nao.cpp @@ -7,7 +7,9 @@ #include "source_base/tool_title.h" // use title #include "source_base/global_function.h" // use READ_VALUE +#include #include +#include namespace { @@ -19,6 +21,36 @@ bool read_binary_value(std::ifstream& ifs, T& value) return static_cast(ifs); } +/** + * @brief skip leading comment lines starting with '#' in a text stream. + * + * Text wavefunction files may carry a provenance header (ABACUS version, + * timestamp, ionic step) before the data block. The stream is left positioned + * at the first non-comment character. Must not be used on binary streams, + * where '#' is legitimate record data. + * + * @param ifs text input stream positioned at the start of the file + */ +void skip_comment_lines(std::ifstream& ifs) +{ + std::string line; + while (ifs.peek() != std::char_traits::eof()) + { + const std::streampos pos = ifs.tellg(); + ifs >> std::ws; + if (ifs.peek() == '#') + { + std::getline(ifs, line); + } + else + { + ifs.seekg(pos); + ifs.clear(); + break; + } + } +} + template bool read_record_value(std::ifstream& ifs, T& value, const bool binary) { @@ -101,6 +133,10 @@ bool ModuleIO::read_wfc_nao_spin_populations(const std::string& readin_dir, ik, ik2iktot, nspin, nkstot, read_type, false, gamma_only, -1); const std::ios_base::openmode mode = binary ? std::ios::in | std::ios::binary : std::ios::in; std::ifstream ifs(file.c_str(), mode); + if (!binary) + { + skip_comment_lines(ifs); + } if (!gamma_only) { int ik_file = 0; @@ -261,6 +297,11 @@ bool ModuleIO::read_wfc_nao( std::cout << " Read NAO wave functions from " << ss << std::endl; } + if (!binary) + { + skip_comment_lines(ifs); + } + const auto incomplete_file = [&](const std::string& field) { error_message << "The wave function file is incomplete or corrupted while reading " << field << ": " << ss << std::endl; @@ -384,18 +425,146 @@ bool ModuleIO::read_wfc_nao( { if (myrank == 0) { - const bool out_app_flag = false; std::stringstream error_message; - std::string readin_dir = global_readin_dir; - if(istep >= 0) + const int read_type = binary ? 2 : 1; + // Candidate file locations, in priority order: + // 1. readin_dir + step-suffixed name: layout written by older runs + // with out_app_flag 0 (per-step g files in the OUT directory root) + // 2. readin_dir + "PSI/" + step-suffixed name: layout written by + // runs with out_app_flag 2 (also accepts a g file in the root) + // 3. readin_dir + "WFC/" + step-suffixed name: layout written by + // releases before the out_app_flag rework + // 4. readin_dir + name without the step suffix: written in fixed-name + // modes (out_app_flag 0/1) + // + // Step-suffixed names are generated with out_app_flag 2, the only + // mode that still embeds g{step}; this keeps backward compatibility + // with files written by older versions. + // + // istep semantics: + // - istep > 0: positive indexed restart. Only step-suffixed + // snapshots are accepted; the append-mode fixed-name file is + // excluded because read_one_file always loads its first frame, + // which would silently return step-0 data for a positive index. + // - istep == 0: ordinary first-frame initialization (e.g. + // non-restart TDDFT). Try the step-suffixed g1 layout first, + // then fall back to the append-mode fixed-name file. + // - istep < 0: default read. Append-mode fixed-name file only. + std::vector candidates; + if (istep > 0) { - readin_dir = readin_dir + "WFC/"; + const int suffix_app_flag = 2; + candidates.push_back(ModuleIO::filename_output(global_readin_dir, + "wf", + "nao", + ik, + ik2iktot, + nspin, + nkstot, + read_type, + suffix_app_flag, + gamma_only, + istep)); + const std::string psi_dir = global_readin_dir + "PSI/"; + candidates.push_back(ModuleIO::filename_output(psi_dir, + "wf", + "nao", + ik, + ik2iktot, + nspin, + nkstot, + read_type, + suffix_app_flag, + gamma_only, + istep)); + const std::string legacy_dir = global_readin_dir + "WFC/"; + candidates.push_back(ModuleIO::filename_output(legacy_dir, + "wf", + "nao", + ik, + ik2iktot, + nspin, + nkstot, + read_type, + suffix_app_flag, + gamma_only, + istep)); + } + else if (istep == 0) + { + const int suffix_app_flag = 2; + candidates.push_back(ModuleIO::filename_output(global_readin_dir, + "wf", + "nao", + ik, + ik2iktot, + nspin, + nkstot, + read_type, + suffix_app_flag, + gamma_only, + istep)); + const std::string psi_dir = global_readin_dir + "PSI/"; + candidates.push_back(ModuleIO::filename_output(psi_dir, + "wf", + "nao", + ik, + ik2iktot, + nspin, + nkstot, + read_type, + suffix_app_flag, + gamma_only, + istep)); + const std::string legacy_dir = global_readin_dir + "WFC/"; + candidates.push_back(ModuleIO::filename_output(legacy_dir, + "wf", + "nao", + ik, + ik2iktot, + nspin, + nkstot, + read_type, + suffix_app_flag, + gamma_only, + istep)); + const int append_app_flag = 1; + candidates.push_back(ModuleIO::filename_output(global_readin_dir, + "wf", + "nao", + ik, + ik2iktot, + nspin, + nkstot, + read_type, + append_app_flag, + gamma_only, + istep)); + } + else + { + const int append_app_flag = 1; + candidates.push_back(ModuleIO::filename_output(global_readin_dir, + "wf", + "nao", + ik, + ik2iktot, + nspin, + nkstot, + read_type, + append_app_flag, + gamma_only, + istep)); } - const int read_type = binary ? 2 : 1; - std::string ss = ModuleIO::filename_output(readin_dir,"wf","nao", - ik,ik2iktot,nspin,nkstot,read_type,out_app_flag,gamma_only,istep); - read_success = read_one_file(ss, error_message, ik, ctot); + for (size_t ic = 0; ic < candidates.size(); ++ic) + { + read_success = read_one_file(candidates[ic], error_message, ik, ctot); + if (read_success) + { + break; + } + } errors = error_message.str(); } #ifdef __MPI diff --git a/source/source_io/module_wf/write_wfc_nao.cpp b/source/source_io/module_wf/write_wfc_nao.cpp index 2d28e96e60d..25f240654fb 100644 --- a/source/source_io/module_wf/write_wfc_nao.cpp +++ b/source/source_io/module_wf/write_wfc_nao.cpp @@ -1,6 +1,7 @@ #include "write_wfc_nao.h" #include "source_io/module_parameter/parameter.h" +#include "source_base/fs_compat.h" #include "source_base/memory_recorder.h" #include "source_base/timer.h" #include "source_base/tool_title.h" @@ -10,6 +11,61 @@ #include "source_base/global_function.h" #include "source_base/module_out/binstream.h" #include "source_base/module_out/filename.h" +#include "source_base/formatter.h" +#include "source_io/module_output/provenance_header.h" +#include "source_io/module_output/spin_tag.h" + +namespace +{ + +/** + * @brief spin channel of a global k-point index, consistent with the s{N} + * block of filename_output. + * + * @param ik0 global k-point index (the ik2iktot value) + * @param nspin number of spins + * @param nkstot total number of k-points including the spin multiplicity + * @return 0 for spin up, 1 for spin down, -1 when nspin != 2 + */ +int wfc_spin_index(const int ik0, const int nspin, const int nkstot) +{ + if (nspin != 2) + { + return -1; + } + const int half_k = nkstot / 2; + if (ik0 >= half_k) + { + return 1; + } + return 0; +} + +/** + * @brief build the provenance header that precedes the wavefunction data + * block, mirroring the bands1.txt/bands2.txt header style. + * + * In append mode (out_app_flag == 1) every accumulated frame carries its own + * header, so readers can tell the ionic steps apart. + * + * @param istep zero-based ionic step index, printed one-based; the ionic-step + * line is omitted when istep < 0 (no step context in the run) + * @param spin_tag label of the collinear spin channel, empty when nspin != 2 + * @return two or three comment lines terminated by newlines + */ +std::string build_wfc_nao_header(const int istep, const std::string& spin_tag) +{ + const std::string header = ModuleIO::provenance_header(ModuleIO::local_timestamp()); + if (istep < 0) + { + return header; + } + const int istep_one_based = istep + 1; + const std::string step_line = FmtCore::format("# IONIC STEP %d%s\n", istep_one_based, spin_tag.c_str()); + return header + step_line; +} + +} // namespace namespace ModuleIO { @@ -21,7 +77,8 @@ void wfc_nao_write2file(const std::string& name, const ModuleBase::matrix& ekb, const ModuleBase::matrix& wg, const bool& writeBinary, - const bool& append_flag) + const bool& append_flag, + const std::string& header) { ModuleBase::TITLE("ModuleIO", "wfc_nao_write2file"); ModuleBase::timer::start("ModuleIO", "wfc_nao_write2file"); @@ -75,6 +132,12 @@ void wfc_nao_write2file(const std::string& name, { ModuleBase::WARNING_QUIT("ModuleIO::wfc_nao_write2file", "Can't write local orbital wave functions."); } + // provenance header precedes the data block; the binary branch above + // keeps its fixed record layout and ignores the header + if (!header.empty()) + { + ofs << header; + } ofs << nbands << " (number of bands)" << std::endl; ofs << nlocal << " (number of orbitals)"; ofs << std::setprecision(8); @@ -112,7 +175,8 @@ void wfc_nao_write2file_complex(const std::string& name, const ModuleBase::matrix& ekb, const ModuleBase::matrix& wg, const bool& writeBinary, - const bool& append_flag) + const bool& append_flag, + const std::string& header) { ModuleBase::TITLE("ModuleIO","wfc_nao_write2file_complex"); ModuleBase::timer::start("ModuleIO","wfc_nao_write2file_complex"); @@ -169,6 +233,12 @@ void wfc_nao_write2file_complex(const std::string& name, { ModuleBase::WARNING_QUIT("ModuleIO::wfc_nao_write2file_complex", "Can't write local orbital wave functions."); } + // provenance header precedes the k-point record; the binary branch + // above keeps its fixed record layout and ignores the header + if (!header.empty()) + { + ofs << header; + } ofs << std::setprecision(8); ofs << ik + 1 << " (index of k points)" << std::endl; ofs << kvec_c.x << " " << kvec_c.y << " " << kvec_c.z << std::endl; @@ -202,7 +272,7 @@ void wfc_nao_write2file_complex(const std::string& name, template void write_wfc_nao(const int out_type, - const bool out_app_flag, + const int out_app_flag, const psi::Psi& psi, const ModuleBase::matrix& ekb, const ModuleBase::matrix& wg, @@ -270,7 +340,7 @@ void write_wfc_nao(const int out_type, if (myid == 0) { std::string wfc_dir; - if(out_app_flag) + if(out_app_flag == 1) { wfc_dir = PARAM.globalv.global_out_dir; } @@ -278,10 +348,19 @@ void write_wfc_nao(const int out_type, { wfc_dir = PARAM.globalv.global_wfc_dir; } + // The mode-2 PSI/ subdirectory is created lazily at write time; + // otherwise this is a no-op on the existing output directory. + ModuleBase::ensure_directory(wfc_dir); std::string fn = filename_output(wfc_dir,"wf","nao",ik,ik2iktot,nspin,nkstot, out_type,out_app_flag,gamma_only,istep); - bool append_flag = (istep > 0 && out_app_flag); + bool append_flag = (istep > 0 && out_app_flag == 1); + // spin channel of the current k-point and the per-frame + // provenance header (version, timestamp, ionic step, spin tag) + const int ik0 = ik2iktot[ik]; + const int is = wfc_spin_index(ik0, nspin, nkstot); + const std::string spin_tag = ModuleIO::make_compact_spin_tag(is, nspin); + const std::string header = build_wfc_nao_header(istep, spin_tag); if (std::is_same::value) { wfc_nao_write2file(fn, @@ -291,7 +370,8 @@ void write_wfc_nao(const int out_type, ekb, wg, writeBinary, - append_flag); + append_flag, + header); } else { @@ -303,7 +383,8 @@ void write_wfc_nao(const int out_type, ekb, wg, writeBinary, - append_flag); + append_flag, + header); } } } @@ -311,7 +392,7 @@ void write_wfc_nao(const int out_type, } template void write_wfc_nao(const int out_type, - const bool out_app_flag, + const int out_app_flag, const psi::Psi& psi, const ModuleBase::matrix& ekb, const ModuleBase::matrix& wg, @@ -323,7 +404,7 @@ template void write_wfc_nao(const int out_type, const int istep); template void write_wfc_nao>(const int out_type, - const bool out_app_flag, + const int out_app_flag, const psi::Psi>& psi, const ModuleBase::matrix& ekb, const ModuleBase::matrix& wg, diff --git a/source/source_io/module_wf/write_wfc_nao.h b/source/source_io/module_wf/write_wfc_nao.h index 152723b5109..a1421404b6d 100644 --- a/source/source_io/module_wf/write_wfc_nao.h +++ b/source/source_io/module_wf/write_wfc_nao.h @@ -4,6 +4,7 @@ #include "source_basis/module_ao/parallel_orbitals.h" #include "source_psi/psi.h" #include "source_base/vector3.h" +#include #include namespace ModuleIO @@ -25,7 +26,7 @@ namespace ModuleIO */ template void write_wfc_nao(const int out_type, - const bool out_app_flag, + const int out_app_flag, const psi::Psi& psi, const ModuleBase::matrix& ekb, const ModuleBase::matrix& wg, @@ -36,6 +37,20 @@ void write_wfc_nao(const int out_type, const int nspin, const int istep=-1) ; +/** + * @brief write real-valued Gamma-only wavefunction coefficients to a file. + * + * @param name output file name + * @param ctot global wavefunction coefficient array, size nbands*nlocal + * @param nlocal number of orbitals (basis functions) + * @param ik local k-point index, used to index ekb/wg rows + * @param ekb eigenvalue matrix + * @param wg occupation matrix + * @param writeBinary true for the binary format, false for text + * @param append_flag true to append to an existing file, false to truncate + * @param header provenance comment block written before the data in text + * mode; ignored in binary mode + */ void wfc_nao_write2file(const std::string& name, const double* ctot, const int nlocal, @@ -43,8 +58,24 @@ void wfc_nao_write2file(const std::string& name, const ModuleBase::matrix& ekb, const ModuleBase::matrix& wg, const bool& writeBinary, - const bool& append_flag = false); + const bool& append_flag, + const std::string& header); +/** + * @brief write complex wavefunction coefficients to a file. + * + * @param name output file name + * @param ctot global wavefunction coefficient array, size nbands*nlocal + * @param nlocal number of orbitals (basis functions) + * @param ik local k-point index, used to index ekb/wg rows + * @param kvec_c Cartesian k-vector of the k-point + * @param ekb eigenvalue matrix + * @param wg occupation matrix + * @param writeBinary true for the binary format, false for text + * @param append_flag true to append to an existing file, false to truncate + * @param header provenance comment block written before the data in text + * mode; ignored in binary mode + */ void wfc_nao_write2file_complex(const std::string& name, const std::complex* ctot, const int nlocal, @@ -52,7 +83,8 @@ void wfc_nao_write2file_complex(const std::string& name, const ModuleBase::Vector3& kvec_c, const ModuleBase::matrix& ekb, const ModuleBase::matrix& wg, - const bool& writeBinary = false, - const bool& append_flag = false); + const bool& writeBinary, + const bool& append_flag, + const std::string& header); }// namespace ModuleIO #endif diff --git a/source/source_io/module_wf/write_wfc_pw.cpp b/source/source_io/module_wf/write_wfc_pw.cpp index 450bb596988..50cfcb117a7 100644 --- a/source/source_io/module_wf/write_wfc_pw.cpp +++ b/source/source_io/module_wf/write_wfc_pw.cpp @@ -14,6 +14,7 @@ void ModuleIO::write_wfc_pw( const int istep, const int iter, + const int append_step, const int kpar, const int my_pool, const int my_rank, @@ -23,6 +24,7 @@ void ModuleIO::write_wfc_pw( const int rank_in_pool, const int nproc_in_pool, const int out_wfc_pw, + const int out_app_flag, const double& ecutwfc, const std::string& global_out_dir, const psi::Psi>& psi, @@ -43,7 +45,6 @@ void ModuleIO::write_wfc_pw( assert(nkstot>0); assert(nks>0); - bool out_app_flag = false; // need to modify later, mohan 2025-05-17 bool gamma_only = false; // need to modify later, mohan 2025-05-17 std::string* wfilename = new std::string[nks]; @@ -65,15 +66,26 @@ void ModuleIO::write_wfc_pw( if (rank_in_pool == 0) { - if (out_wfc_pw == 1) + // Mirrors write_wfc_nao's append_flag: the first step of a + // run truncates the file, later steps append a new frame + // when out_app_flag == 1, so the file accumulates one frame + // per ionic step instead of being rewritten each step. The + // decision uses append_step (the real output step), not istep + // (the filename sentinel), so an out_freq_ion == 0 run that + // writes a fixed-name file still appends after its first step. + const bool append_frame = (append_step > 0 && out_app_flag == 1); + if (!append_frame) { - std::ofstream ofs(fn.c_str()); // clear all wavefunc files. - ofs.close(); - } - else if (out_wfc_pw == 2) - { - Binstream wfs(fn, "w"); - wfs.close(); + if (out_wfc_pw == 1) + { + std::ofstream ofs(fn.c_str()); // clear all wavefunc files. + ofs.close(); + } + else if (out_wfc_pw == 2) + { + Binstream wfs(fn, "w"); + wfs.close(); + } } } } diff --git a/source/source_io/module_wf/write_wfc_pw.h b/source/source_io/module_wf/write_wfc_pw.h index 7e035bf82cd..fbe6c387416 100644 --- a/source/source_io/module_wf/write_wfc_pw.h +++ b/source/source_io/module_wf/write_wfc_pw.h @@ -7,9 +7,14 @@ namespace ModuleIO { +// istep is the filename sentinel: -1 keeps the ionic step out of the file +// name. append_step is the real output step that decides whether the fixed +// file is appended (a new frame per step) or truncated; pass -1 for a +// single-point write that always truncates. void write_wfc_pw( const int istep, const int iter, + const int append_step, const int kpar, const int my_pool, const int my_rank, @@ -19,6 +24,7 @@ void write_wfc_pw( const int rank_in_pool, const int nproc_in_pool, const int out_wfc_pw, + const int out_app_flag, const double& ecutwfc, const std::string& global_out_dir, const psi::Psi>& psi, diff --git a/source/source_io/test_serial/CMakeLists.txt b/source/source_io/test_serial/CMakeLists.txt index 991c2d77821..440f2ba54fd 100644 --- a/source/source_io/test_serial/CMakeLists.txt +++ b/source/source_io/test_serial/CMakeLists.txt @@ -28,6 +28,9 @@ add_library( ../module_parameter/availability.cpp ../module_parameter/availability_validator.cpp ../module_parameter/input_domain.cpp + # read_set_globalv.cpp references stru_frame_io::extract_last_frame, so + # every target linking io_input_serial needs this translation unit. + ../../source_cell/stru_frame_io.cpp ) abacus_copy_test_files(support) diff --git a/source/source_io/test_serial/io_sys_var_test.cpp b/source/source_io/test_serial/io_sys_var_test.cpp index 678ee6a81b4..c0ad11665ee 100644 --- a/source/source_io/test_serial/io_sys_var_test.cpp +++ b/source/source_io/test_serial/io_sys_var_test.cpp @@ -5,6 +5,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "source_base/fs_compat.h" #include "source_base/tool_quit.h" #include "source_io/module_parameter/input_parameter.h" #define private public @@ -67,8 +68,18 @@ TEST_F(InputTest, Item_test) TestParameters::set_global_dir(readinput, input, sys); EXPECT_EQ(sys.global_out_dir, "OUT.test/"); - EXPECT_EQ(sys.global_stru_dir, "OUT.test/STRU/"); - EXPECT_EQ(sys.global_matrix_dir, "OUT.test/matrix/"); + // out_app_flag defaults to 0, so stru dir maps to global out dir + EXPECT_EQ(sys.global_stru_dir, "OUT.test/"); + // out_app_flag defaults to 0, so matrix dir maps to global out dir + EXPECT_EQ(sys.global_matrix_dir, "OUT.test/"); + EXPECT_EQ(sys.global_wfc_dir, "OUT.test/"); + EXPECT_EQ(sys.global_psi_dir, "OUT.test/"); + EXPECT_EQ(sys.global_chg_dir, "OUT.test/"); + EXPECT_EQ(sys.global_pot_dir, "OUT.test/"); + EXPECT_EQ(sys.global_elf_dir, "OUT.test/"); + EXPECT_EQ(sys.global_dos_dir, "OUT.test/"); + EXPECT_EQ(sys.global_dip_dir, "OUT.test/"); + EXPECT_EQ(sys.global_mat_dir, "OUT.test/"); TestParameters::set_globalv(readinput, input, sys); @@ -100,3 +111,133 @@ TEST_F(InputTest, Item_test) EXPECT_EQ(sys.npol, 1); } } + +TEST_F(InputTest, SetGlobalDirMdRestart) +{ + readinput.check_ntype_flag = false; + + const std::string case_dir = testing::TempDir() + "/abacus_md_restart_io/"; + ModuleBase::make_directory(case_dir); + + // Restart from a user-provided read_file_dir holding the legacy + // "STRU_MD_{istep}" naming: the structure file must be picked up. + const std::string legacy_dir = case_dir + "legacy/"; + ModuleBase::make_directory(legacy_dir); + const std::string restart_txt = legacy_dir + "Restart_md.txt"; + std::ofstream restart_file(restart_txt.c_str()); + restart_file << 5 << std::endl; + restart_file << 0.03 << std::endl; + restart_file.close(); + const std::string stru_legacy = legacy_dir + "STRU_MD_5"; + std::ofstream stru_file_legacy(stru_legacy.c_str()); + stru_file_legacy << "ATOMIC_SPECIES" << std::endl; + stru_file_legacy.close(); + + input.suffix = "autotest"; + input.calculation = "md"; + input.mdp.md_restart = true; + input.read_file_dir = legacy_dir; + TestParameters::set_global_dir(readinput, input, sys); + EXPECT_EQ(sys.global_in_stru, stru_legacy); + + // Restart from a read_file_dir holding the current "STRU{istep + 1}" + // naming written by run_md (1-based, unified with relax). + const std::string newnaming_dir = case_dir + "newnaming/"; + ModuleBase::make_directory(newnaming_dir); + const std::string restart_txt_new = newnaming_dir + "Restart_md.txt"; + std::ofstream restart_file_new(restart_txt_new.c_str()); + restart_file_new << 5 << std::endl; + restart_file_new << 0.03 << std::endl; + restart_file_new.close(); + const std::string stru_new = newnaming_dir + "STRU6"; + std::ofstream stru_file_new(stru_new.c_str()); + stru_file_new << "ATOMIC_SPECIES" << std::endl; + stru_file_new.close(); + + input.read_file_dir = newnaming_dir; + TestParameters::set_global_dir(readinput, input, sys); + EXPECT_EQ(sys.global_in_stru, stru_new); + + // Restart from a read_file_dir holding only the fixed-name cumulative + // "STRU" written in out_app_flag == 1 mode: the last "# MD step N" frame + // must be extracted to a single-frame file and picked up. + const std::string cumulative_dir = case_dir + "cumulative/"; + ModuleBase::make_directory(cumulative_dir); + const std::string restart_txt_cum = cumulative_dir + "Restart_md.txt"; + std::ofstream restart_file_cum(restart_txt_cum.c_str()); + restart_file_cum << 2 << std::endl; + restart_file_cum << 0.03 << std::endl; + restart_file_cum.close(); + const std::string stru_cumulative = cumulative_dir + "STRU"; + std::ofstream stru_file_cum(stru_cumulative.c_str()); + stru_file_cum << "# MD step 1\n\nATOMIC_SPECIES\nSi 28.0855\n" << std::endl; + stru_file_cum << "# MD step 2\n\nATOMIC_SPECIES\nSi 28.0855\n\nLATTICE_CONSTANT\n10.2\n" << std::endl; + stru_file_cum.close(); + + input.read_file_dir = cumulative_dir; + TestParameters::set_global_dir(readinput, input, sys); + // the router picks the extracted single-frame side file next to STRU; the + // frame contents themselves are covered by the stru_frame_io unit test + const std::string stru_frame = cumulative_dir + "STRU_restart_frame"; + EXPECT_EQ(sys.global_in_stru, stru_frame); + std::ifstream frame_ifs(stru_frame.c_str()); + EXPECT_TRUE(frame_ifs.good()); + frame_ifs.close(); + + // Regression: a stale number-named file from an earlier run must not + // shadow the cumulative STRU of the run being restarted. Here the fixed + // name holds the frame of step 1 that Restart_md.txt resumes from, while + // "STRU1" is a leftover of a previous run in the same output directory. + const std::string stale_dir = case_dir + "stale/"; + ModuleBase::make_directory(stale_dir); + const std::string restart_txt_stale = stale_dir + "Restart_md.txt"; + std::ofstream restart_file_stale(restart_txt_stale.c_str()); + restart_file_stale << 0 << std::endl; + restart_file_stale << 0.03 << std::endl; + restart_file_stale.close(); + const std::string stru_stale = stale_dir + "STRU"; + std::ofstream stru_file_stale(stru_stale.c_str()); + stru_file_stale << "# MD step 1\n\nATOMIC_SPECIES\nSi 28.0855\n" << std::endl; + stru_file_stale.close(); + const std::string stru_leftover = stale_dir + "STRU1"; + std::ofstream stru_file_leftover(stru_leftover.c_str()); + stru_file_leftover << "ATOMIC_SPECIES\nO 15.999\n" << std::endl; + stru_file_leftover.close(); + + input.read_file_dir = stale_dir; + TestParameters::set_global_dir(readinput, input, sys); + EXPECT_EQ(sys.global_in_stru, stale_dir + "STRU_restart_frame"); + + // restore the shared input state so other tests are not affected + input.calculation = "scf"; + input.mdp.md_restart = false; + input.read_file_dir = "OUT.$suffix"; +} + +TEST_F(InputTest, SetGlobalDirMode2) +{ + readinput.check_ntype_flag = false; + + // out_app_flag = 2 routes every output class to its unified + // subdirectory below OUT./ + input.suffix = "modetwo"; + input.out_app_flag = 2; + TestParameters::set_global_dir(readinput, input, sys); + + EXPECT_EQ(sys.global_out_dir, "OUT.modetwo/"); + EXPECT_EQ(sys.global_stru_dir, "OUT.modetwo/STRUC/"); + EXPECT_EQ(sys.global_psi_dir, "OUT.modetwo/PSI/"); + EXPECT_EQ(sys.global_chg_dir, "OUT.modetwo/CHG/"); + EXPECT_EQ(sys.global_pot_dir, "OUT.modetwo/POT/"); + EXPECT_EQ(sys.global_elf_dir, "OUT.modetwo/ELF/"); + EXPECT_EQ(sys.global_dos_dir, "OUT.modetwo/DOS/"); + EXPECT_EQ(sys.global_dip_dir, "OUT.modetwo/DIP/"); + EXPECT_EQ(sys.global_mat_dir, "OUT.modetwo/MAT/"); + // legacy aliases follow the unified directories + EXPECT_EQ(sys.global_wfc_dir, sys.global_psi_dir); + EXPECT_EQ(sys.global_matrix_dir, sys.global_mat_dir); + + // restore the shared input state so other tests are not affected + input.suffix = "test"; + input.out_app_flag = 1; +} diff --git a/source/source_io/test_serial/read_input_item_test.cpp b/source/source_io/test_serial/read_input_item_test.cpp index e66274bc7c8..d69cc2648e0 100644 --- a/source/source_io/test_serial/read_input_item_test.cpp +++ b/source/source_io/test_serial/read_input_item_test.cpp @@ -1437,6 +1437,33 @@ TEST_F(InputTest, Item_test) } } +TEST_F(InputTest, OutEbandLegacyAlias) +{ + { + ModuleIO::ReadInput readinput(0); + Parameter param; + auto legacy = find_label("out_eband_terms", readinput.input_lists); + legacy->second.str_values = {"1"}; + legacy->second.read_value(legacy->second, param); + + readinput.normalize_out_eband_alias(param); + EXPECT_TRUE(TestParameters::input(param).out_eband); + } + { + ModuleIO::ReadInput readinput(0); + Parameter param; + auto primary = find_label("out_eband", readinput.input_lists); + auto legacy = find_label("out_eband_terms", readinput.input_lists); + primary->second.str_values = {"0"}; + legacy->second.str_values = {"1"}; + primary->second.read_value(primary->second, param); + legacy->second.read_value(legacy->second, param); + + readinput.normalize_out_eband_alias(param); + EXPECT_FALSE(TestParameters::input(param).out_eband); + } +} + TEST_F(InputTest, HsOutputAliases) { { @@ -1523,6 +1550,66 @@ TEST_F(InputTest, HsOutputAliases) EXPECT_EQ(TestParameters::input(param).out_hsk[0], 1); } } + +// The out_mat_h_* keywords are legacy aliases for the canonical k-space names +// (out_mat_tk and out_mat_{vnl,vl,vh,vxc,exx}k). Verify that each alias feeds +// the canonical value and that the canonical name wins when both are present. +TEST_F(InputTest, HkComponentAliases) +{ + // alias alone populates the canonical name + { + ModuleIO::ReadInput readinput(0); + Parameter param; + auto legacy = find_label("out_mat_h_t", readinput.input_lists); + legacy->second.str_values = {"1", "5"}; + legacy->second.read_value(legacy->second, param); + readinput.normalize_hs_output_options(param); + EXPECT_EQ(TestParameters::input(param).out_mat_tk[0], 1); + EXPECT_EQ(TestParameters::input(param).out_mat_tk[1], 5); + } + // canonical name takes precedence over the alias + { + ModuleIO::ReadInput readinput(0); + Parameter param; + auto legacy = find_label("out_mat_h_t", readinput.input_lists); + auto primary = find_label("out_mat_tk", readinput.input_lists); + legacy->second.str_values = {"1", "5"}; + primary->second.str_values = {"1", "3"}; + legacy->second.read_value(legacy->second, param); + primary->second.read_value(primary->second, param); + readinput.normalize_hs_output_options(param); + EXPECT_EQ(TestParameters::input(param).out_mat_tk[0], 1); + EXPECT_EQ(TestParameters::input(param).out_mat_tk[1], 3); + } + // the five renamed terms: legacy alias -> canonical k-space name + const std::vector> aliases = { + {"out_mat_h_vnl", "out_mat_vnlk"}, + {"out_mat_h_vl", "out_mat_vlk"}, + {"out_mat_h_vh", "out_mat_vhk"}, + {"out_mat_h_vxc", "out_mat_vxck"}, + {"out_mat_h_exx", "out_mat_exxk"}, + }; + for (const auto& pair : aliases) + { + SCOPED_TRACE(pair.first); + ModuleIO::ReadInput readinput(0); + Parameter param; + auto legacy = find_label(pair.first, readinput.input_lists); + auto primary = find_label(pair.second, readinput.input_lists); + legacy->second.str_values = {"1", "6"}; + legacy->second.read_value(legacy->second, param); + readinput.normalize_hs_output_options(param); + const auto& canon = TestParameters::input(param); + const std::vector* value = nullptr; + if (pair.second == "out_mat_vnlk") { value = &canon.out_mat_vnlk; } + else if (pair.second == "out_mat_vlk") { value = &canon.out_mat_vlk; } + else if (pair.second == "out_mat_vhk") { value = &canon.out_mat_vhk; } + else if (pair.second == "out_mat_vxck") { value = &canon.out_mat_vxck; } + else { value = &canon.out_mat_exxk; } + EXPECT_EQ((*value)[0], 1); + EXPECT_EQ((*value)[1], 6); + } +} TEST_F(InputTest, PreserveExplicitMagneticMixing) { const std::string filename = testing::TempDir() + "abacus_magnetic_mixing_INPUT"; @@ -2893,6 +2980,51 @@ TEST_F(InputTest, PerAtomSccsCavityWidthsDisableSpatialSymmetry) EXPECT_EQ(input.symmetry, "1"); } +// The optional second value of the H(k) component controls selects the text +// precision; it must survive both direct reads and legacy alias merging so the +// writer can forward it to save_mat. +TEST_F(InputTest, OutMatComponentPrecision) +{ + { + ModuleIO::ReadInput readinput(0); + Parameter param; + auto it = find_label("out_mat_vnlk", readinput.input_lists); + ASSERT_NE(it, readinput.input_lists.end()); + it->second.str_values = {"1", "5"}; + it->second.read_value(it->second, param); + EXPECT_EQ(TestParameters::input(param).out_mat_vnlk[0], 1); + EXPECT_EQ(TestParameters::input(param).out_mat_vnlk[1], 5); + } + { + // Legacy out_mat_h_vnl carries its own precision; the alias merge must + // copy the whole value vector, not only the enable flag. + ModuleIO::ReadInput readinput(0); + Parameter param; + auto legacy = find_label("out_mat_h_vnl", readinput.input_lists); + ASSERT_NE(legacy, readinput.input_lists.end()); + legacy->second.str_values = {"1", "4"}; + legacy->second.read_value(legacy->second, param); + readinput.normalize_hs_output_options(param); + EXPECT_EQ(TestParameters::input(param).out_mat_vnlk[0], 1); + EXPECT_EQ(TestParameters::input(param).out_mat_vnlk[1], 4); + } +} + +// MD writes ABACUS STRU frames only and the restart logic reads them back, so +// CIF output (out_stru = 2) must be rejected instead of silently emitting +// STRU content under the CIF setting. +TEST_F(InputTest, OutStruCifRejectedForMd) +{ + ModuleIO::ReadInput readinput(0); + readinput.check_ntype_flag = false; + Parameter param; + TestParameters::input(param).calculation = "md"; + TestParameters::input(param).out_stru = 2; + const std::string error = readinput.prepare_input(param); + EXPECT_THAT(error, testing::HasSubstr("out_stru")); + EXPECT_THAT(error, testing::HasSubstr("not supported for calculation = md")); +} + TEST_F(InputTest, DfptMixTypeDomainContract) { ModuleIO::ReadInput readinput(0); diff --git a/source/source_io/test_serial/read_input_test.cpp b/source/source_io/test_serial/read_input_test.cpp index e640708c720..8e973fd4b8d 100644 --- a/source/source_io/test_serial/read_input_test.cpp +++ b/source/source_io/test_serial/read_input_test.cpp @@ -39,7 +39,6 @@ void make_dir_out(const std::string& suffix, const bool& restart, const bool out_alllog, const std::string& global_out_dir, - const std::string& global_stru_dir, const std::string& global_matrix_dir, const std::string& global_wfc_dir, const std::string& global_mlkedf_descriptor_dir, @@ -427,6 +426,46 @@ TEST_F(InputTest, Check) EXPECT_TRUE(std::remove("./INPUT.ref") == 0); } +TEST_F(InputTest, OutAppFlagParsing) +{ + // numeric modes 0/1/2 are parsed as integers + { + Parameter param; + read_parameters("app_flag_numeric_INPUT", "basis_type lcao\nout_app_flag 2\n", param); + EXPECT_EQ(param.inp.out_app_flag, 2); + } + { + Parameter param; + read_parameters("app_flag_zero_INPUT", "basis_type lcao\nout_app_flag 0\n", param); + EXPECT_EQ(param.inp.out_app_flag, 0); + } + // the legacy Boolean spellings remain valid: true-like maps to mode 1, + // false-like maps to mode 0 + const std::vector> spellings + = {{"true", 1}, + {"TRUE", 1}, + {"Yes", 1}, + {"ON", 1}, + {".true.", 1}, + {"t", 1}, + {"false", 0}, + {"False", 0}, + {"NO", 0}, + {"off", 0}, + {".false.", 0}, + {"F", 0}}; + for (const auto& spelling : spellings) + { + Parameter param; + read_parameters("app_flag_bool_INPUT", "basis_type lcao\nout_app_flag " + spelling.first + "\n", param); + EXPECT_EQ(param.inp.out_app_flag, spelling.second) << "spelling: " << spelling.first; + } + // values outside 0/1/2 are rejected + expect_invalid_input("app_flag_oob_INPUT", + "basis_type lcao\nout_app_flag 3\n", + "out_app_flag should be 0, 1, or 2"); +} + TEST_F(InputTest, ExplicitNoccSurvivesUnresolvedElectronNumber) { Parameter parameters; diff --git a/source/source_io/test_serial/rho_io_test.cpp b/source/source_io/test_serial/rho_io_test.cpp index 38b226fc994..5a63f4fd485 100644 --- a/source/source_io/test_serial/rho_io_test.cpp +++ b/source/source_io/test_serial/rho_io_test.cpp @@ -7,6 +7,10 @@ #include "prepare_unitcell.h" #include "source_base/parallel_grid.h" +#include +#include +#include + Magnetism::Magnetism() { this->tot_mag = 0.0; @@ -97,7 +101,7 @@ TEST_F(RhoIOTest, Write) ucell->atoms[0].ncpp.zv = 4; Parallel_Grid pgrid(nx, ny, nz, nz, nrxx, nz, 1); ModuleIO::read_vdata_palgrid(pgrid, my_rank, ofs_running, "support/chg.cube", rho[0], ucell->nat); - ModuleIO::write_vdata_palgrid(pgrid, rho[0], 0, nspin, 0, "test_write_vdata_palgrid.cube", 0.461002, ucell, 11, 1, false, false, ofs_running, "charge density"); + ModuleIO::write_vdata_palgrid(pgrid, rho[0], 0, nspin, 0, "test_write_vdata_palgrid.cube", 0.461002, ucell, 11, 1, false, false, false, ofs_running, "charge density"); std::ifstream ifs1("test_write_vdata_palgrid.cube", std::ifstream::binary | std::ifstream::ate); std::ifstream ifs2("support/chg.cube", std::ifstream::binary | std::ifstream::ate); EXPECT_EQ(ifs1.tellg(), ifs2.tellg()); @@ -217,10 +221,80 @@ TEST_F(CubeIOTest, WriteCube) comment, natom, origin, nx_read, ny_read, nz_read, dx, dy, dz, atom_type, - atom_charge, atom_pos, data_read, 11); + atom_charge, atom_pos, data_read, 11, false); std::ifstream ifs1("test_write.cube", std::ifstream::binary | std::ifstream::ate); std::ifstream ifs2("./support/chg.cube", std::ifstream::binary | std::ifstream::ate); EXPECT_EQ(ifs1.tellg(), ifs2.tellg()); ifs1.close(); ifs2.close(); } + +/** + * - Tested Functions: + * - write_cube() + * - append == false truncates the file, leaving exactly one frame + * - append == true accumulates one complete frame per call, which is how + * out_app_flag == 1 writes successive ionic steps into a fixed filename + */ +TEST_F(CubeIOTest, WriteCubeAppendFrames) +{ + ModuleIO::read_cube(fn, comment, natom, origin, + nx_read, ny_read, nz_read, + dx, dy, dz, + atom_type, atom_charge, atom_pos, data_read); + + const std::string fname = std::string(testing::TempDir()) + "abacus_cube_append.cube"; + + auto count_frames = [&fname, this]() { + std::ifstream ifs(fname.c_str()); + const std::string content((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + std::size_t frames = 0; + std::size_t pos = content.find(comment[0]); + while (pos != std::string::npos) + { + ++frames; + pos = content.find(comment[0], pos + comment[0].size()); + } + return frames; + }; + + // The first frame of a run truncates stale content from any previous run. + ModuleIO::write_cube(fname, + comment, natom, origin, + nx_read, ny_read, nz_read, + dx, dy, dz, atom_type, + atom_charge, atom_pos, data_read, 11, false); + EXPECT_EQ(count_frames(), 1u); + + // Two appended frames stack a second and a third complete cube block. + for (int iframe = 0; iframe < 2; ++iframe) + { + ModuleIO::write_cube(fname, + comment, natom, origin, + nx_read, ny_read, nz_read, + dx, dy, dz, atom_type, + atom_charge, atom_pos, data_read, 11, true); + } + EXPECT_EQ(count_frames(), 3u); + + // Every frame ends with a newline, so the next frame starts on a fresh line. + { + std::ifstream ifs(fname.c_str()); + const std::string content((std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + ifs.close(); + EXPECT_EQ(content.back(), '\n'); + } + + // Overwriting once more drops the two accumulated frames. + ModuleIO::write_cube(fname, + comment, natom, origin, + nx_read, ny_read, nz_read, + dx, dy, dz, atom_type, + atom_charge, atom_pos, data_read, 11, false); + EXPECT_EQ(count_frames(), 1u); + + std::remove(fname.c_str()); +} diff --git a/source/source_io/unittests/CMakeLists.txt b/source/source_io/unittests/CMakeLists.txt index df0ca86d38b..f5a42e49077 100644 --- a/source/source_io/unittests/CMakeLists.txt +++ b/source/source_io/unittests/CMakeLists.txt @@ -16,11 +16,20 @@ AddTest( SOURCES test_spin_tag.cpp ) +AddTest( + TARGET MODULE_IO_provenance_header + SOURCES test_provenance_header.cpp +) + if(NOT ENABLE_MPI) return() endif() -add_executable(MODULE_IO_input_error_mpi test_read_input.cpp) +add_executable(MODULE_IO_input_error_mpi test_read_input.cpp + # io_input (via read_set_globalv.cpp) references + # stru_frame_io::extract_last_frame, but this test does not link + # the cell library, so provide the translation unit directly. + ../../source_cell/stru_frame_io.cpp) target_link_libraries(MODULE_IO_input_error_mpi PRIVATE parameter base device io_input abacus::linalg_libs) if(ENABLE_COVERAGE) add_coverage(MODULE_IO_input_error_mpi) @@ -56,6 +65,10 @@ AddTest( TARGET MODULE_IO_input_test_para LIBS parameter base device io_input SOURCES test_read_input_parallel.cpp + # io_input (via read_set_globalv.cpp) references + # stru_frame_io::extract_last_frame, but this test does not link + # the cell library, so provide the translation unit directly. + ../../source_cell/stru_frame_io.cpp ) add_test(NAME MODULE_IO_input_test_para_4 @@ -132,6 +145,7 @@ AddTest( TARGET MODULE_IO_parse_args LIBS parameter base device io_input SOURCES test_parse_args.cpp ../parse_args.cpp ../input_help.cpp + ../../source_cell/stru_frame_io.cpp ) AddTest( @@ -144,6 +158,7 @@ AddTest( TARGET MODULE_IO_input_help_test LIBS parameter base device io_input SOURCES test_input_help.cpp ../input_help.cpp + ../../source_cell/stru_frame_io.cpp ) AddTest( diff --git a/source/source_io/unittests/test_output_mulliken.cpp b/source/source_io/unittests/test_output_mulliken.cpp index 2d4b0800d02..6e7ab943a19 100644 --- a/source/source_io/unittests/test_output_mulliken.cpp +++ b/source/source_io/unittests/test_output_mulliken.cpp @@ -27,11 +27,11 @@ TYPED_TEST(OutputMullikenTest, OrbInfo) { CellIndex cell_index = CellIndex(this->atomLabels, this->atomCounts, this->lnchiCounts, 1); cell_index.write_orb_info("./"); - std::ifstream ifs("./Orbital"); + std::ifstream ifs("./nao.txt"); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); EXPECT_THAT(str, testing::HasSubstr("#io spec l m z sym")); EXPECT_THAT(str, testing::HasSubstr("0 Si 2 3 1 dx^2-y^2")); - remove("./Orbital"); + remove("./nao.txt"); } #ifdef __MPI diff --git a/source/source_io/unittests/test_provenance_header.cpp b/source/source_io/unittests/test_provenance_header.cpp new file mode 100644 index 00000000000..c2dd48b2195 --- /dev/null +++ b/source/source_io/unittests/test_provenance_header.cpp @@ -0,0 +1,31 @@ +#include "source_io/module_output/provenance_header.h" + +#include "source_main/version.h" + +#include "gtest/gtest.h" + +#include + +/** + * Tests for the provenance header shared by the band, wavefunction and + * band-decomposed energy writers. + */ + +TEST(ProvenanceHeaderTest, TimestampFormat) +{ + const std::string stamp = ModuleIO::local_timestamp(); + ASSERT_EQ(stamp.size(), 19u); + EXPECT_EQ(stamp[4], '-'); + EXPECT_EQ(stamp[7], '-'); + EXPECT_EQ(stamp[10], ' '); + EXPECT_EQ(stamp[13], ':'); + EXPECT_EQ(stamp[16], ':'); +} + +TEST(ProvenanceHeaderTest, HeaderCarriesVersionAndTimestamp) +{ + const std::string header = ModuleIO::provenance_header("2026-01-01 00:00:00"); + const std::string expected = "# ABACUS version: " + std::string(VERSION) + + "\n# Written at 2026-01-01 00:00:00\n"; + EXPECT_EQ(header, expected); +} diff --git a/source/source_io/unittests/test_read_input_parallel.cpp b/source/source_io/unittests/test_read_input_parallel.cpp index 7476f0082a5..3323023524e 100644 --- a/source/source_io/unittests/test_read_input_parallel.cpp +++ b/source/source_io/unittests/test_read_input_parallel.cpp @@ -220,7 +220,7 @@ TEST_F(InputParaTest, ParaRead) EXPECT_FALSE(param.inp.out_mat_xc); EXPECT_EQ(param.inp.out_mat_xc2[0], 0); EXPECT_EQ(param.inp.out_mat_xc2[1], 8); - EXPECT_FALSE(param.inp.out_eband_terms); + EXPECT_FALSE(param.inp.out_eband); EXPECT_EQ(param.inp.out_app_flag, 0); EXPECT_EQ(param.inp.out_mat_r[0], 0); EXPECT_EQ(param.inp.out_mat_r[1], 8); diff --git a/source/source_io/unittests/test_read_wf2rho_pw.cpp b/source/source_io/unittests/test_read_wf2rho_pw.cpp index 991a46a8f26..f0c7a51cd34 100644 --- a/source/source_io/unittests/test_read_wf2rho_pw.cpp +++ b/source/source_io/unittests/test_read_wf2rho_pw.cpp @@ -18,10 +18,24 @@ #ifdef __MPI #include "mpi.h" +#include "source_base/module_parallel/para_mpi_func.h" +#include "source_base/module_parallel/para_world.h" #include "source_base/parallel_global.h" #include "source_basis/module_pw/test/test_tool.h" #endif +/// @brief Collectively synchronize the world communicator without direct MPI. +/// @details Governance rule 9 forbids calling MPI routines directly, so the +/// tests go through the guarded Parallel wrappers; the helper is a no-op in +/// serial builds. +void world_barrier() +{ +#ifdef __MPI + const Parallel::ParaWorld world = Parallel::ParaWorld::make_mpi("test_read_wf2rho_pw", MPI_COMM_WORLD); + Parallel::barrier(world); +#endif +} + /// @brief Friend helper to mutate PARAM private members in unit tests. /// @details Parameter grants friend access to TestParameters so the test can /// modify input/sys fields without `#define private public`. The class must @@ -286,9 +300,11 @@ TEST_F(ReadWfcRhoTest, ReadWfcRho) const int istep = -1; // -1 means ionic iteration number will not appear in file name const int iter = -1; // -1 means electronic iteration number will not appear in file name + const int append_step = -1; // single-point write: truncate, do not append ModuleIO::write_wfc_pw(istep, iter, + append_step, kpar, my_pool, my_rank, @@ -298,6 +314,7 @@ TEST_F(ReadWfcRhoTest, ReadWfcRho) GlobalV::RANK_IN_POOL, GlobalV::NPROC_IN_POOL, TestParameters::input().out_wfc_pw, + 0, ecutwfc, out_dir, *psi, @@ -366,6 +383,131 @@ TEST_F(ReadWfcRhoTest, ReadWfcRho) } } +// Size of a file in bytes; returns -1 when the file cannot be opened. +static long file_size_bytes(const std::string& path) +{ + std::ifstream ifs(path.c_str(), std::ios::binary | std::ios::ate); + if (!ifs.is_open()) + { + return -1; + } + return static_cast(ifs.tellg()); +} + +// In append mode (out_app_flag == 1) with a fixed filename (istep == -1), the +// first output step of a run must truncate the file while a later step must +// append a new frame. This is driven by append_step, independent of the +// filename sentinel istep. Before the fix, istep == -1 always truncated, so a +// second write never grew the file. +TEST_F(ReadWfcRhoTest, WriteWfcAppendMode) +{ + const int kpar = GlobalV::KPAR; + const int nspin = 1; + const int nbands = 4; + const int my_pool = GlobalV::MY_POOL; + const int my_rank = GlobalV::MY_RANK; + const int nproc_in_pool = GlobalV::NPROC_IN_POOL; + const int rank_in_pool = GlobalV::RANK_IN_POOL; + const int nks = 2; + const int nkstot = kpar * nks; + + // K points and PW basis + kv->set_nkstot(nkstot); + kv->set_nks(nks); + kv->isk = {0, 0}; + const double shift = my_pool * 0.1; + kv->kvec_d = {ModuleBase::Vector3(shift, shift, shift), ModuleBase::Vector3(0.5 + shift, 0.5 + shift, 0.5 + shift)}; + kv->ik2iktot.resize(nks); + cal_ik2iktot(kv->ik2iktot, nks, nkstot); + +#ifdef __MPI + wfcpw->initmpi(nproc_in_pool, rank_in_pool, POOL_WORLD); +#endif + wfcpw->initgrids(5.3233, ModuleBase::Matrix3(-0.5, 0.0, 0.5, 0.0, 0.5, 0.5, -0.5, 0.5, 0.0), 80); + wfcpw->initparameters(false, 20, nks, kv->kvec_d.data()); + wfcpw->setuptransform(); + wfcpw->collect_local_pw(); + + kv->kvec_c.clear(); + for (int ik = 0; ik < nks; ++ik) + { + kv->kvec_c.push_back(wfcpw->kvec_c[ik]); + } + kv->ngk = {wfcpw->npwk[0], wfcpw->npwk[1]}; + kv->wk = {1.0, 1.0}; + + psi = new psi::Psi>(nks, nbands, wfcpw->npwk_max, kv->ngk, true); + std::complex* ptr = psi->get_pointer(); + for (int i = 0; i < nks * nbands * wfcpw->npwk_max; i++) + { + ptr[i] = std::complex((i + my_pool * 100) / 100.0, (i + my_pool) / 100.0); + } + + const std::string out_dir = "./"; + std::ofstream running_log("running_log_app0.txt"); + const double ecutwfc = 20; + const int npol = 1; + const int istep = -1; // filename sentinel: no step suffix + const int iter = -1; + const int out_app_flag = 1; // append mode + + auto write_once = [&](const int append_step) { + ModuleIO::write_wfc_pw(istep, + iter, + append_step, + kpar, + my_pool, + my_rank, + nbands, + nspin, + npol, + rank_in_pool, + nproc_in_pool, + TestParameters::input().out_wfc_pw, + out_app_flag, + ecutwfc, + out_dir, + *psi, + *kv, + wfcpw, + running_log); + }; + + write_once(0); // fresh first step: truncate + world_barrier(); + const long size_first = file_size_bytes("wfk1_pw.dat"); + + write_once(1); // later step: append a frame + world_barrier(); + const long size_second = file_size_bytes("wfk1_pw.dat"); + + write_once(0); // fresh run again: truncate back to a single frame + world_barrier(); + const long size_third = file_size_bytes("wfk1_pw.dat"); + + if (my_rank == 0) + { + EXPECT_GT(size_first, 0); + EXPECT_GT(size_second, size_first); + EXPECT_EQ(size_third, size_first); + } + + running_log.close(); + delete psi; + + if (my_rank == 0) + { + remove("running_log_app0.txt"); + remove("wfk1_pw.dat"); + remove("wfk2_pw.dat"); + if (kpar == 2) + { + remove("wfk3_pw.dat"); + remove("wfk4_pw.dat"); + } + } +} + int main(int argc, char** argv) { #ifdef __MPI diff --git a/source/source_io/unittests/test_read_wfc_nao.cpp b/source/source_io/unittests/test_read_wfc_nao.cpp index c28f4b09c7e..c40207755a0 100644 --- a/source/source_io/unittests/test_read_wfc_nao.cpp +++ b/source/source_io/unittests/test_read_wfc_nao.cpp @@ -5,6 +5,8 @@ #include "source_io/module_wf/write_wfc_nao.h" #include "source_base/module_out/filename.h" #include "source_base/global_function.h" +#include "source_base/module_parallel/para_mpi_func.h" +#include "source_base/module_parallel/para_world.h" #include @@ -12,6 +14,18 @@ * unit test of functions in read_wfc_nao.cpp ***********************************************/ +namespace +{ +// Domain-aware barrier wrapper (no direct MPI calls in tests); no-op in serial builds. +void world_barrier() +{ +#ifdef __MPI + const Parallel::ParaWorld world = Parallel::ParaWorld::make_mpi("test_read_wfc_nao", MPI_COMM_WORLD); + Parallel::barrier(world); +#endif +} +} // namespace + /** * - Tested Functions: * - distri_wfc_nao() @@ -35,16 +49,12 @@ class ReadWfcNaoTest : public ::testing::Test #endif binary_test_dir = "./read_wfc_nao_binary_np" + std::to_string(nproc) + "/"; ModuleBase::GlobalFunc::MAKE_DIR(binary_test_dir); -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); } void TearDown() override { -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); if (my_rank == 0) { std::remove((binary_test_dir + "wf_nao.dat").c_str()); @@ -52,9 +62,7 @@ class ReadWfcNaoTest : public ::testing::Test std::remove((binary_test_dir + "wfk1_nao.dat").c_str()); std::remove(binary_test_dir.substr(0, binary_test_dir.size() - 1).c_str()); } -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); } void initialize_parallel_orbitals(Parallel_Orbitals& para, const int nlocal, const int nbands) @@ -186,11 +194,10 @@ TEST_F(ReadWfcNaoTest, ReadBinaryGamma) ekb_source, wg_source, true, - false); + false, + ""); } -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); psi::Psi psi_read; ModuleBase::matrix ekb(1, nbands); @@ -215,6 +222,62 @@ TEST_F(ReadWfcNaoTest, ReadBinaryGamma) } } +TEST_F(ReadWfcNaoTest, ReadTextWithCommentHeader) +{ + // Text files carrying the provenance header (version/timestamp/step) + // must stay readable: the reader skips leading '#' comment lines. + const int nbands = 2; + const int nlocal = 3; + Parallel_Orbitals para; + initialize_parallel_orbitals(para, nlocal, nbands); + + const std::vector coefficients = {0.1, 0.2, 0.3, 1.1, 1.2, 1.3}; + ModuleBase::matrix ekb_source(1, nbands); + ModuleBase::matrix wg_source(1, nbands); + ekb_source(0, 0) = -0.5; + ekb_source(0, 1) = 0.7; + wg_source(0, 0) = 2.0; + wg_source(0, 1) = 0.0; + if (my_rank == 0) + { + const std::string header = "# ABACUS version: test\n" + "# Written at 2026-01-01 00:00:00\n" + "# IONIC STEP 1 (spin up)\n"; + ModuleIO::wfc_nao_write2file(binary_test_dir + "wf_nao.txt", + coefficients.data(), + nlocal, + 0, + ekb_source, + wg_source, + false, + false, + header); + } + world_barrier(); + + psi::Psi psi_read; + ModuleBase::matrix ekb(1, nbands); + ModuleBase::matrix wg(1, nbands); + const std::vector ik2iktot = {0}; + EXPECT_TRUE(ModuleIO::read_wfc_nao(binary_test_dir, + para, + psi_read, + ekb, + wg, + ik2iktot, + 1, + 1, + false)); + EXPECT_DOUBLE_EQ(ekb(0, 0), ekb_source(0, 0)); + EXPECT_DOUBLE_EQ(ekb(0, 1), ekb_source(0, 1)); + EXPECT_DOUBLE_EQ(wg(0, 0), wg_source(0, 0)); + EXPECT_DOUBLE_EQ(wg(0, 1), wg_source(0, 1)); + if (my_rank == 0) + { + EXPECT_DOUBLE_EQ(psi_read(0, 0, 0), coefficients[0]); + } +} + TEST_F(ReadWfcNaoTest, ReadBinaryGammaFloat) { const int nbands = 2; @@ -238,11 +301,10 @@ TEST_F(ReadWfcNaoTest, ReadBinaryGammaFloat) ekb_source, wg_source, true, - false); + false, + ""); } -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); psi::Psi psi_read; ModuleBase::matrix ekb(1, nbands); @@ -292,11 +354,10 @@ TEST_F(ReadWfcNaoTest, ReadBinaryComplex) ekb_source, wg_source, true, - false); + false, + ""); } -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); psi::Psi> psi_read; ModuleBase::matrix ekb(1, nbands); @@ -349,11 +410,10 @@ TEST_F(ReadWfcNaoTest, ReadBinaryPart) ekb_source, wg_source, true, - false); + false, + ""); } -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); psi::Psi psi_read; ModuleBase::matrix ekb(1, nbands); @@ -398,11 +458,10 @@ TEST_F(ReadWfcNaoTest, BinaryDoesNotFallBackToText) ekb_source, wg_source, false, - false); + false, + ""); } -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); psi::Psi psi_read; ModuleBase::matrix ekb(1, nbands); @@ -432,9 +491,7 @@ TEST_F(ReadWfcNaoTest, RejectTruncatedBinary) ofs.write(reinterpret_cast(&nbands), sizeof(nbands)); ofs.write(reinterpret_cast(&nlocal), sizeof(nlocal)); } -#ifdef __MPI - MPI_Barrier(MPI_COMM_WORLD); -#endif + world_barrier(); psi::Psi psi_read; ModuleBase::matrix ekb(1, nbands); @@ -451,6 +508,114 @@ TEST_F(ReadWfcNaoTest, RejectTruncatedBinary) true)); } +TEST_F(ReadWfcNaoTest, ReadPsiDirectoryLayout) +{ + // RT-TDDFT restart with out_app_flag = 2: the snapshot lives in the + // PSI/ subdirectory of the read-in directory and carries the one-based + // g{step} suffix in its name. + const int nbands = 2; + const int nlocal = 3; + Parallel_Orbitals para; + initialize_parallel_orbitals(para, nlocal, nbands); + + const std::string psi_dir = binary_test_dir + "PSI/"; + if (my_rank == 0) + { + ModuleBase::GlobalFunc::MAKE_DIR(psi_dir); + } + world_barrier(); + + const std::vector coefficients = {0.1, 0.2, 0.3, 1.1, 1.2, 1.3}; + ModuleBase::matrix ekb_source(1, nbands); + ModuleBase::matrix wg_source(1, nbands); + ekb_source(0, 0) = -0.6; + ekb_source(0, 1) = 0.9; + wg_source(0, 0) = 2.0; + wg_source(0, 1) = 0.0; + + const int istep = 0; + const std::vector ik2iktot = {0}; + const int read_type = 2; + const int suffix_app_flag = 2; + // The real-valued reader builds candidate names through the gamma-only + // branch of filename_output, so the snapshot must use the same naming. + const bool gamma_only_for_name = true; + const std::string snapshot = ModuleIO::filename_output(psi_dir, + "wf", + "nao", + 0, + ik2iktot, + 1, + 1, + read_type, + suffix_app_flag, + gamma_only_for_name, + istep); + if (my_rank == 0) + { + ModuleIO::wfc_nao_write2file(snapshot, + coefficients.data(), + nlocal, + 0, + ekb_source, + wg_source, + true, + false, + ""); + } + world_barrier(); + + psi::Psi psi_read; + ModuleBase::matrix ekb(1, nbands); + ModuleBase::matrix wg(1, nbands); + EXPECT_TRUE(ModuleIO::read_wfc_nao(binary_test_dir, + para, + psi_read, + ekb, + wg, + ik2iktot, + 1, + 1, + true, + 0, + istep)); + EXPECT_DOUBLE_EQ(ekb(0, 0), ekb_source(0, 0)); + EXPECT_DOUBLE_EQ(ekb(0, 1), ekb_source(0, 1)); + if (my_rank == 0) + { + EXPECT_DOUBLE_EQ(psi_read(0, 0, 0), coefficients[0]); + } + + // Rewriting the snapshot must replace it, not append another record. + if (my_rank == 0) + { + std::ifstream ifs(snapshot.c_str(), std::ios::binary); + ifs.seekg(0, std::ios::end); + const std::streamoff size_after_first_write = ifs.tellg(); + ifs.close(); + + ModuleIO::wfc_nao_write2file(snapshot, + coefficients.data(), + nlocal, + 0, + ekb_source, + wg_source, + true, + false, + ""); + + std::ifstream ifs_rewritten(snapshot.c_str(), std::ios::binary); + ifs_rewritten.seekg(0, std::ios::end); + const std::streamoff size_after_second_write = ifs_rewritten.tellg(); + ifs_rewritten.close(); + EXPECT_EQ(size_after_second_write, size_after_first_write); + + std::remove(snapshot.c_str()); + std::remove(psi_dir.c_str()); + } + world_barrier(); +} + TEST_F(ReadWfcNaoTest, CompleteSpinPopulations) @@ -484,13 +649,13 @@ TEST_F(ReadWfcNaoTest, CompleteSpinPopulations) if (gamma_only) { ModuleIO::wfc_nao_write2file(file, real_coefficients.data(), nlocal, - ik, energies, occupations, binary, false); + ik, energies, occupations, binary, false, ""); } else { const ModuleBase::Vector3 kpoint(0.25, 0.0, 0.0); ModuleIO::wfc_nao_write2file_complex(file, complex_coefficients.data(), nlocal, - ik, kpoint, energies, occupations, binary, false); + ik, kpoint, energies, occupations, binary, false, ""); } } } @@ -533,7 +698,7 @@ TEST_F(ReadWfcNaoTest, CompleteGlobalKPointPopulations) { const ModuleBase::Vector3 kpoint(0.25 * ik, 0.0, 0.0); ModuleIO::wfc_nao_write2file_complex(file, coefficients.data(), nlocal, - ik, kpoint, energies, occupations, false, false); + ik, kpoint, energies, occupations, false, false, ""); } } std::vector populations; @@ -552,6 +717,154 @@ TEST_F(ReadWfcNaoTest, CompleteGlobalKPointPopulations) } } +TEST_F(ReadWfcNaoTest, PositiveStepRejectsCumulativeFile) +{ + // A positive restart step must not fall back to the fixed-name cumulative + // file (out_app_flag == 1): that file is always read from its first frame, + // so accepting it would silently return step-0 data for a later step. Only + // the step-suffixed candidates are valid for a positive istep, so reading + // a directory that holds ONLY the cumulative file must fail. + const int nbands = 2; + const int nlocal = 3; + Parallel_Orbitals para; + initialize_parallel_orbitals(para, nlocal, nbands); + + const std::vector coefficients = {0.1, 0.2, 0.3, 1.1, 1.2, 1.3}; + ModuleBase::matrix ekb_source(1, nbands); + ModuleBase::matrix wg_source(1, nbands); + ekb_source(0, 0) = -0.5; + ekb_source(0, 1) = 0.7; + wg_source(0, 0) = 2.0; + wg_source(0, 1) = 0.0; + + const std::vector ik2iktot = {0}; + const int read_type = 2; + const int append_app_flag = 1; // fixed name without the step suffix + const bool gamma_only_for_name = true; + const std::string cumulative = ModuleIO::filename_output(binary_test_dir, + "wf", + "nao", + 0, + ik2iktot, + 1, + 1, + read_type, + append_app_flag, + gamma_only_for_name, + -1); + if (my_rank == 0) + { + ModuleIO::wfc_nao_write2file(cumulative, + coefficients.data(), + nlocal, + 0, + ekb_source, + wg_source, + true, + false, + ""); + } + world_barrier(); + + psi::Psi psi_read; + ModuleBase::matrix ekb(1, nbands); + ModuleBase::matrix wg(1, nbands); + const int restart_istep = 5; // positive restart step: no step-suffixed file exists + EXPECT_FALSE(ModuleIO::read_wfc_nao(binary_test_dir, + para, + psi_read, + ekb, + wg, + ik2iktot, + 1, + 1, + true, + 0, + restart_istep)); + if (my_rank == 0) + { + std::remove(cumulative.c_str()); + } + world_barrier(); +} + +TEST_F(ReadWfcNaoTest, ZeroStepFallsBackToCumulativeFile) +{ + // Ordinary first-frame initialization (istep == 0, e.g. non-restart TDDFT) + // must fall back to the append-mode fixed-name file when no step-suffixed + // snapshot exists. The cumulative file holds the first frame only, which is + // exactly what step-zero initialization expects. + const int nbands = 2; + const int nlocal = 3; + Parallel_Orbitals para; + initialize_parallel_orbitals(para, nlocal, nbands); + + const std::vector coefficients = {0.1, 0.2, 0.3, 1.1, 1.2, 1.3}; + ModuleBase::matrix ekb_source(1, nbands); + ModuleBase::matrix wg_source(1, nbands); + ekb_source(0, 0) = -0.5; + ekb_source(0, 1) = 0.7; + wg_source(0, 0) = 2.0; + wg_source(0, 1) = 0.0; + + const std::vector ik2iktot = {0}; + const int read_type = 2; + const int append_app_flag = 1; // fixed name without the step suffix + const bool gamma_only_for_name = true; + const std::string cumulative = ModuleIO::filename_output(binary_test_dir, + "wf", + "nao", + 0, + ik2iktot, + 1, + 1, + read_type, + append_app_flag, + gamma_only_for_name, + -1); + if (my_rank == 0) + { + ModuleIO::wfc_nao_write2file(cumulative, + coefficients.data(), + nlocal, + 0, + ekb_source, + wg_source, + true, + false, + ""); + } + world_barrier(); + + psi::Psi psi_read; + ModuleBase::matrix ekb(1, nbands); + ModuleBase::matrix wg(1, nbands); + const int init_istep = 0; // ordinary first-frame initialization + EXPECT_TRUE(ModuleIO::read_wfc_nao(binary_test_dir, + para, + psi_read, + ekb, + wg, + ik2iktot, + 1, + 1, + true, + 0, + init_istep)); + EXPECT_DOUBLE_EQ(ekb(0, 0), ekb_source(0, 0)); + EXPECT_DOUBLE_EQ(ekb(0, 1), ekb_source(0, 1)); + if (my_rank == 0) + { + EXPECT_DOUBLE_EQ(psi_read(0, 0, 0), coefficients[0]); + } + + if (my_rank == 0) + { + std::remove(cumulative.c_str()); + } + world_barrier(); +} + #ifdef __MPI int main(int argc, char** argv) { diff --git a/source/source_io/unittests/test_write_bands.cpp b/source/source_io/unittests/test_write_bands.cpp index 4fdcbdbb7e5..79990fc0118 100644 --- a/source/source_io/unittests/test_write_bands.cpp +++ b/source/source_io/unittests/test_write_bands.cpp @@ -5,6 +5,7 @@ #include "source_base/module_parallel/para_world.h" #include "source_base/parallel_comm.h" #include "source_base/parallel_global.h" +#include "source_main/version.h" #include "gtest/gtest.h" @@ -227,6 +228,61 @@ class BandOutputTest : public testing::TestWithParam input_.out_band = {1, 8}; } + /// Read the next data line of a band file, skipping the provenance header. + static bool read_data_line(std::istream& stream, std::string& line) + { + while (static_cast(std::getline(stream, line))) + { + const std::size_t first = line.find_first_not_of(" \t"); + if (first != std::string::npos && line[first] == '#') + { + continue; + } + return true; + } + return false; + } + + /// Collect the comment lines of a band file in the order they were written. + std::vector comment_lines(const std::string& name) const + { + std::vector comments; + if (world_->rank() != 0) + { + return comments; + } + std::ifstream file(directory_ + name); + EXPECT_TRUE(file.is_open()) << name; + std::string line; + while (static_cast(std::getline(file, line))) + { + const std::size_t first = line.find_first_not_of(" \t"); + if (first != std::string::npos && line[first] == '#') + { + comments.push_back(line); + } + } + return comments; + } + + /// Check the three comment lines preceding k-point block number `block`. + void expect_header(const std::string& name, int block, int spin, int channels) const + { + if (world_->rank() != 0) + { + return; + } + SCOPED_TRACE(name); + const std::vector comments = comment_lines(name); + ASSERT_GE(comments.size(), static_cast(3 * block + 3)); + EXPECT_EQ(comments[3 * block], "# ABACUS version: " + std::string(VERSION)); + const std::string written = "# Written at "; + EXPECT_EQ(comments[3 * block + 1].compare(0, written.size(), written), 0) + << comments[3 * block + 1]; + const std::string tag = (channels == 2) ? (spin == 0 ? " (spin up)" : " (spin down)") : ""; + EXPECT_EQ(comments[3 * block + 2], "# IONIC STEP " + std::to_string(block + 1) + tag); + } + void expect_output(const std::string& name, int spin, int nbands, double fermi, int precision, int copies) const { @@ -244,7 +300,7 @@ class BandOutputTest : public testing::TestWithParam { for (int ik = 0; ik < 5; ++ik) { - ASSERT_TRUE(static_cast(std::getline(file, line))) << "Missing k-point " << ik; + ASSERT_TRUE(read_data_line(file, line)) << "Missing k-point " << ik; std::istringstream row(line); int index = 0; ASSERT_TRUE(static_cast(row >> index)); @@ -271,7 +327,7 @@ class BandOutputTest : public testing::TestWithParam EXPECT_FALSE(static_cast(row >> extra)) << "Unexpected column: " << extra; } } - EXPECT_FALSE(static_cast(std::getline(file, line))) << "Unexpected line: " << line; + EXPECT_FALSE(read_data_line(file, line)) << "Unexpected line: " << line; } std::string read_text(const std::string& name) const @@ -304,18 +360,21 @@ class BandOutputTest : public testing::TestWithParam { SCOPED_TRACE(distributed ? "distributed" : "replicated"); prepare(nspin, 3, distributed); - ModuleIO::write_bands(input_, ekb_, kv_, ofs_running); + ModuleIO::write_bands(input_, ekb_, kv_, ofs_running, 0); if (nspin == 2) { expect_output("bands1.txt", 0, 3, 0.0, 8, 1); expect_output("bands2.txt", 1, 3, 0.0, 8, 1); expect_missing("band.txt"); + expect_header("bands1.txt", 0, 0, 2); + expect_header("bands2.txt", 0, 1, 2); } else { expect_output("band.txt", 0, 3, 0.0, 8, 1); expect_missing("bands1.txt"); expect_missing("bands2.txt"); + expect_header("band.txt", 0, 0, 1); } // Root must finish reading before another collective call overwrites output. Parallel::barrier(*world_); @@ -326,7 +385,7 @@ class BandOutputTest : public testing::TestWithParam TEST_P(BandOutputTest, DirectSingleSpin) { prepare(1, 3, false); - ModuleIO::nscf_bands(0, directory_ + "direct.txt", 3, 0.0, 8, ekb_, kv_); + ModuleIO::nscf_bands(0, directory_ + "direct.txt", 3, 0.0, 8, ekb_, kv_, ""); expect_output("direct.txt", 0, 3, 0.0, 8, 1); if (world_->rank() == 0) { @@ -339,8 +398,8 @@ TEST_P(BandOutputTest, DirectSingleSpin) TEST_P(BandOutputTest, DirectBothSpins) { prepare(2, 3, false); - ModuleIO::nscf_bands(0, directory_ + "up.txt", 3, 0.0, 8, ekb_, kv_); - ModuleIO::nscf_bands(1, directory_ + "down.txt", 3, 0.0, 8, ekb_, kv_); + ModuleIO::nscf_bands(0, directory_ + "up.txt", 3, 0.0, 8, ekb_, kv_, ""); + ModuleIO::nscf_bands(1, directory_ + "down.txt", 3, 0.0, 8, ekb_, kv_, ""); expect_output("up.txt", 0, 3, 0.0, 8, 1); expect_output("down.txt", 1, 3, 0.0, 8, 1); } @@ -350,7 +409,7 @@ TEST_P(BandOutputTest, PrecisionAndFermiShift) prepare(1, 3, false); for (const int precision : {4, 8}) { - ModuleIO::nscf_bands(0, directory_ + "direct.txt", 3, 0.25, precision, ekb_, kv_); + ModuleIO::nscf_bands(0, directory_ + "direct.txt", 3, 0.25, precision, ekb_, kv_, ""); expect_output("direct.txt", 0, 3, 0.25, precision, 1); if (world_->rank() == 0 && precision == 4) { @@ -366,23 +425,25 @@ TEST_P(BandOutputTest, OverwriteAndAppend) prepare(1, 3, true); seed_file("band.txt", "stale output\n"); Parallel::barrier(*world_); - ModuleIO::write_bands(input_, ekb_, kv_, ofs_running); + ModuleIO::write_bands(input_, ekb_, kv_, ofs_running, 0); expect_output("band.txt", 0, 3, 0.0, 8, 1); + expect_header("band.txt", 0, 0, 1); Parallel::barrier(*world_); parameters_->set_output(directory_, true); - ModuleIO::write_bands(input_, ekb_, kv_, ofs_running); + ModuleIO::write_bands(input_, ekb_, kv_, ofs_running, 1); expect_output("band.txt", 0, 3, 0.0, 8, 2); + expect_header("band.txt", 1, 0, 1); } TEST_P(BandOutputTest, OutputDisabled) { prepare(1, 3, true); input_.out_band[0] = 0; - ModuleIO::write_bands(input_, ekb_, kv_, ofs_running); + ModuleIO::write_bands(input_, ekb_, kv_, ofs_running, 0); expect_missing("band.txt"); seed_file("band.txt", "keep this content\n"); Parallel::barrier(*world_); - ModuleIO::write_bands(input_, ekb_, kv_, ofs_running); + ModuleIO::write_bands(input_, ekb_, kv_, ofs_running, 0); if (world_->rank() == 0) { EXPECT_EQ(read_text("band.txt"), "keep this content\n"); @@ -413,9 +474,11 @@ TEST_P(BandOutputTest, EmptyBandShard) { EXPECT_EQ(ekb_.nc, 0); } - ModuleIO::write_bands(input_, ekb_, kv_, ofs_running); + ModuleIO::write_bands(input_, ekb_, kv_, ofs_running, 0); expect_output("bands1.txt", 0, 1, 0.0, 8, 1); expect_output("bands2.txt", 1, 1, 0.0, 8, 1); + expect_header("bands1.txt", 0, 0, 2); + expect_header("bands2.txt", 0, 1, 2); } TEST_P(BandOutputTest, RunningLogNamesSpinChannels) @@ -425,7 +488,7 @@ TEST_P(BandOutputTest, RunningLogNamesSpinChannels) + std::to_string(world_->rank()) + ".log"; std::ofstream log(log_name); EXPECT_TRUE(log.is_open()); - ModuleIO::write_bands(input_, ekb_, kv_, log); + ModuleIO::write_bands(input_, ekb_, kv_, log, 0); log.close(); std::ifstream in(log_name); diff --git a/source/source_io/unittests/test_write_elf_logic.cpp b/source/source_io/unittests/test_write_elf_logic.cpp index 25269172f73..6bbf65c5f5a 100644 --- a/source/source_io/unittests/test_write_elf_logic.cpp +++ b/source/source_io/unittests/test_write_elf_logic.cpp @@ -170,6 +170,101 @@ TEST_F(ElfLogicTest, Nspin4MixedSigns) } } +// --------------------------------------------------------------------------- +// Zero-grid partition regression tests +// +// When the real-space FFT grid is decomposed across more MPI processes than +// it has z-slabs, some ranks receive nrxx == 0, which is a legitimate empty +// partition (see Parallel_Grid::reduce assert contract). write_elf does NOT +// skip the FFT path on those ranks: PW_Basis::real2recip / recip2real call +// gatherp_scatters, which performs an MPI_Alltoallv requiring every rank in +// POOL_WORLD to participate. Only the per-grid-point loops execute zero +// iterations; the FFT itself must still run so the Alltoallv matches across +// ranks. Every rank also enters write_vdata_palgrid so Parallel_Grid::reduce +// (MPI_Gatherv) sees a collective call. These tests document the zero-length +// container and zero-iteration invariants that keep empty partitions safe. +// --------------------------------------------------------------------------- + +TEST_F(ElfLogicTest, ZeroGridHasLocalGrid) +{ + // A rank holds local grid data only when both nrxx and npw are positive. + // write_elf no longer branches on this (every rank runs the FFT path), + // but the predicate is kept here to document the empty-partition + // condition that motivates the invariant checks below. + auto has_local_grid = [](int nrxx, int npw) { + return nrxx > 0 && npw > 0; + }; + + EXPECT_FALSE(has_local_grid(0, 0)); + EXPECT_FALSE(has_local_grid(0, 100)); + EXPECT_FALSE(has_local_grid(100, 0)); + EXPECT_TRUE(has_local_grid(100, 50)); + EXPECT_TRUE(has_local_grid(1, 1)); +} +TEST_F(ElfLogicTest, ZeroLengthVectorIsLegal) +{ + // std::vector(0) is well-defined and .data() is non-null only + // when size() > 0. Empty partitions pass a null pointer into the MPI + // reduce call, which Parallel_Grid::reduce explicitly tolerates when + // nrxx == 0 (sendcount 0 ignores the buffer). + std::vector empty(0, 0.0); + EXPECT_EQ(empty.size(), 0u); + EXPECT_EQ(empty.data(), nullptr); + + // All-zero non-empty vector for comparison: .data() is non-null. + std::vector zeros(5, 0.0); + EXPECT_EQ(zeros.size(), 5u); + EXPECT_NE(zeros.data(), nullptr); +} + +TEST_F(ElfLogicTest, ZeroGridLoopSkipsAllIterations) +{ + // Every for-loop in write_elf is bounded by `ir < nrxx` or `ip < npw`; + // with nrxx == 0 the loop body must execute zero times. + int iterations = 0; + const int nrxx = 0; + for (int ir = 0; ir < nrxx; ++ir) + { + ++iterations; + } + EXPECT_EQ(iterations, 0); + + iterations = 0; + const int npw = 0; + for (int ip = 0; ip < npw; ++ip) + { + ++iterations; + } + EXPECT_EQ(iterations, 0); +} + +TEST_F(ElfLogicTest, ZeroGridElfValuesRemainInitialised) +{ + // On an empty partition, `elf` and `tau_vw` are all-zero initialised by + // the std::vector constructor and never updated, so every element stays + // at the supplied default. + const int nrxx = 0; + const int nspin_eff = 1; + + std::vector> elf(nspin_eff, std::vector(nrxx, 0.0)); + + EXPECT_EQ(elf.size(), 1u); + EXPECT_EQ(elf[0].size(), 0u); + EXPECT_EQ(elf[0].data(), nullptr); + + // For a non-empty partition the vector still initialises to zero, then + // the computation fills it, documenting the contract contrast. + const int nrxx_nonempty = 8; + std::vector> elf_nonempty(nspin_eff, + std::vector(nrxx_nonempty, 0.0)); + EXPECT_EQ(elf_nonempty[0].size(), 8u); + EXPECT_NE(elf_nonempty[0].data(), nullptr); + for (int i = 0; i < nrxx_nonempty; ++i) + { + EXPECT_DOUBLE_EQ(elf_nonempty[0][i], 0.0); + } +} + int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); diff --git a/source/source_io/unittests/test_write_orb_info.cpp b/source/source_io/unittests/test_write_orb_info.cpp index 6795439f743..2f8d4876dc6 100644 --- a/source/source_io/unittests/test_write_orb_info.cpp +++ b/source/source_io/unittests/test_write_orb_info.cpp @@ -50,7 +50,7 @@ TEST(OrbInfo,WriteOrbInfo) unitcell::cal_nwfc(ofs,*ucell,ucell->atoms, nspin, nlocal, npol, basis_type, esolver_type, init_wfc, nbands); ModuleIO::write_orb_info(ucell); ofs.close(); - std::ifstream ifs("Orbital"); + std::ifstream ifs("nao.txt"); std::string str((std::istreambuf_iterator(ifs)),std::istreambuf_iterator()); EXPECT_THAT(str, testing::HasSubstr("#io spec l m z sym")); EXPECT_THAT(str, testing::HasSubstr("0 Si 0 0 1 s")); @@ -58,7 +58,7 @@ TEST(OrbInfo,WriteOrbInfo) EXPECT_THAT(str, testing::HasSubstr("#sym =Symmetry name of real orbital")); ifs.close(); delete ucell; - std::remove("Orbital"); + std::remove("nao.txt"); std::remove("running.log"); } diff --git a/source/source_io/unittests/test_write_wfc_nao.cpp b/source/source_io/unittests/test_write_wfc_nao.cpp index b5a64f2a336..5f48d79a561 100644 --- a/source/source_io/unittests/test_write_wfc_nao.cpp +++ b/source/source_io/unittests/test_write_wfc_nao.cpp @@ -35,9 +35,9 @@ TEST(GenWfcLcaoFnameTest, OutType1GammaOnlyOutAppFlagTrue) const int nspin = 1; const int nkstot = 1; const int out_type = 1; - const bool out_app_flag = true; + const int out_app_flag = 1; const bool gamma_only = true; - // if out_app_flag = true, then the 'g' label will not show up + // if out_app_flag = 1 (append), then the 'g' label will not show up const int istep = 0; std::string result = ModuleIO::filename_output( @@ -60,9 +60,9 @@ TEST(GenWfcLcaoFnameTest, OutType2GammaOnlyOutAppFlagFalse) const int nspin = 2; const int nkstot = 2; const int out_type = 2; - const bool out_app_flag = false; + const int out_app_flag = 0; const bool gamma_only = true; - // if out_app_flag = false, then the 'g' label appears + // if out_app_flag = 0 (overwrite a fixed file), the 'g' label never appears const int istep = 2; std::string result = ModuleIO::filename_output( @@ -70,7 +70,7 @@ TEST(GenWfcLcaoFnameTest, OutType2GammaOnlyOutAppFlagFalse) nkstot, out_type, out_app_flag, gamma_only, istep); // output .dat file when out_type=2 - std::string expected_output = "wfs2g3_nao.dat"; + std::string expected_output = "wfs2_nao.dat"; EXPECT_EQ(result, expected_output); } @@ -84,7 +84,7 @@ TEST(GenWfcLcaoFnameTest, OutTypeInvalid) const int nspin = 1; const int nkstot = 3; const int out_type = 3; - const bool out_app_flag = true; + const int out_app_flag = 1; const bool gamma_only = false; const int istep = 3; @@ -102,6 +102,34 @@ TEST(GenWfcLcaoFnameTest, OutTypeInvalid) EXPECT_EQ(result, expected_output); } +// --------------------------------------------------------------------------- +// out_app_flag = 2 (write to a per-step subdirectory) is the only mode that +// embeds the g{N} suffix in the filename; directory routing is handled by the +// caller. +// --------------------------------------------------------------------------- +TEST(GenWfcLcaoFnameTest, OutAppFlagMode2EmbedsStep) +{ + const std::string directory = ""; + const std::string property = "wf"; + const std::string basis = "nao"; + const int ik = 1; + const std::vector ik2iktot = {0, 1}; + const int nspin = 2; + const int nkstot = 2; + const int out_type = 2; + const int out_app_flag = 2; + const bool gamma_only = true; + const int istep = 2; + + std::string result = ModuleIO::filename_output( + directory, property, basis, ik, ik2iktot, nspin, + nkstot, out_type, out_app_flag, gamma_only, istep); + + // Same g-suffixed naming as mode 0. + std::string expected_output = "wfs2g3_nao.dat"; + EXPECT_EQ(result, expected_output); +} + template void read_bin(const std::string& name, std::vector& data) { @@ -151,7 +179,7 @@ class WriteWfcLcaoTest : public testing::Test // mohan add 2025-05-11 std::vector ik2iktot = {0,1}; int nkstot = 2; - bool out_app_flag = true; + int out_app_flag = 1; void SetUp() override { @@ -299,6 +327,51 @@ TEST_F(WriteWfcLcaoTest, WriteWfcLcaoComplex) } } +TEST_F(WriteWfcLcaoTest, WriteWfcLcaoTextHeader) +{ + const std::string out_dir = testing::TempDir(); + TestParameters::sys().global_out_dir = out_dir; + + const std::string property = "wf"; + const std::string basis = "nao"; + const int nspin = 2; + const int out_type = 1; + const int out_app_flag = 1; + const bool gamma_only = true; + const int istep = 0; + + psi::Psi my_psi(psi_local_double.data(), nk, nbands_local, nbasis_local, nbasis_local, true); + + ModuleIO::write_wfc_nao(out_type, out_app_flag, my_psi, ekb, wg, kvec_c, + ik2iktot, nkstot, pv, nspin, istep); + + if (GlobalV::MY_RANK == 0) + { + // ik2iktot[0] == 0 belongs to the first spin half of nkstot == 2, + // so the header must carry the one-based step and the spin tag + const std::string fname = ModuleIO::filename_output( + out_dir, property, basis, 0, ik2iktot, nspin, + nkstot, out_type, out_app_flag, gamma_only, istep); + + std::ifstream file1(fname); + ASSERT_TRUE(file1.good()); + std::string first_line; + std::getline(file1, first_line); + EXPECT_THAT(first_line, testing::HasSubstr("# ABACUS version:")); + std::string rest((std::istreambuf_iterator(file1)), std::istreambuf_iterator()); + EXPECT_THAT(rest, testing::HasSubstr("# IONIC STEP 1 (spin up)")); + EXPECT_THAT(rest, testing::HasSubstr("(number of bands)")); + file1.close(); + + std::remove(fname.c_str()); + // the second k-point belongs to the spin-down half; clean it up too + const std::string fname_down = ModuleIO::filename_output( + out_dir, property, basis, 1, ik2iktot, nspin, + nkstot, out_type, out_app_flag, gamma_only, istep); + std::remove(fname_down.c_str()); + } +} + TEST(ModuleIOTest, WriteWfcNao) { if (GlobalV::MY_RANK == 0) @@ -323,7 +396,7 @@ TEST(ModuleIOTest, WriteWfcNao) wg(1, 1) = 1.2; // Call the function to be tested - ModuleIO::wfc_nao_write2file(filename, ctot.data(), TestParameters::sys().nlocal, 0, ekb, wg, false); + ModuleIO::wfc_nao_write2file(filename, ctot.data(), TestParameters::sys().nlocal, 0, ekb, wg, false, false, ""); // Check the output file std::ifstream ifs(filename); @@ -345,6 +418,50 @@ TEST(ModuleIOTest, WriteWfcNao) } } +TEST(ModuleIOTest, WriteWfcNaoHeader) +{ + if (GlobalV::MY_RANK == 0) + { + // Set up GlobalV + GlobalV::DRANK = 0; + TestParameters::input().nbands = 2; + TestParameters::sys().nlocal = 2; + + const std::string filename = std::string(testing::TempDir()) + "test_wfc_nao_header.txt"; + std::vector ctot = {0.1, 0.2, 0.3, 0.4}; + ModuleBase::matrix ekb(2, 2); + ekb(0, 0) = 0.5; + ekb(1, 0) = 0.6; + ekb(0, 1) = 0.7; + ekb(1, 1) = 0.8; + ModuleBase::matrix wg(2, 2); + wg(0, 0) = 0.9; + wg(1, 0) = 1.0; + wg(0, 1) = 1.1; + wg(1, 1) = 1.2; + + const std::string header = "# ABACUS version: test\n" + "# Written at 2026-01-01 00:00:00\n" + "# IONIC STEP 1 (spin up)\n"; + + // Call the function to be tested + ModuleIO::wfc_nao_write2file(filename, ctot.data(), TestParameters::sys().nlocal, 0, ekb, wg, false, false, header); + + // Check the output file: the provenance header precedes the data block + std::ifstream ifs(filename); + std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + EXPECT_THAT(str, testing::HasSubstr("# ABACUS version: test\n")); + EXPECT_THAT(str, testing::HasSubstr("# Written at 2026-01-01 00:00:00\n")); + EXPECT_THAT(str, testing::HasSubstr("# IONIC STEP 1 (spin up)\n")); + EXPECT_THAT(str, testing::HasSubstr("2 (number of bands)")); + EXPECT_THAT(str, testing::HasSubstr("1.00000000e-01 2.00000000e-01")); + ifs.close(); + + // clean up + std::remove(filename.c_str()); + } +} + TEST(ModuleIOTest, WriteWfcNaoBinary) { if (GlobalV::MY_RANK == 0) @@ -369,7 +486,7 @@ TEST(ModuleIOTest, WriteWfcNaoBinary) wg(1, 1) = 1.2; // Call the function to be tested - ModuleIO::wfc_nao_write2file(filename, ctot.data(), TestParameters::sys().nlocal, 0, ekb, wg, true); + ModuleIO::wfc_nao_write2file(filename, ctot.data(), TestParameters::sys().nlocal, 0, ekb, wg, true, false, ""); // Check the output file Binstream wfc(filename, "r"); @@ -402,6 +519,46 @@ TEST(ModuleIOTest, WriteWfcNaoBinary) } } +TEST(ModuleIOTest, WriteWfcNaoBinaryIgnoresHeader) +{ + if (GlobalV::MY_RANK == 0) + { + // Set up GlobalV + GlobalV::DRANK = 0; + TestParameters::input().nbands = 2; + TestParameters::sys().nlocal = 2; + + const std::string filename = std::string(testing::TempDir()) + "test_wfc_nao_header.dat"; + std::vector ctot = {0.1, 0.2, 0.3, 0.4}; + ModuleBase::matrix ekb(2, 2); + ekb(0, 0) = 0.5; + ekb(1, 0) = 0.6; + ekb(0, 1) = 0.7; + ekb(1, 1) = 0.8; + ModuleBase::matrix wg(2, 2); + wg(0, 0) = 0.9; + wg(1, 0) = 1.0; + wg(0, 1) = 1.1; + wg(1, 1) = 1.2; + + const std::string header = "# ABACUS version: test\n"; + ModuleIO::wfc_nao_write2file(filename, ctot.data(), TestParameters::sys().nlocal, 0, ekb, wg, true, false, header); + + // the binary record must start with the band count, not the header + Binstream wfc(filename, "r"); + int nbands = 0; + int nlocal = 0; + wfc >> nbands; + wfc >> nlocal; + EXPECT_EQ(nbands, 2); + EXPECT_EQ(nlocal, 2); + wfc.close(); + + // clean up + std::remove(filename.c_str()); + } +} + TEST(ModuleIOTest, WriteWfcNaoComplex) { if (GlobalV::MY_RANK == 0) @@ -427,7 +584,7 @@ TEST(ModuleIOTest, WriteWfcNaoComplex) std::complex(0.0, 3.0)}; // Call the function - ModuleIO::wfc_nao_write2file_complex(name, ctot.data(), TestParameters::sys().nlocal, ik, kvec_c, ekb, wg); + ModuleIO::wfc_nao_write2file_complex(name, ctot.data(), TestParameters::sys().nlocal, ik, kvec_c, ekb, wg, false, false, ""); // Check the output file std::ifstream ifs(name); std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); @@ -470,7 +627,7 @@ TEST(ModuleIOTest, WriteWfcNaoComplexBinary) std::complex(6.0, 8.0)}; // Call the function - ModuleIO::wfc_nao_write2file_complex(name, ctot.data(), TestParameters::sys().nlocal, ik, kvec_c, ekb, wg, true); + ModuleIO::wfc_nao_write2file_complex(name, ctot.data(), TestParameters::sys().nlocal, ik, kvec_c, ekb, wg, true, false, ""); // Check the output file Binstream wfc(name, "r"); @@ -529,7 +686,7 @@ TEST(ModuleIOTest, WriteWfcNaoBinaryOpenFail) ModuleBase::matrix wg(2, 2); testing::internal::CaptureStdout(); - EXPECT_EXIT(ModuleIO::wfc_nao_write2file(filename, ctot.data(), nlocal, 0, ekb, wg, true), + EXPECT_EXIT(ModuleIO::wfc_nao_write2file(filename, ctot.data(), nlocal, 0, ekb, wg, true, false, ""), ::testing::ExitedWithCode(1), ""); std::string output = testing::internal::GetCapturedStdout(); @@ -560,7 +717,7 @@ TEST(ModuleIOTest, WriteWfcNaoComplexBinaryOpenFail) std::complex(0.0, 3.0)}; testing::internal::CaptureStdout(); - EXPECT_EXIT(ModuleIO::wfc_nao_write2file_complex(name, ctot.data(), nlocal, ik, kvec_c, ekb, wg, true), + EXPECT_EXIT(ModuleIO::wfc_nao_write2file_complex(name, ctot.data(), nlocal, ik, kvec_c, ekb, wg, true, false, ""), ::testing::ExitedWithCode(1), ""); std::string output = testing::internal::GetCapturedStdout(); diff --git a/source/source_lcao/module_lr/lr_density.cpp b/source/source_lcao/module_lr/lr_density.cpp index f2186ed806f..ecf931298bb 100644 --- a/source/source_lcao/module_lr/lr_density.cpp +++ b/source/source_lcao/module_lr/lr_density.cpp @@ -87,7 +87,7 @@ void LR_Density::write_density_single_state(const double* const* const densit { const std::string data_desc = "LR-TDDFT electron-hole density difference (electrons/Bohr^3)"; ModuleIO::write_vdata_palgrid(pgrid_, density[0], 0, 1, 0, filepath, 0.0, &ucell_, - this->out_chg_precision_, 0, false, true, ofs_running, data_desc); + this->out_chg_precision_, 0, false, true, false, ofs_running, data_desc); } template diff --git a/source/source_lcao/module_lr/test/test_lr_util.cpp b/source/source_lcao/module_lr/test/test_lr_util.cpp index 7aaae902029..928fd4f3414 100644 --- a/source/source_lcao/module_lr/test/test_lr_util.cpp +++ b/source/source_lcao/module_lr/test/test_lr_util.cpp @@ -237,7 +237,8 @@ TEST_F(LRDMTest, SparseWriterKeepsSpinFilesAndDisjointRankKeys) ASSERT_TRUE(input.is_open()); std::string line; std::getline(input, line); - EXPECT_EQ(line, "STEP: 0"); + // Header step is one-based per governance rule 22 (writer receives istep = 0). + EXPECT_EQ(line, "STEP: 1"); std::getline(input, line); EXPECT_EQ(line, "Matrix Dimension of EDM(R): " + std::to_string(dimension)); std::getline(input, line); diff --git a/source/source_lcao/module_lr/utils/exciton_plotter.cpp b/source/source_lcao/module_lr/utils/exciton_plotter.cpp index 037ba0586d7..5bfe7091a76 100644 --- a/source/source_lcao/module_lr/utils/exciton_plotter.cpp +++ b/source/source_lcao/module_lr/utils/exciton_plotter.cpp @@ -510,6 +510,7 @@ void ExcitonPlotter::plot_average_density(const int istate, const std::string 0, false, /*two_fermi*/ false, + false, GlobalV::ofs_running, "exciton density"); } diff --git a/source/source_lcao/test/test_init_dm_from_file.cpp b/source/source_lcao/test/test_init_dm_from_file.cpp index 99647533ab5..408c5e4b61c 100644 --- a/source/source_lcao/test/test_init_dm_from_file.cpp +++ b/source/source_lcao/test/test_init_dm_from_file.cpp @@ -107,7 +107,8 @@ class InitDMFileTest : public testing::Test } std::string fname = filename; - ModuleIO::write_dmr_csr(fname, &ucell, 8, &hc, 0, ispin, nspin); + const bool append = false; + ModuleIO::write_dmr_csr(fname, &ucell, 8, &hc, 0, ispin, nspin, append); } /// Create DensityMatrix with given nspin and initialize DMR from an HContainer template diff --git a/source/source_lcao/test/test_output_hcontainer_consistency.cpp b/source/source_lcao/test/test_output_hcontainer_consistency.cpp index bed577dc630..e5f912f2e00 100644 --- a/source/source_lcao/test/test_output_hcontainer_consistency.cpp +++ b/source/source_lcao/test/test_output_hcontainer_consistency.cpp @@ -115,7 +115,8 @@ class OutputHContainerTest : public testing::Test int ispin, int nspin, int precision = 8) { std::string fname = filename; - ModuleIO::write_dmr_csr(fname, &ucell, precision, hc, 0, ispin, nspin); + const bool append = false; + ModuleIO::write_dmr_csr(fname, &ucell, precision, hc, 0, ispin, nspin, append); } }; diff --git a/source/source_md/md_base.cpp b/source/source_md/md_base.cpp index dc583afb136..94803caade5 100644 --- a/source/source_md/md_base.cpp +++ b/source/source_md/md_base.cpp @@ -62,7 +62,15 @@ void MD_base::setup(ModuleESolver::ESolver* p_esolver, const std::string& global ModuleIO::print_screen(stress_step, force_step, istep_print); - MD_func::force_virial(p_esolver, step_, mdcell, decomp, potential, cal_stress, virial, mdp.md_out_force); + // Pass the global (1 + restarted) step index so that output writers see a + // non-zero step on a restarted run's first step and append to the + // accumulated files instead of truncating the previous run's records. + const int global_step = step_ + step_rst_; + // The initial force evaluation of a run has no preceding MD drift, so it + // must not advance the charge-extrapolation history -- even on restart + // (step_rst_ > 0), where the extrapolation state starts degenerate. + const bool positions_updated = false; + MD_func::force_virial(p_esolver, global_step, positions_updated, mdcell, decomp, potential, cal_stress, virial, mdp.md_out_force); MD_func::compute_stress(mdcell, cal_stress, virial, stress); if (mdcell.has_backing_unitcell()) { diff --git a/source/source_md/md_func.cpp b/source/source_md/md_func.cpp index 55927c345bf..5649df9ba0d 100644 --- a/source/source_md/md_func.cpp +++ b/source/source_md/md_func.cpp @@ -312,6 +312,7 @@ void init_vel(MDCell& mdcell, void force_virial(ModuleESolver::ESolver* p_esolver, const int& istep, + const bool& positions_updated, MDCell& mdcell, DomainDecomposition& decomp, double& potential, @@ -343,8 +344,9 @@ void force_virial(ModuleESolver::ESolver* p_esolver, { backing_velocities[static_cast(it)] = ucell.atoms[it].vel; } - // Step zero evaluates the initial coordinates before the first MD drift. - const bool positions_updated = istep > 0; + // The caller reports whether positions drifted in this run; the + // initial force evaluation (also of a restarted run) passes false so + // the charge-extrapolation history only advances after an MD drift. mdcell.sync_backing_unitcell(positions_updated); for (int it = 0; it < ucell.ntype; ++it) { diff --git a/source/source_md/md_func.h b/source/source_md/md_func.h index 5dc21f2f85a..c35dab47e5e 100644 --- a/source/source_md/md_func.h +++ b/source/source_md/md_func.h @@ -84,6 +84,9 @@ void rescale_vel(const int& natom, * * @param p_esolver enrergy solver * @param istep current md step + * @param positions_updated whether ionic positions have drifted in this run; + * the initial force evaluation of a run (including a restarted run) + * passes false so the charge-extrapolation history is not advanced * @param mdcell MD cell information * @param potential potential energy * @param cal_stress whether calculate stress @@ -91,6 +94,7 @@ void rescale_vel(const int& natom, */ void force_virial(ModuleESolver::ESolver* p_esolver, const int& istep, + const bool& positions_updated, MDCell& mdcell, DomainDecomposition& decomp, double& potential, diff --git a/source/source_md/run_md.cpp b/source/source_md/run_md.cpp index d71ac5679a1..66f208a3719 100644 --- a/source/source_md/run_md.cpp +++ b/source/source_md/run_md.cpp @@ -1,6 +1,7 @@ #include "run_md.h" #include "source_base/constants.h" +#include "source_base/fs_compat.h" #include "source_base/global_function.h" #include "source_base/global_variable.h" #include "source_base/parallel_cell.h" @@ -25,6 +26,7 @@ #include "source_md/module_plumed/plumed_interface.h" #include +#include #ifdef __MPI #include #endif @@ -32,6 +34,11 @@ namespace Run_MD { +bool is_cadence_step(const int current_step, const int freq) +{ + return freq > 0 && current_step % freq == 0; +} + void prepare_mdcell(MDCell& mdcell, const Parameter& param_in, DomainDecomposition& decomp) { const Input_para& input = param_in.inp; @@ -188,6 +195,37 @@ void md_line(MDCell& mdcell, apply_plumed_forces(mdcell, plumed_forces); }; + /// Effective ionic-step output frequency for STRU and other per-step + /// output files. Prefer out_freq_ion; fall back to md_restartfreq for + /// backward compatibility when out_freq_ion is not set (0). + const int eff_output_freq = (param_in.inp.out_freq_ion > 0) + ? param_in.inp.out_freq_ion + : param_in.mdp.md_restartfreq; + + /// Restart-file cadence is independent of the output cadence so that + /// out_freq_ion and md_restartfreq can be set to different values. + const int restart_freq = param_in.mdp.md_restartfreq; + + /// out_app_flag == 1 writes one fixed-name "STRU" file. A previous + /// release kept per-step STRU_MD_{step} files inside a STRU/ + /// subdirectory of the output directory (the mode-2 directory has + /// since been renamed to STRUC/), so an MD restarted from those + /// files still sees that directory. Writing the fixed name on top of a + /// directory aborts the run, therefore the legacy numbered layout is + /// kept whenever such a directory is present. + const bool append_stru_allowed = (param_in.inp.out_app_flag == 1); + bool has_legacy_stru_dir = false; + std::string stru_out_dir = PARAM.globalv.global_stru_dir; + if (append_stru_allowed) + { + struct stat stru_path; + if (stat((stru_out_dir + "STRU").c_str(), &stru_path) == 0 && S_ISDIR(stru_path.st_mode)) + { + has_legacy_stru_dir = true; + stru_out_dir += "STRU/"; + } + } + /// md cycle, mohan update 2026-01-04, change '<=' to '<' while ((mdrun->step_ + mdrun->step_rst_) < param_in.mdp.md_nstep && !mdrun->stop) { @@ -240,8 +278,16 @@ void md_line(MDCell& mdcell, mdrun->first_half(GlobalV::ofs_running); /// update force and virial due to the update of atom positions + /// Use the global (1 + restarted) step index so a restarted run's + /// first local step still appends to the accumulated output files + /// instead of truncating the previous run's records. + const int global_step = mdrun->step_ + mdrun->step_rst_; + /// first_half has already drifted the positions in this run, so + /// the charge-extrapolation history may advance. + const bool positions_updated = true; MD_func::force_virial(p_esolver, - mdrun->step_, + global_step, + positions_updated, mdcell, decomp, mdrun->potential, @@ -280,17 +326,51 @@ void md_line(MDCell& mdcell, mdrun->virial); } - if (param_in.mdp.md_restartfreq > 0 - && (mdrun->step_ + mdrun->step_rst_) % param_in.mdp.md_restartfreq == 0) + const int current_step = mdrun->step_ + mdrun->step_rst_; + const bool do_output_step = is_cadence_step(current_step, eff_output_freq); + if (do_output_step) { if (mdcell.has_backing_unitcell()) { const bool positions_updated = false; mdcell.sync_backing_unitcell(positions_updated); } - std::stringstream file; - file << PARAM.globalv.global_stru_dir << "STRU_MD_" << mdrun->step_ + mdrun->step_rst_; - mdcell::print_stru_file(mdcell, mdcell.stru_meta(), file.str()); + if (param_in.inp.out_stru > 0) + { + // Mode-2 STRUC/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(stru_out_dir); + const int stru_step = current_step + 1; + // File-name selection and append/truncate are two independent + // decisions. The file name is fixed to "STRU" whenever mode 1 + // accumulation is allowed (and no legacy STRU/ directory is in + // the way); modes 0/2 keep the numbered STRU{stru_step} files. + // Whether to append depends on whether this is the very first + // step of a fresh (non-restart) run: a fresh first frame + // truncates the fixed-name file so it does not keep frames + // from a previous run, while later steps and restarted runs + // append one frame each, introduced by a one-based + // "# MD step N" comment line. + const bool use_fixed_name = append_stru_allowed && !has_legacy_stru_dir; + const bool is_first_step_of_fresh_run = (mdrun->step_ == 0) && (mdrun->step_rst_ == 0); + const bool append_stru = use_fixed_name && !is_first_step_of_fresh_run; + const std::string frame_comment = use_fixed_name + ? "# MD step " + std::to_string(stru_step) + "\n\n" + : ""; + std::stringstream file; + file << stru_out_dir << "STRU"; + if (!use_fixed_name) + { + file << stru_step; + } + mdcell::print_stru_file(mdcell, mdcell.stru_meta(), file.str(), frame_comment, append_stru); + } + } + + /// Restart files follow md_restartfreq independently of the output + /// cadence, so setting out_freq_ion does not suppress or re-aim + /// md_restartfreq. + if (is_cadence_step(current_step, restart_freq)) + { mdrun->write_restart(PARAM.globalv.global_out_dir); } diff --git a/source/source_md/run_md.h b/source/source_md/run_md.h index 8abf531a571..7edb18eeb61 100644 --- a/source/source_md/run_md.h +++ b/source/source_md/run_md.h @@ -17,6 +17,12 @@ class ESolver; */ namespace Run_MD { +/// Returns true when a per-step action with cadence frequency `freq` should +/// fire at `current_step`. A frequency <= 0 disables the action (never +/// fires). Keeping output and restart cadences behind this predicate makes +/// their independence directly unit-testable. +bool is_cadence_step(int current_step, int freq); + void prepare_mdcell(MDCell& mdcell, const Parameter& param_in, DomainDecomposition& decomp); void prepare_mdcell(MDCell& mdcell, UnitCell& ucell, DomainDecomposition& decomp); diff --git a/source/source_md/test/lj_pot_test.cpp b/source/source_md/test/lj_pot_test.cpp index 0ae49fb2b41..9bfd08d1c02 100644 --- a/source/source_md/test/lj_pot_test.cpp +++ b/source/source_md/test/lj_pot_test.cpp @@ -48,7 +48,8 @@ TEST_F(LJ_pot_test, potential) DomainDecomposition decomp; decomp.init(ModuleBase::world_comm_domain(), mdcell.latvec(), mdcell.lat0(), 0.0, 0.0); p_esolver->before_all_runners(mdcell, inp); - MD_func::force_virial(p_esolver, 0, mdcell, decomp, potential, true, stress, false); + const bool positions_updated = false; + MD_func::force_virial(p_esolver, 0, positions_updated, mdcell, decomp, potential, true, stress, false); EXPECT_NEAR(potential, -0.011957818623534381, doublethreshold); } @@ -73,7 +74,8 @@ TEST_F(LJ_pot_test, force) DomainDecomposition decomp; decomp.init(ModuleBase::world_comm_domain(), mdcell.latvec(), mdcell.lat0(), 0.0, 0.0); p_esolver->before_all_runners(mdcell, inp); - MD_func::force_virial(p_esolver, 0, mdcell, decomp, potential, true, stress, false); + const bool positions_updated = false; + MD_func::force_virial(p_esolver, 0, positions_updated, mdcell, decomp, potential, true, stress, false); const std::vector& atoms = mdcell.owned_atoms(); EXPECT_NEAR(atoms[0].force.x, 0.00049817733089377704, doublethreshold); EXPECT_NEAR(atoms[0].force.y, 0.00082237246837022328, doublethreshold); @@ -119,7 +121,8 @@ TEST_F(LJ_pot_test, stress) DomainDecomposition decomp; decomp.init(ModuleBase::world_comm_domain(), mdcell.latvec(), mdcell.lat0(), 0.0, 0.0); p_esolver->before_all_runners(mdcell, inp); - MD_func::force_virial(p_esolver, 0, mdcell, decomp, potential, true, stress, false); + const bool positions_updated = false; + MD_func::force_virial(p_esolver, 0, positions_updated, mdcell, decomp, potential, true, stress, false); EXPECT_NEAR(stress(0, 0), 8.0360222227631859e-07, doublethreshold); EXPECT_NEAR(stress(0, 1), 1.7207745586539077e-07, doublethreshold); EXPECT_NEAR(stress(0, 2), 0.0, doublethreshold); diff --git a/source/source_md/test/run_md_test.cpp b/source/source_md/test/run_md_test.cpp index 9c459d3e886..1d6fe731102 100644 --- a/source/source_md/test/run_md_test.cpp +++ b/source/source_md/test/run_md_test.cpp @@ -60,3 +60,51 @@ TEST(RunMDTest, prepare_mdcell_from_unitcell) EXPECT_EQ(atom.mbl.x, 0); EXPECT_EQ(atom.owner_rank, 0); } + +// --- Cadence independence: output (out_freq_ion) and restart +// (md_restartfreq) must be driven by their own frequencies so that +// setting out_freq_ion does not silently re-aim md_restartfreq. + +TEST(RunMDCadenceTest, disabled_when_freq_non_positive) +{ + for (int step = 0; step <= 20; ++step) + { + EXPECT_FALSE(Run_MD::is_cadence_step(step, 0)); + EXPECT_FALSE(Run_MD::is_cadence_step(step, -1)); + } +} + +TEST(RunMDCadenceTest, fires_every_freq_steps) +{ + const int freq = 3; + for (int step = 0; step <= 20; ++step) + { + const bool expected = (step % freq == 0); + EXPECT_EQ(Run_MD::is_cadence_step(step, freq), expected) << "step=" << step; + } +} + +// Core regression guard: with out_freq_ion and md_restartfreq set to +// different positive values, the two cadences must disagree on at least +// one step. This guarantees md_restartfreq is no longer swallowed by +// out_freq_ion > 0. +TEST(RunMDCadenceTest, output_and_restart_are_independent) +{ + const int out_freq_ion = 4; + const int md_restartfreq = 6; + + bool found_disagreement = false; + for (int step = 0; step <= 60; ++step) + { + const bool output_step = Run_MD::is_cadence_step(step, out_freq_ion); + const bool restart_step = Run_MD::is_cadence_step(step, md_restartfreq); + if (output_step != restart_step) + { + found_disagreement = true; + break; + } + } + EXPECT_TRUE(found_disagreement) + << "out_freq_ion=" << out_freq_ion << " and md_restartfreq=" << md_restartfreq + << " produced identical cadences; md_restartfreq is still being ignored."; +} diff --git a/source/source_md/test/test_md_func.cpp b/source/source_md/test/test_md_func.cpp index 9e0aef2f6ee..5e335a4572b 100644 --- a/source/source_md/test/test_md_func.cpp +++ b/source/source_md/test/test_md_func.cpp @@ -64,7 +64,8 @@ TEST(MdForceVirialTest, ReturnsGhostForcesBeforeConvertingUnitsExactlyOnce) ModuleBase::matrix virial(3, 3); for (int step = 0; step < 2; ++step) { - MD_func::force_virial(&solver, step, cell, decomp, potential, true, virial, false); + const bool positions_updated = step > 0; + MD_func::force_virial(&solver, step, positions_updated, cell, decomp, potential, true, virial, false); EXPECT_DOUBLE_EQ(potential, 3.0); EXPECT_DOUBLE_EQ(virial(0, 0), 4.0); ASSERT_EQ(cell.owned_atoms().size(), 1); diff --git a/source/source_md/unittests/test_md_base.cpp b/source/source_md/unittests/test_md_base.cpp index c0fa9644d74..7405c92efca 100644 --- a/source/source_md/unittests/test_md_base.cpp +++ b/source/source_md/unittests/test_md_base.cpp @@ -526,12 +526,29 @@ TEST_F(MdBoundaryTest, InitialForceEvaluationDoesNotAdvancePositionUpdateState) } std::ofstream log; integrator.first_half(log); - MD_func::force_virial(&solver, 1, cell, decomposition, + const bool positions_updated = true; + MD_func::force_virial(&solver, 1, positions_updated, cell, decomposition, integrator.potential, false, integrator.virial, false); ASSERT_EQ(solver.observed_position_updates.size(), 2u); EXPECT_TRUE(solver.observed_position_updates[1]); } +TEST_F(MdBoundaryTest, RestartedSetupDoesNotAdvancePositionUpdateState) +{ + const int rank = domain.rank(); + MD_base integrator(parameters, false, true, rank, cell); + PositionStateSolver solver; + const std::string readin_directory; + // Simulate a restarted run: the global step (step_ + step_rst_) is already + // nonzero at the first force evaluation, but no drift happened in this + // run, so the charge-extrapolation history must not advance. + integrator.step_rst_ = 2; + backing.ionic_position_updated = false; + integrator.setup(&solver, readin_directory, decomposition); + ASSERT_EQ(solver.observed_position_updates.size(), 1u); + EXPECT_FALSE(solver.observed_position_updates[0]); +} + TEST_F(MdBoundaryTest, InitialSyncPreservesPositionUpdateState) { for (int index = 0; index < 4; ++index) diff --git a/source/source_psi/psi_init_file.cpp b/source/source_psi/psi_init_file.cpp index 2087051fc12..2c422393107 100644 --- a/source/source_psi/psi_init_file.cpp +++ b/source/source_psi/psi_init_file.cpp @@ -52,7 +52,7 @@ void psi_init_file::init_psig(T* psig, const int& ik) // mohan update, this is for plane wave, 2025-05-17 const int out_type = 2; - const bool out_app_flag = false; + const int out_app_flag = 0; const bool gamma_only = false; const int istep = -1; diff --git a/source/source_relax/relax_driver.cpp b/source/source_relax/relax_driver.cpp index cf56ffd2da4..a8f13286875 100644 --- a/source/source_relax/relax_driver.cpp +++ b/source/source_relax/relax_driver.cpp @@ -4,6 +4,7 @@ #include "relax_stru_io.h" #include "socket_driver.h" #include "source_base/formatter.h" +#include "source_base/fs_compat.h" #include "source_base/global_file.h" #include "source_base/module_out/read_exit_file.h" #include "source_io/module_json/output_info.h" @@ -21,7 +22,7 @@ void Relax_Driver::relax_driver(ModuleESolver::ESolver* p_esolver, // Cache global configuration once so downstream helpers do not each reach // into PARAM / GlobalV. - out_dir_ = PARAM.globalv.global_out_dir; + out_dir_ = PARAM.globalv.global_stru_dir; deepks_setorb_ = PARAM.globalv.deepks_setorb; my_rank_ = GlobalV::MY_RANK; @@ -226,7 +227,7 @@ void Relax_Driver::stru_out(const int istep, const ModuleBase::matrix& stress, const ModuleBase::matrix& force) { - // out_stru is effective for scf/nscf/relax/cell-relax (md writes STRU_MD_* via md_restartfreq) + // out_stru is effective for scf/nscf/relax/cell-relax; md handles STRU output in run_md.cpp if (inp.calculation != "relax" && inp.calculation != "cell-relax" && inp.calculation != "scf" && inp.calculation != "nscf") { @@ -254,6 +255,8 @@ void Relax_Driver::stru_out(const int istep, // is written in final_out() to avoid a duplicate file. if (is_relax) { + // Mode-2 STRUC/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(out_dir_); const std::string now_file = out_dir_ + (inp.out_stru == 1 ? "STRU_NOW" : "STRU_NOW.cif"); relax_stru_io::write_stru(ucell, inp, @@ -263,13 +266,21 @@ void Relax_Driver::stru_out(const int istep, need_orb, deepks_setorb_, my_rank_, - inp.cal_force); + inp.cal_force, + false); } // Numbered files per out_freq_ion: only meaningful for relaxation calculations - if (is_relax && freq_ok) + if (is_relax && freq_ok && inp.out_app_flag != 0) { - const std::string step_file = out_dir_ + "STRU" + std::to_string(istep + 1) + (inp.out_stru == 1 ? "" : ".cif"); + // Mode 1: single fixed-name STRU accumulates one block per step (CIF + // format truncates to the latest frame instead). Mode 2 keeps the + // numbered STRU{istep+1} files. Mode 0 only writes STRU_NOW, which is + // already overwritten each step above. + const bool append_stru = (inp.out_app_flag == 1) && (inp.out_stru == 1) && (istep > 0); + const std::string step_file = (inp.out_app_flag == 1) + ? out_dir_ + (inp.out_stru == 1 ? "STRU" : "STRU.cif") + : out_dir_ + "STRU" + std::to_string(istep + 1) + (inp.out_stru == 1 ? "" : ".cif"); relax_stru_io::write_stru(ucell, inp, step_file, @@ -278,7 +289,8 @@ void Relax_Driver::stru_out(const int istep, need_orb, deepks_setorb_, my_rank_, - inp.cal_force); + inp.cal_force, + append_stru); } } @@ -322,6 +334,8 @@ void Relax_Driver::final_out(const int istep, // 1: write STRU_FINAL; 2: write STRU_FINAL.cif if (stru_effective && (inp.out_stru == 1 || inp.out_stru == 2)) { + // Mode-2 STRUC/ subdirectory is created lazily at write time. + ModuleBase::ensure_directory(out_dir_); const std::string header = relax_stru_io::build_stru_header(istep, etot, stress, inp, true, geometry_evaluated); const bool need_orb = relax_stru_io::need_orbital(inp); const std::string final_file = out_dir_ + (inp.out_stru == 1 ? "STRU_FINAL" : "STRU_FINAL.cif"); @@ -336,7 +350,8 @@ void Relax_Driver::final_out(const int istep, need_orb, deepks_setorb_, my_rank_, - write_force); + write_force, + false); } if (stop_reason == RelaxStopReason::MaxSteps) diff --git a/source/source_relax/relax_stru_io.cpp b/source/source_relax/relax_stru_io.cpp index 0f191ac644b..21883283cd2 100644 --- a/source/source_relax/relax_stru_io.cpp +++ b/source/source_relax/relax_stru_io.cpp @@ -117,7 +117,8 @@ void write_stru(UnitCell& ucell, const bool need_orb, const bool deepks_setorb, const int my_rank, - const bool has_force) + const bool has_force, + const bool append) { if (inp.out_stru == 1) { @@ -133,7 +134,8 @@ void write_stru(UnitCell& ucell, deepks_setorb, my_rank, force, - has_force); + has_force, + append); } else if (inp.out_stru == 2) { diff --git a/source/source_relax/relax_stru_io.h b/source/source_relax/relax_stru_io.h index 2830f4b09b7..87e37882b07 100644 --- a/source/source_relax/relax_stru_io.h +++ b/source/source_relax/relax_stru_io.h @@ -64,6 +64,8 @@ bool need_orbital(const Input_para& inp); * @param has_force true to emit per-atom f fields; forwarded to * print_stru_file. Callers must pass false when the forces do not * belong to the geometry being written (e.g. early relax exit). + * @param append true to append the structure block (STRU format only, + * for out_app_flag == 1 multi-step runs); CIF always truncates. */ void write_stru(UnitCell& ucell, const Input_para& inp, @@ -73,7 +75,8 @@ void write_stru(UnitCell& ucell, const bool need_orb, const bool deepks_setorb, const int my_rank, - const bool has_force); + const bool has_force, + const bool append); } // namespace relax_stru_io #endif diff --git a/source/source_relax/unittests/test_relax_driver.cpp b/source/source_relax/unittests/test_relax_driver.cpp index 3f832eabf58..bcd818013a3 100644 --- a/source/source_relax/unittests/test_relax_driver.cpp +++ b/source/source_relax/unittests/test_relax_driver.cpp @@ -100,6 +100,7 @@ void write_stru(UnitCell&, const bool, const bool, const int, + const bool, const bool) { } diff --git a/tests/01_PW/md_nvt/result.ref b/tests/01_PW/md_nvt/result.ref index 447ab04b814..e38155d4a9e 100644 --- a/tests/01_PW/md_nvt/result.ref +++ b/tests/01_PW/md_nvt/result.ref @@ -1,5 +1,5 @@ -etotref -211.8028658619059 -etotperatomref -105.9014329310 -totalforceref 0.334170 -totalstressref 380.831666 +etotref -211.8028657875923 +etotperatomref -105.9014328938 +totalforceref 0.334198 +totalstressref 380.833478 totaltimeref 3.13 diff --git a/tests/02_NAO_Gamma/md_out_hk_spin2/INPUT b/tests/02_NAO_Gamma/md_out_hk_spin2/INPUT index 02da18f7a6a..802e4a0aa1a 100644 --- a/tests/02_NAO_Gamma/md_out_hk_spin2/INPUT +++ b/tests/02_NAO_Gamma/md_out_hk_spin2/INPUT @@ -7,6 +7,7 @@ basis_type lcao gamma_only 1 nspin 2 out_mat_hs 1 5 +out_app_flag 1 #Parameters (Accuracy) ecutwfc 5 diff --git a/tests/02_NAO_Gamma/md_out_hk_syns/INPUT b/tests/02_NAO_Gamma/md_out_hk_syns/INPUT index b3ad0c1822f..dfe5ef88f8a 100644 --- a/tests/02_NAO_Gamma/md_out_hk_syns/INPUT +++ b/tests/02_NAO_Gamma/md_out_hk_syns/INPUT @@ -6,6 +6,7 @@ calculation md basis_type lcao gamma_only 1 out_mat_hs 1 5 +out_app_flag 1 #Parameters (Accuracy) ecutwfc 5 diff --git a/tests/02_NAO_Gamma/scf_out_hxc/INPUT b/tests/02_NAO_Gamma/scf_out_hxc/INPUT index b53704a5dd3..5fc20dc3944 100644 --- a/tests/02_NAO_Gamma/scf_out_hxc/INPUT +++ b/tests/02_NAO_Gamma/scf_out_hxc/INPUT @@ -24,7 +24,7 @@ smearing_sigma 0.002 out_mat_xc 1 out_mat_xc2 1 -out_eband_terms 1 +out_eband 1 #Parameters (5.Mixing) mixing_type broyden diff --git a/tests/02_NAO_Gamma/scf_out_hxc/Vxc_R_spin0.ref b/tests/02_NAO_Gamma/scf_out_hxc/Vxc_R_spin0.ref index 87ee81fb2e7..e359703fc9e 100644 --- a/tests/02_NAO_Gamma/scf_out_hxc/Vxc_R_spin0.ref +++ b/tests/02_NAO_Gamma/scf_out_hxc/Vxc_R_spin0.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of Vxc_R_spin0(R): 26 Matrix number of Vxc_R_spin0(R): 1 0 0 0 132 diff --git a/tests/02_NAO_Gamma/scf_out_hxc/vhartree_out.ref b/tests/02_NAO_Gamma/scf_out_hxc/eband_hart.ref similarity index 100% rename from tests/02_NAO_Gamma/scf_out_hxc/vhartree_out.ref rename to tests/02_NAO_Gamma/scf_out_hxc/eband_hart.ref diff --git a/tests/02_NAO_Gamma/scf_out_hxc/kinetic_out.ref b/tests/02_NAO_Gamma/scf_out_hxc/eband_kin.ref similarity index 100% rename from tests/02_NAO_Gamma/scf_out_hxc/kinetic_out.ref rename to tests/02_NAO_Gamma/scf_out_hxc/eband_kin.ref diff --git a/tests/02_NAO_Gamma/scf_out_hxc/vpp_local_out.ref b/tests/02_NAO_Gamma/scf_out_hxc/eband_vl.ref similarity index 100% rename from tests/02_NAO_Gamma/scf_out_hxc/vpp_local_out.ref rename to tests/02_NAO_Gamma/scf_out_hxc/eband_vl.ref diff --git a/tests/02_NAO_Gamma/scf_out_hxc/vpp_nonlocal_out.ref b/tests/02_NAO_Gamma/scf_out_hxc/eband_vnl.ref similarity index 100% rename from tests/02_NAO_Gamma/scf_out_hxc/vpp_nonlocal_out.ref rename to tests/02_NAO_Gamma/scf_out_hxc/eband_vnl.ref diff --git a/tests/02_NAO_Gamma/scf_out_hxc/vxc.ref b/tests/02_NAO_Gamma/scf_out_hxc/eband_vxc.ref similarity index 100% rename from tests/02_NAO_Gamma/scf_out_hxc/vxc.ref rename to tests/02_NAO_Gamma/scf_out_hxc/eband_vxc.ref diff --git a/tests/03_NAO_multik/get_s/dsrxs1.csr.ref b/tests/03_NAO_multik/get_s/dsrxs1.csr.ref index 304c2fc81a8..aaec88d6f49 100644 --- a/tests/03_NAO_multik/get_s/dsrxs1.csr.ref +++ b/tests/03_NAO_multik/get_s/dsrxs1.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of dHx(R): 112 Matrix number of dHx(R): 3 -1 0 0 268 diff --git a/tests/03_NAO_multik/get_s/dsrys1.csr.ref b/tests/03_NAO_multik/get_s/dsrys1.csr.ref index e596e253c0a..dfdfb209dce 100644 --- a/tests/03_NAO_multik/get_s/dsrys1.csr.ref +++ b/tests/03_NAO_multik/get_s/dsrys1.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of dHy(R): 112 Matrix number of dHy(R): 3 -1 0 0 192 diff --git a/tests/03_NAO_multik/get_s/dsrzs1.csr.ref b/tests/03_NAO_multik/get_s/dsrzs1.csr.ref index 8b4a094f573..16c5b952134 100644 --- a/tests/03_NAO_multik/get_s/dsrzs1.csr.ref +++ b/tests/03_NAO_multik/get_s/dsrzs1.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of dHz(R): 112 Matrix number of dHz(R): 3 -1 0 0 192 diff --git a/tests/03_NAO_multik/get_s/sr_nao.csr.ref b/tests/03_NAO_multik/get_s/sr_nao.csr.ref index 00c283dd399..3c6902b76a6 100644 --- a/tests/03_NAO_multik/get_s/sr_nao.csr.ref +++ b/tests/03_NAO_multik/get_s/sr_nao.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of S(R): 112 Matrix number of S(R): 3 -1 0 0 268 diff --git a/tests/03_NAO_multik/nscf_out_band_pband/refOrbital b/tests/03_NAO_multik/nscf_out_band_pband/refNaoTxt similarity index 100% rename from tests/03_NAO_multik/nscf_out_band_pband/refOrbital rename to tests/03_NAO_multik/nscf_out_band_pband/refNaoTxt diff --git a/tests/03_NAO_multik/nscf_out_hsr_tr_rr/rr_nao.txt.ref b/tests/03_NAO_multik/nscf_out_hsr_tr_rr/rr_nao.txt.ref index db8fe0b5461..42b80ea265e 100644 --- a/tests/03_NAO_multik/nscf_out_hsr_tr_rr/rr_nao.txt.ref +++ b/tests/03_NAO_multik/nscf_out_hsr_tr_rr/rr_nao.txt.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of r(R): 6 Matrix number of r(R): 9 -1 0 0 diff --git a/tests/03_NAO_multik/nscf_out_hsr_tr_rr/tr_nao.csr.ref b/tests/03_NAO_multik/nscf_out_hsr_tr_rr/tr_nao.csr.ref index 00e95fe978b..bb0639f3d07 100644 --- a/tests/03_NAO_multik/nscf_out_hsr_tr_rr/tr_nao.csr.ref +++ b/tests/03_NAO_multik/nscf_out_hsr_tr_rr/tr_nao.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of T(R): 6 Matrix number of T(R): 9 -1 0 0 15 diff --git a/tests/03_NAO_multik/scf_out_dh_t/dhrxs1_nao.csr.ref b/tests/03_NAO_multik/scf_out_dh_t/dhrxs1_nao.csr.ref index 1a624c49e90..90eb4f45d82 100644 --- a/tests/03_NAO_multik/scf_out_dh_t/dhrxs1_nao.csr.ref +++ b/tests/03_NAO_multik/scf_out_dh_t/dhrxs1_nao.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of dHx(R): 13 Matrix number of dHx(R): 1 0 0 0 24 diff --git a/tests/03_NAO_multik/scf_out_dh_t/dhrys1_nao.csr.ref b/tests/03_NAO_multik/scf_out_dh_t/dhrys1_nao.csr.ref index 1dc059da783..a5286bde5f3 100644 --- a/tests/03_NAO_multik/scf_out_dh_t/dhrys1_nao.csr.ref +++ b/tests/03_NAO_multik/scf_out_dh_t/dhrys1_nao.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of dHy(R): 13 Matrix number of dHy(R): 1 0 0 0 24 diff --git a/tests/03_NAO_multik/scf_out_dh_t/dhrzs1_nao.csr.ref b/tests/03_NAO_multik/scf_out_dh_t/dhrzs1_nao.csr.ref index ccfe6e99635..8c1fa290e1e 100644 --- a/tests/03_NAO_multik/scf_out_dh_t/dhrzs1_nao.csr.ref +++ b/tests/03_NAO_multik/scf_out_dh_t/dhrzs1_nao.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of dHz(R): 13 Matrix number of dHz(R): 1 0 0 0 20 diff --git a/tests/03_NAO_multik/scf_out_dh_t/tr_nao.csr.ref b/tests/03_NAO_multik/scf_out_dh_t/tr_nao.csr.ref index 1c241cae74c..52a639bfda5 100644 --- a/tests/03_NAO_multik/scf_out_dh_t/tr_nao.csr.ref +++ b/tests/03_NAO_multik/scf_out_dh_t/tr_nao.csr.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of T(R): 13 Matrix number of T(R): 1 0 0 0 21 diff --git a/tests/03_NAO_multik/scf_out_hsr_spin4/rr_nao.txt.ref b/tests/03_NAO_multik/scf_out_hsr_spin4/rr_nao.txt.ref index e735fc09448..26635300247 100644 --- a/tests/03_NAO_multik/scf_out_hsr_spin4/rr_nao.txt.ref +++ b/tests/03_NAO_multik/scf_out_hsr_spin4/rr_nao.txt.ref @@ -1,4 +1,4 @@ -STEP: 0 +STEP: 1 Matrix Dimension of r(R): 26 Matrix number of r(R): 7 -1 0 0 diff --git a/tests/04_FF/14_LJ_rescale_v/INPUT b/tests/04_FF/14_LJ_rescale_v/INPUT index 5915109bea5..3ee45712a15 100644 --- a/tests/04_FF/14_LJ_rescale_v/INPUT +++ b/tests/04_FF/14_LJ_rescale_v/INPUT @@ -1,6 +1,7 @@ INPUT_PARAMETERS #Parameters (General) suffix autotest +read_file_dir ./ calculation md pseudo_dir ../../PP_ORB diff --git a/tests/04_FF/14_LJ_rescale_v/OUT.autotest/Restart_md.txt b/tests/04_FF/14_LJ_rescale_v/Restart_md.txt similarity index 100% rename from tests/04_FF/14_LJ_rescale_v/OUT.autotest/Restart_md.txt rename to tests/04_FF/14_LJ_rescale_v/Restart_md.txt diff --git a/tests/04_FF/14_LJ_rescale_v/OUT.autotest/STRU/STRU_MD_10 b/tests/04_FF/14_LJ_rescale_v/STRU_MD_10 similarity index 100% rename from tests/04_FF/14_LJ_rescale_v/OUT.autotest/STRU/STRU_MD_10 rename to tests/04_FF/14_LJ_rescale_v/STRU_MD_10 diff --git a/tests/08_RI/60_LIBRPA_RPA_READER_V1/README b/tests/08_RI/60_LIBRPA_RPA_READER_V1/README index e629f45d763..ed7c290b7f0 100644 --- a/tests/08_RI/60_LIBRPA_RPA_READER_V1/README +++ b/tests/08_RI/60_LIBRPA_RPA_READER_V1/README @@ -1,5 +1,5 @@ Check reader-v1 RPA producer output, including shrink LRI, Coulomb, and GW -preprocessor input (`KPT.info`, `vxc.txt`, and text LCAO wavefunctions), +preprocessor input (`KPT.info`, `eband_vxc.txt`, and text LCAO wavefunctions), for a compact two-atom H `1x1x2` solid. For symmetry-reduced grids, reader-v1 appends the actual full-to-SCF mapping diff --git a/tests/08_RI/60_LIBRPA_RPA_READER_V1/librpa_producer_manifest.json b/tests/08_RI/60_LIBRPA_RPA_READER_V1/librpa_producer_manifest.json index e876b34b675..064f73d5cdf 100644 --- a/tests/08_RI/60_LIBRPA_RPA_READER_V1/librpa_producer_manifest.json +++ b/tests/08_RI/60_LIBRPA_RPA_READER_V1/librpa_producer_manifest.json @@ -12,8 +12,8 @@ {"pattern": "V_full_*.dat", "kind": "coulomb_v1", "reference": true, "abs_tol": 1e-4}, {"pattern": "KS_wfc_0.dat", "kind": "ks_wfc_v1", "nao_dir": "OUT.autotest", "abs_tol": 1e-7}, {"pattern": "velocity_matrix.txt", "kind": "velocity"}, - {"base_dir": ".", "pattern": "OUT.autotest/vxc.txt", "kind": "text", "reference": true, "abs_tol": 1e-7}, - {"base_dir": ".", "pattern": "OUT.autotest/KPT.info", "kind": "text", "reference": true, "abs_tol": 1e-7}, + {"base_dir": "OUT.autotest", "pattern": "eband_vxc.txt", "kind": "text", "reference": true, "abs_tol": 1e-7}, + {"base_dir": "OUT.autotest", "pattern": "KPT.info", "kind": "text", "reference": true, "abs_tol": 1e-7}, {"base_dir": ".", "pattern": "OUT.autotest/wfk*_nao.txt", "kind": "wfc_nao", "expected_count": 2} ] } diff --git a/tests/08_RI/60_LIBRPA_RPA_READER_V1/producer_reference/OUT.autotest/KPT.info b/tests/08_RI/60_LIBRPA_RPA_READER_V1/producer_reference/KPT.info similarity index 100% rename from tests/08_RI/60_LIBRPA_RPA_READER_V1/producer_reference/OUT.autotest/KPT.info rename to tests/08_RI/60_LIBRPA_RPA_READER_V1/producer_reference/KPT.info diff --git a/tests/08_RI/60_LIBRPA_RPA_READER_V1/producer_reference/OUT.autotest/vxc.txt b/tests/08_RI/60_LIBRPA_RPA_READER_V1/producer_reference/eband_vxc.txt similarity index 100% rename from tests/08_RI/60_LIBRPA_RPA_READER_V1/producer_reference/OUT.autotest/vxc.txt rename to tests/08_RI/60_LIBRPA_RPA_READER_V1/producer_reference/eband_vxc.txt diff --git a/tests/08_RI/64_LIBRPA_NSPIN2/librpa_producer_manifest.json b/tests/08_RI/64_LIBRPA_NSPIN2/librpa_producer_manifest.json index 3983bb8146f..eb2c1a366e7 100644 --- a/tests/08_RI/64_LIBRPA_NSPIN2/librpa_producer_manifest.json +++ b/tests/08_RI/64_LIBRPA_NSPIN2/librpa_producer_manifest.json @@ -12,8 +12,8 @@ {"pattern": "V_full_*.dat", "kind": "coulomb_v1", "reference": true, "abs_tol": 1e-4}, {"pattern": "KS_wfc_0.dat", "kind": "ks_wfc_v1", "nao_dir": "OUT.autotest", "abs_tol": 1e-7}, {"pattern": "velocity_matrix.txt", "kind": "velocity"}, - {"base_dir": ".", "pattern": "OUT.autotest/vxc.txt", "kind": "text", "reference": true, "abs_tol": 1e-7}, - {"base_dir": ".", "pattern": "OUT.autotest/KPT.info", "kind": "text", "reference": true, "abs_tol": 1e-7}, + {"base_dir": "OUT.autotest", "pattern": "eband_vxc.txt", "kind": "text", "reference": true, "abs_tol": 1e-7}, + {"base_dir": "OUT.autotest", "pattern": "KPT.info", "kind": "text", "reference": true, "abs_tol": 1e-7}, {"base_dir": ".", "pattern": "OUT.autotest/wfk*s*_nao.txt", "kind": "wfc_nao", "expected_count": 4} ] } diff --git a/tests/08_RI/64_LIBRPA_NSPIN2/producer_reference/OUT.autotest/KPT.info b/tests/08_RI/64_LIBRPA_NSPIN2/producer_reference/KPT.info similarity index 100% rename from tests/08_RI/64_LIBRPA_NSPIN2/producer_reference/OUT.autotest/KPT.info rename to tests/08_RI/64_LIBRPA_NSPIN2/producer_reference/KPT.info diff --git a/tests/08_RI/64_LIBRPA_NSPIN2/producer_reference/OUT.autotest/vxc.txt b/tests/08_RI/64_LIBRPA_NSPIN2/producer_reference/eband_vxc.txt similarity index 100% rename from tests/08_RI/64_LIBRPA_NSPIN2/producer_reference/OUT.autotest/vxc.txt rename to tests/08_RI/64_LIBRPA_NSPIN2/producer_reference/eband_vxc.txt diff --git a/tests/08_RI/scf_out_xc_multik/INPUT b/tests/08_RI/scf_out_xc_multik/INPUT index 90aec1200f6..e4a05ccaa73 100644 --- a/tests/08_RI/scf_out_xc_multik/INPUT +++ b/tests/08_RI/scf_out_xc_multik/INPUT @@ -26,7 +26,7 @@ mixing_type broyden mixing_beta 0.7 out_mat_xc 1 -out_eband_terms 1 +out_eband 1 ks_solver scalapack_gvx dft_functional hse diff --git a/tests/08_RI/scf_out_xc_multik/README b/tests/08_RI/scf_out_xc_multik/README index f11eb37f242..2810c86200c 100644 --- a/tests/08_RI/scf_out_xc_multik/README +++ b/tests/08_RI/scf_out_xc_multik/README @@ -1 +1 @@ -scf HSE on H2O, multik, out_mat_xc=1, out_eband_terms=1. +scf HSE on H2O, multik, out_mat_xc=1, out_eband=1. diff --git a/tests/08_RI/scf_out_xc_multik/vhartree_out.ref b/tests/08_RI/scf_out_xc_multik/eband_hart.ref similarity index 100% rename from tests/08_RI/scf_out_xc_multik/vhartree_out.ref rename to tests/08_RI/scf_out_xc_multik/eband_hart.ref diff --git a/tests/08_RI/scf_out_xc_multik/kinetic_out.ref b/tests/08_RI/scf_out_xc_multik/eband_kin.ref similarity index 100% rename from tests/08_RI/scf_out_xc_multik/kinetic_out.ref rename to tests/08_RI/scf_out_xc_multik/eband_kin.ref diff --git a/tests/08_RI/scf_out_xc_multik/vpp_local_out.ref b/tests/08_RI/scf_out_xc_multik/eband_vl.ref similarity index 100% rename from tests/08_RI/scf_out_xc_multik/vpp_local_out.ref rename to tests/08_RI/scf_out_xc_multik/eband_vl.ref diff --git a/tests/08_RI/scf_out_xc_multik/vpp_nonlocal_out.ref b/tests/08_RI/scf_out_xc_multik/eband_vnl.ref similarity index 100% rename from tests/08_RI/scf_out_xc_multik/vpp_nonlocal_out.ref rename to tests/08_RI/scf_out_xc_multik/eband_vnl.ref diff --git a/tests/08_RI/scf_out_xc_multik/vxc.ref b/tests/08_RI/scf_out_xc_multik/eband_vxc.ref similarity index 100% rename from tests/08_RI/scf_out_xc_multik/vxc.ref rename to tests/08_RI/scf_out_xc_multik/eband_vxc.ref diff --git a/tests/17_DS_DFTU/01_LCAO_SPIN_S2_Z/INPUT b/tests/17_DS_DFTU/01_LCAO_SPIN_S2_Z/INPUT index 7b498cf0ca4..c84b53c057d 100644 --- a/tests/17_DS_DFTU/01_LCAO_SPIN_S2_Z/INPUT +++ b/tests/17_DS_DFTU/01_LCAO_SPIN_S2_Z/INPUT @@ -7,7 +7,7 @@ gamma_only 0 nspin 2 #nbands 28 -scf_thr 1.0e-6 +scf_thr 1.0e-8 scf_nmax 50 out_chg 0 smearing_method gaussian diff --git a/tests/17_DS_DFTU/01_LCAO_SPIN_S2_Z/result.ref b/tests/17_DS_DFTU/01_LCAO_SPIN_S2_Z/result.ref index 83d1b45cfc4..e7d6685fe7b 100644 --- a/tests/17_DS_DFTU/01_LCAO_SPIN_S2_Z/result.ref +++ b/tests/17_DS_DFTU/01_LCAO_SPIN_S2_Z/result.ref @@ -1,3 +1,3 @@ -etotref -6787.961875326573 -etotperatomref -3393.9809376640 -totaltimeref 3.93 +etotref -6787.961891459786 +etotperatomref -3393.9809457299 +totaltimeref 6.57 diff --git a/tests/17_DS_DFTU/03_LCAO_DFTU_S2_Z/INPUT b/tests/17_DS_DFTU/03_LCAO_DFTU_S2_Z/INPUT index edd5eda7ef8..fd0dd957fbf 100644 --- a/tests/17_DS_DFTU/03_LCAO_DFTU_S2_Z/INPUT +++ b/tests/17_DS_DFTU/03_LCAO_DFTU_S2_Z/INPUT @@ -7,7 +7,7 @@ gamma_only 0 nspin 2 #nbands 28 -scf_thr 1.0e-6 +scf_thr 1.0e-8 scf_nmax 50 out_chg 0 smearing_method gaussian diff --git a/tests/17_DS_DFTU/03_LCAO_DFTU_S2_Z/result.ref b/tests/17_DS_DFTU/03_LCAO_DFTU_S2_Z/result.ref index 2c12a092ecd..fe09f1b1239 100644 --- a/tests/17_DS_DFTU/03_LCAO_DFTU_S2_Z/result.ref +++ b/tests/17_DS_DFTU/03_LCAO_DFTU_S2_Z/result.ref @@ -1,6 +1,6 @@ -etotref -6771.6902262248995612 -etotperatomref -3385.8451131124 +etotref -6771.690330397053 +etotperatomref -3385.8451651985 pointgroupref C_3v spacegroupref C_3v nksibzref 2 -totaltimeref 6.11 +totaltimeref 11.37 diff --git a/tests/17_DS_DFTU/CASES_CPU.txt b/tests/17_DS_DFTU/CASES_CPU.txt index 1b66080746d..892a9f68ddd 100644 --- a/tests/17_DS_DFTU/CASES_CPU.txt +++ b/tests/17_DS_DFTU/CASES_CPU.txt @@ -13,90 +13,3 @@ 03_LCAO_DFTU_S2_Z # 04_LCAO_DFTU_S4_XY # DISABLED: Convergence / numerical stability issue with LCAO noncollinear DFT+U # 05_LCAO_DFTU_S4_XYZ # DISABLED: Convergence / numerical stability issue with LCAO noncollinear DFT+U - -# ======================== -# PW baseline tests -# ======================== -06_PW_SPIN_S2_Z -07_PW_SPIN_S4_XYZ -# 08_PW_DFTU_S2_Z # MIGRATED to 01_PW/815_PW_DFTU_S2_Z -# 09_PW_DFTU_S4_XY # MIGRATED to 01_PW/816_PW_DFTU_S4_XY -11_PW_DFTU_S2_FeO - -# ======================== -# PW pure DeltaSpin tests -# ======================== -12_PW_DS_S2_Z -14_PW_DS_S4_XYZ -15_PW_DS_S4_Z -16_PW_DS_S4_XY - -# ======================== -# PW DFT+U + DeltaSpin tests -# ======================== -18_PW_DFTU_DS_S2_Z -19_PW_DFTU_DS_S4_XY -21_PW_DFTU_DS_S4_Z - -# ======================== -# LCAO pure DeltaSpin tests -# ======================== -# 24_LCAO_DS_S2_Z # DISABLED: Convergence / numerical stability issue with LCAO DeltaSpin -# 26_LCAO_DS_S4_XYZ # DISABLED: Convergence / numerical stability issue with LCAO noncollinear DeltaSpin -# 27_LCAO_DS_S4_Z # DISABLED: Convergence / numerical stability issue with LCAO noncollinear DeltaSpin -# 28_LCAO_DS_S4_XY # DISABLED: Convergence / numerical stability issue with LCAO noncollinear DeltaSpin - -# ======================== -# LCAO DFT+U + DeltaSpin tests -# ======================== -# 30_LCAO_DFTU_DS_S2_Z # DISABLED: Convergence / numerical stability issue with LCAO DFT+U+DeltaSpin -# 31_LCAO_DFTU_DS_S4_XY # DISABLED: Convergence / numerical stability issue with LCAO DFT+U+DeltaSpin -# 32_LCAO_DFTU_DS_S4_XYZ # DISABLED: Convergence / numerical stability issue with LCAO DFT+U+DeltaSpin -# 33_LCAO_DFTU_DS_S4_Z # DISABLED: Convergence / numerical stability issue with LCAO DFT+U+DeltaSpin - -# ======================== -# ReadLam mode (nsc=1, load pre-computed lambda) -# ======================== -36_PW_DS_S2_ReadLam_Z -37_PW_DS_S4_ReadLam_XY - -# ======================== -# Lambda loop behavior: thr1e-10 = no lambda loop, load from STRU -# ======================== -38_PW_DS_S2_Thr1e10_Z -39_PW_DS_S4_Thr1e10_XY -42_PW_DFTU_DS_S2_Thr1e10_Z -43_PW_DFTU_DS_S4_Thr1e10_XY - -# ======================== -# Lambda loop behavior: thr10 = immediate lambda loop -# ======================== -40_PW_DS_S2_Thr10_Z -41_PW_DS_S4_Thr10_XY -# 44_PW_DFTU_DS_S2_Thr10_Z # DISABLED: Convergence / numerical stability issue with loose threshold - -# ======================== -# FeO multi-element DFT+U -# ======================== -50_FeO_O_first_Fe_second -51_FeO_Fe_first_O_second - -# ======================== -# NSCF (non-self-consistent) tests -# ======================== -# DISABLED: These tests use SCF+NSCF workflow (run_scf_nscf.sh) and require running SCF first. -# Run manually with: bash run_scf_nscf.sh [mpi_np] -# 55_PW_DS_NSCF_S4_XY # DISABLED: SCF+NSCF workflow test, run manually -# 60_PW_DFTU_DS_NSCF_Band_XY # DISABLED: SCF+NSCF workflow test, run manually -# 61_LCAO_DS_NSCF_S4_XY # DISABLED: SCF+NSCF workflow test, run manually -# 62_LCAO_DFTU_NSCF_Band_XY # DISABLED: SCF+NSCF workflow + convergence issue (genelpa eigenvalue inconsistency) -# 63_LCAO_DFTU_DS_NSCF_Band_XY # DISABLED: SCF+NSCF workflow test, run manually -# 64_PW_DFTU_NSCF_Band_XY # DISABLED: SCF+NSCF workflow test, run manually - -# ======================== -# DirectionOnly mode (constrain direction, not magnitude) -# ======================== -# 56_PW_DS_S4_DirectionOnly_XY # DISABLED: Sensitive to SCF convergence with MPI/OMP variations -# 57_PW_DFTU_DS_S4_DirectionOnly_XY # DISABLED: Sensitive to SCF convergence with MPI/OMP variations -# 58_LCAO_DS_S4_DirectionOnly_XY # DISABLED: Convergence / numerical stability issue with LCAO DirectionOnly -# 59_LCAO_DFTU_DS_S4_DirectionOnly_XY # DISABLED: Convergence / numerical stability issue with LCAO DirectionOnly diff --git a/tests/integrate/validation_tools/check_librpa_producer.py b/tests/integrate/validation_tools/check_librpa_producer.py index ee8a47e6e98..3396870cdf9 100644 --- a/tests/integrate/validation_tools/check_librpa_producer.py +++ b/tests/integrate/validation_tools/check_librpa_producer.py @@ -63,8 +63,9 @@ def _numeric_tokens(path): def _compare_text(actual, reference, tolerance): - actual_lines = actual.read_text().splitlines() - reference_lines = reference.read_text().splitlines() + # ignore provenance comment lines starting with '#' on both sides + actual_lines = [line for line in actual.read_text().splitlines() if not line.lstrip().startswith("#")] + reference_lines = [line for line in reference.read_text().splitlines() if not line.lstrip().startswith("#")] # Some ABACUS text writers leave an extra blank record at EOF. It has no # parser-visible meaning, so keep the value comparison strict while # ignoring only trailing empty records. @@ -153,7 +154,8 @@ def _check_stru(path, minimum_symmetry_rows, spin_symmetry_rows=None): raise ProducerContractError(str(error)) def _check_wfc_nao(path): """Validate the text LCAO wavefunction format consumed by GW preprocessing.""" - lines = [line.strip() for line in path.read_text().splitlines() if line.strip()] + # ignore provenance comment lines starting with '#' + lines = [line.strip() for line in path.read_text().splitlines() if line.strip() and not line.strip().startswith("#")] if len(lines) < 6: raise ProducerContractError("{} is too short for an LCAO wavefunction".format(path)) diff --git a/tests/integrate/validation_tools/compare_hsr_binary.py b/tests/integrate/validation_tools/compare_hsr_binary.py index 2e3e295ed6e..4caea802bee 100644 --- a/tests/integrate/validation_tools/compare_hsr_binary.py +++ b/tests/integrate/validation_tools/compare_hsr_binary.py @@ -17,11 +17,34 @@ def read_native(stream, fmt, description): return struct.unpack(fmt, read_exact(stream, size, description)) +HSR_BINARY_MAGIC = 0x48535242 +HSR_BINARY_FORMAT_VERSION = 2 + + def read_binary(filename, value_type): with open(filename, "rb") as stream: - step, nbasis, nr = read_native(stream, "@iii", "record header") - if step != 0: - raise ValueError("expected zero-based ionic step 0, found {}".format(step)) + # The record header is versioned: magic 'HSRB' and the layout version + # precede the ionic step, so unversioned files from earlier builds + # (no magic, small first int) are rejected instead of silently + # misreading the step field. + magic = read_native(stream, "@i", "record header magic")[0] + if magic != HSR_BINARY_MAGIC: + raise ValueError( + "missing native binary H(R)/S(R) magic 0x{:X} (found 0x{:X}); " + "the file was written by an unversioned ABACUS build".format( + HSR_BINARY_MAGIC, magic + ) + ) + version, step, nbasis, nr = read_native(stream, "@iiii", "record header") + if version != HSR_BINARY_FORMAT_VERSION: + raise ValueError( + "unsupported native binary H(R)/S(R) format version {} " + "(expected {})".format(version, HSR_BINARY_FORMAT_VERSION) + ) + # The binary step field follows governance rule 22: the first ionic + # step is written one-based as 1. + if step != 1: + raise ValueError("expected one-based ionic step 1, found {}".format(step)) if nbasis <= 0 or nr < 0: raise ValueError("invalid matrix dimensions nbasis={} nr={}".format(nbasis, nr)) diff --git a/tests/integrate/validation_tools/cube_tool.py b/tests/integrate/validation_tools/cube_tool.py index 52ea0e64ac9..a390ea62dbb 100755 --- a/tests/integrate/validation_tools/cube_tool.py +++ b/tests/integrate/validation_tools/cube_tool.py @@ -9,6 +9,11 @@ density, partial charge, wavefunction modulus, potential, and other scalar cube outputs. The command prints one number: ``sum(values) * voxel_volume``. +``integrate-last CUBE`` + Integrate the final complete ABACUS scalar cube frame, using its own grid. + An incomplete or malformed trailing frame is an error. Used by the solvent + relaxation metric; other consumers keep their existing frame selection. + ``fingerprint-wfc RE IM [RE IM ...]`` Generate a compact, deterministic fingerprint of one complex wavefunction state. One real/imaginary pair represents a scalar component; two pairs in @@ -107,6 +112,77 @@ def integrate_cube(path): return sum(values) * volume +def read_last_cube(path): + """Read the final complete ABACUS scalar frame; reject an incomplete tail.""" + input_path = Path(path) + last_frame = None + with input_path.open("r", encoding="utf-8") as stream: + while True: + comment = stream.readline() + if not comment: + break + if not comment.strip(): + continue + if not stream.readline(): + raise ValueError(f"incomplete cube comment in {input_path}") + atom_line = stream.readline().split() + if len(atom_line) != 4: + raise ValueError(f"invalid scalar atom header in {input_path}") + natom = int(atom_line[0]) + if natom < 0: + raise ValueError(f"integrate-last requires a scalar cube in {input_path}") + origin = [float(value) for value in atom_line[1:]] + shape = [] + vectors = [] + for _ in range(3): + grid_line = stream.readline().split() + if len(grid_line) != 4: + raise ValueError(f"invalid grid header in {input_path}") + size = int(grid_line[0]) + if size <= 0: + raise ValueError(f"invalid grid size in {input_path}") + shape.append(size) + vectors.append([float(value) for value in grid_line[1:]]) + for _ in range(natom): + atom = stream.readline().split() + if len(atom) != 5: + raise ValueError(f"incomplete atom record in {input_path}") + int(atom[0]) + coordinates = [float(value) for value in atom[1:]] + if not all(math.isfinite(value) for value in coordinates): + raise ValueError(f"non-finite atom record in {input_path}") + metadata = origin + [value for vector in vectors for value in vector] + if not all(math.isfinite(value) for value in metadata): + raise ValueError(f"non-finite cube header in {input_path}") + v1 = vectors[0] + v2 = vectors[1] + v3 = vectors[2] + val0 = v2[1] * v3[2] - v2[2] * v3[1] + val1 = v2[0] * v3[2] - v2[2] * v3[0] + val2 = v2[0] * v3[1] - v2[1] * v3[0] + volume = abs(v1[0] * val0 - v1[1] * val1 + v1[2] * val2) + if volume <= 0.0 or not math.isfinite(volume): + raise ValueError(f"invalid voxel volume in {input_path}") + count = shape[0] * shape[1] * shape[2] + values = [] + while len(values) < count: + line = stream.readline() + if not line: + raise ValueError(f"incomplete cube data in {input_path}") + values.extend(float(value) for value in line.split()) + if len(values) != count or not all(math.isfinite(value) for value in values): + raise ValueError(f"invalid scalar cube data in {input_path}") + last_frame = (tuple(shape), volume, values) + if last_frame is None: + raise ValueError(f"no complete cube frame in {input_path}") + return last_frame + + +def integrate_last_cube(path): + _, volume, values = read_last_cube(path) + return sum(values) * volume + + def splitmix64(value): value = (value + SPLITMIX_GOLDEN) & UINT64_MASK value = ((value ^ (value >> 30)) * SPLITMIX_MIX1) & UINT64_MASK @@ -312,6 +388,10 @@ def run_integrate(args): print(f"{integrate_cube(args.cube):.10g}") +def run_integrate_last(args): + print(f"{integrate_last_cube(args.cube):.10g}") + + def run_fingerprint_wfc(args): for name, value in wfc_fingerprint(args.cube_paths): print(f"{name} {format_metric(value)}") @@ -333,6 +413,12 @@ def build_parser(): integrate_parser.add_argument("cube", type=Path) integrate_parser.set_defaults(handler=run_integrate) + last_parser = subparsers.add_parser( + "integrate-last", help="integrate the final complete ABACUS scalar cube frame" + ) + last_parser.add_argument("cube", type=Path) + last_parser.set_defaults(handler=run_integrate_last) + fingerprint_parser = subparsers.add_parser( "fingerprint-wfc", help="generate a deterministic global-phase-invariant wavefunction fingerprint", diff --git a/tests/integrate/validation_tools/librpa_wavefunctions.py b/tests/integrate/validation_tools/librpa_wavefunctions.py index 15303a35ef3..f643b0d647d 100644 --- a/tests/integrate/validation_tools/librpa_wavefunctions.py +++ b/tests/integrate/validation_tools/librpa_wavefunctions.py @@ -41,7 +41,8 @@ def check_ks_nao(path, nao_dir, tolerance, spinor=False): else: name = "wfk{}_nao.txt".format(ik) if nspin == 1 else "wfk{}s{}_nao.txt".format(ik, spin) source = nao_dir / name - lines = [line.strip() for line in source.read_text().splitlines() if line.strip()] + # ignore provenance comment lines starting with '#' + lines = [line.strip() for line in source.read_text().splitlines() if line.strip() and not line.strip().startswith("#")] if len(lines) < 4 or "(index of k points)" not in lines[0]: raise ValueError("{} has an invalid text KS header".format(source)) text_index = ik if spinor else ik + (spin - 1) * nk diff --git a/tests/integrate/validation_tools/props_basic.sh b/tests/integrate/validation_tools/props_basic.sh index 04311083287..b1e8fe906ed 100644 --- a/tests/integrate/validation_tools/props_basic.sh +++ b/tests/integrate/validation_tools/props_basic.sh @@ -112,7 +112,8 @@ if [[ "$out_sol" == "1" ]]; then exit 1 fi done - integral=$(python3 "$CUBE_TOOL" integrate "OUT.autotest/sol_cavity.cube") + # Append mode retains earlier geometries; compare the final complete frame. + integral=$(python3 "$CUBE_TOOL" integrate-last "OUT.autotest/sol_cavity.cube") || return 1 echo "sol_cavity_integralref $integral" >>$props_result_file fi diff --git a/tests/integrate/validation_tools/props_common.sh b/tests/integrate/validation_tools/props_common.sh index 983d2f1b6d1..672935c0b71 100644 --- a/tests/integrate/validation_tools/props_common.sh +++ b/tests/integrate/validation_tools/props_common.sh @@ -118,7 +118,7 @@ props_init(){ fi has_xc=$(get_input_key_value "out_mat_xc" "INPUT") has_xc2=$(get_input_key_value "out_mat_xc2" "INPUT") - has_eband_separate=$(get_input_key_value "out_eband_terms" "INPUT") + has_eband_separate=$(get_input_key_value "out_eband" "INPUT") has_lowf=$(get_input_key_value "out_wfc_lcao" "INPUT") out_app_flag=$(get_input_key_value "out_app_flag" "INPUT") has_wfc_r=$(get_input_key_value "out_wfc_r" "INPUT") diff --git a/tests/integrate/validation_tools/props_cube.sh b/tests/integrate/validation_tools/props_cube.sh index 68134aa6ffb..ff7596331fa 100644 --- a/tests/integrate/validation_tools/props_cube.sh +++ b/tests/integrate/validation_tools/props_cube.sh @@ -148,16 +148,25 @@ fi # echo "$has_lowf" # test out_wfc_lcao > 0 #-------------------------------------------- if ! test -z "$has_lowf" && [ "$has_lowf" == 1 ]; then + wfc_prep_ok=1 if ! test -z "$gamma_only" && [ $gamma_only == 1 ]; then wfc_cal=OUT.autotest/wf_nao.txt wfc_ref=wf_nao.txt.ref else # multi-k point case if ! test -z "$out_app_flag" && [ $out_app_flag == 0 ]; then - wfc_name=wfk1g3_nao - input_file=WFC/wfk1g3_nao + # mode 0 overwrites one fixed file at every write, so the file holds + # the last written step; the reference below records that step (3). + wfc_name=wfk1_nao + input_file=wfk1_nao + wfc_ref_name=wfk1g3_nao else wfc_name=wfk2_nao input_file=wfk2_nao + wfc_ref_name=wfk2_nao + fi + if [[ ! -f "OUT.autotest/$input_file.txt" ]]; then + echo "Can't find file OUT.autotest/$input_file.txt" + exit 1 fi awk 'BEGIN {flag=999} { @@ -171,12 +180,39 @@ if ! test -z "$has_lowf" && [ "$has_lowf" == 1 ]; then } else {print $0} }' OUT.autotest/"$input_file".txt > OUT.autotest/"$wfc_name"_mod.txt + if [ $? -ne 0 ]; then + echo "Error: failed to preprocess OUT.autotest/$input_file.txt" + wfc_prep_ok=0 + fi wfc_cal=OUT.autotest/"$wfc_name"_mod.txt - wfc_ref="$wfc_name"_mod.txt.ref + wfc_ref="$wfc_ref_name"_mod.txt.ref fi - python3 $COMPARE_SCRIPT $wfc_cal $wfc_ref 8 -abs 1 - echo "Compare_wfc_lcao_pass $?" >>$props_result_file + # wfc text output carries a provenance header ("# ABACUS version:", + # "# Written at ...", "# IONIC STEP ...") since v3.11; the multi-k awk + # pass also forwards those comment lines into the *_mod.txt file. + # Strip comment lines from both sides so the (run-dependent) header and + # its timestamp never affect the comparison. + local wfc_cal_nohdr + local wfc_ref_nohdr + wfc_cal_nohdr=$(mktemp) + wfc_ref_nohdr=$(mktemp) + # A missing source file makes sed leave a truncated temporary file, and + # CompareFile.py would then compare a nonempty reference against nothing. + # Detect that preprocessing failure here instead of trusting the + # downstream comparison. + if [ "$wfc_prep_ok" -ne 1 ]; then + wfc_pass=1 + elif ! sed '/^[[:space:]]*#/d' "$wfc_cal" >"$wfc_cal_nohdr" \ + || ! sed '/^[[:space:]]*#/d' "$wfc_ref" >"$wfc_ref_nohdr"; then + echo "Error: failed to strip comment lines from $wfc_cal or $wfc_ref" + wfc_pass=1 + else + python3 $COMPARE_SCRIPT "$wfc_cal_nohdr" "$wfc_ref_nohdr" 8 -abs 1 + wfc_pass=$? + fi + rm -f "$wfc_cal_nohdr" "$wfc_ref_nohdr" + echo "Compare_wfc_lcao_pass $wfc_pass" >>$props_result_file fi } diff --git a/tests/integrate/validation_tools/props_mat.sh b/tests/integrate/validation_tools/props_mat.sh index 5128c3dd379..06bcc32f65d 100644 --- a/tests/integrate/validation_tools/props_mat.sh +++ b/tests/integrate/validation_tools/props_mat.sh @@ -37,6 +37,20 @@ compare_hr_csr(){ python3 $COMPARE_SCRIPT <(sed -n "$pick" "$ref") <(sed -n "$pick" "$cal") 3 } +# Band-energy-term files start with comment lines (version, timestamp, ionic +# step, formula, units) that legitimately change between runs, so the header +# is stripped and only the numeric body is compared. +compare_eband_energy(){ + local ref=$1 + local cal=$2 + local accuracy=${3:-4} + if ! test -f "$ref" || ! test -f "$cal"; then + echo "Error: can not find file $ref or $cal" + return 1 + fi + python3 $COMPARE_SCRIPT <(sed '/^[[:space:]]*#/d' "$ref") <(sed '/^[[:space:]]*#/d' "$cal") "$accuracy" +} + run_mat_dm1_props(){ #------------------------------- @@ -69,8 +83,8 @@ fi # echo $out_pband #------------------------------- if ! test -z "$out_pband" && [ $out_pband == 1 ]; then - orbref=refOrbital - orbcal=OUT.autotest/Orbital + orbref=refNaoTxt + orbcal=OUT.autotest/nao.txt python3 $COMPARE_SCRIPT $orbref $orbcal 8 echo "CompareOrb_pass $?" >>$props_result_file fi @@ -106,8 +120,18 @@ fi if ! test -z "$has_band" && [ $has_band == 1 ]; then bandref=band.txt.ref bandcal=OUT.autotest/band.txt - python3 $COMPARE_SCRIPT $bandref $bandcal 8 - echo "CompareBand_pass $?" >>$props_result_file + # Every ionic step in band.txt is preceded by a provenance comment block + # ("# ABACUS version: ...", "# Written at ...", "# IONIC STEP n"), so the + # reference and the computed file are compared after those lines are + # stripped. A missing file must be reported explicitly, because process + # substitution would hand CompareFile.py an empty stream and pass silently. + if ! test -f "$bandref" || ! test -f "$bandcal"; then + echo "Error: can not find file $bandref or $bandcal" + echo "CompareBand_pass 1" >>$props_result_file + else + python3 $COMPARE_SCRIPT <(sed '/^[[:space:]]*#/d' "$bandref") <(sed '/^[[:space:]]*#/d' "$bandcal") 8 + echo "CompareBand_pass $?" >>$props_result_file + fi fi #-------------------------------- @@ -214,11 +238,11 @@ if ! test -z "$has_xc" && [ "$has_xc" == 1 ]; then xcref=vxck2_nao.txt.ref xccal=OUT.autotest/vxck2_nao.txt fi - oeref=vxc.ref - oecal=OUT.autotest/vxc.txt + oeref=eband_vxc.ref + oecal=OUT.autotest/eband_vxc.txt python3 $COMPARE_SCRIPT $xcref $xccal 4 echo "CompareVXC_pass $?" >>$props_result_file - python3 $COMPARE_SCRIPT $oeref $oecal 5 + compare_eband_energy $oeref $oecal 5 echo "CompareOrbXC_pass $?" >>$props_result_file fi @@ -236,21 +260,13 @@ fi # separate terms in band enegy #-------------------------------- if ! test -z "$has_eband_separate" && [ $has_eband_separate == 1 ]; then - ekref=kinetic_out.ref - ekcal=OUT.autotest/kinetic_out.dat - python3 $COMPARE_SCRIPT $ekref $ekcal 4 + compare_eband_energy eband_kin.ref OUT.autotest/eband_kin.txt echo "CompareOrbKinetic_pass $?" >>$props_result_file - vlref=vpp_local_out.ref - vlcal=OUT.autotest/vpp_local_out.dat - python3 $COMPARE_SCRIPT $vlref $vlcal 4 + compare_eband_energy eband_vl.ref OUT.autotest/eband_vl.txt echo "CompareOrbVL_pass $?" >>$props_result_file - vnlref=vpp_nonlocal_out.ref - vnlcal=OUT.autotest/vpp_nonlocal_out.dat - python3 $COMPARE_SCRIPT $vnlref $vnlcal 4 + compare_eband_energy eband_vnl.ref OUT.autotest/eband_vnl.txt echo "CompareOrbVNL_pass $?" >>$props_result_file - vhref=vhartree_out.ref - vhcal=OUT.autotest/vhartree_out.dat - python3 $COMPARE_SCRIPT $vhref $vhcal 4 + compare_eband_energy eband_hart.ref OUT.autotest/eband_hart.txt echo "CompareOrbVHartree_pass $?" >>$props_result_file fi diff --git a/tests/integrate/validation_tools/test_cube_tool.py b/tests/integrate/validation_tools/test_cube_tool.py new file mode 100644 index 00000000000..164efbfefd8 --- /dev/null +++ b/tests/integrate/validation_tools/test_cube_tool.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Final-frame solvent metrics preserve existing first-frame cube consumers.""" +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +import cube_tool + + +def scalar_frame(values, step, voxel_x): + return (f"Ionic_Step {step}\nscalar field\n1 0 0 0\n" + f"2 {voxel_x} 0 0\n1 0 1 0\n1 0 0 1\n" + f"1 1 0 0 0\n{values}\n") + + +class CubeToolLastFrameTest(unittest.TestCase): + def setUp(self): + self.scratch = tempfile.TemporaryDirectory() + self.addCleanup(self.scratch.cleanup) + self.path = Path(self.scratch.name) / "sol_cavity.cube" + self.first = scalar_frame("1 2", 1, 1) + self.last = scalar_frame("4 5", 2, 2) + + def write_frames(self, text): + self.path.write_text(text, encoding="utf-8") + + def run_command(self, command): + tool = Path(cube_tool.__file__).resolve() + return subprocess.run([sys.executable, str(tool), command, str(self.path)], + capture_output=True, text=True, check=False) + + def test_single_frame_keeps_existing_integral(self): + self.write_frames(self.first) + self.assertEqual(cube_tool.integrate_cube(self.path), 3.0) + self.assertEqual(cube_tool.integrate_last_cube(self.path), 3.0) + + def test_last_frame_uses_its_own_values_and_grid(self): + self.write_frames(self.first + self.last) + self.assertEqual(cube_tool.integrate_last_cube(self.path), 18.0) + self.assertEqual(cube_tool.integrate_cube(self.path), 3.0) + + def test_trailing_whitespace_is_accepted(self): + self.write_frames(self.first + self.last + "\n \n") + self.assertEqual(cube_tool.integrate_last_cube(self.path), 18.0) + + def test_cli_selects_last_only_when_requested(self): + self.write_frames(self.first + self.last) + old = self.run_command("integrate") + final = self.run_command("integrate-last") + self.assertEqual(old.returncode, 0, old.stderr) + self.assertEqual(final.returncode, 0, final.stderr) + self.assertEqual(old.stdout.strip(), "3") + self.assertEqual(final.stdout.strip(), "18") + + def test_truncated_tail_does_not_fall_back_to_first_frame(self): + self.write_frames(self.first + scalar_frame("4", 2, 2)) + with self.assertRaises(ValueError): + cube_tool.integrate_last_cube(self.path) + result = self.run_command("integrate-last") + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, "") + + def test_malformed_tail_is_rejected(self): + for tail in (scalar_frame("bad 5", 2, 2), scalar_frame("nan 5", 2, 2), + scalar_frame("4 5 6", 2, 2), "Ionic_Step 2\n"): + with self.subTest(tail=tail): + self.write_frames(self.first + tail) + with self.assertRaises(ValueError): + cube_tool.integrate_last_cube(self.path) + + def test_empty_file_is_rejected(self): + self.write_frames("") + with self.assertRaises(ValueError): + cube_tool.integrate_last_cube(self.path) + + def run_solvent_metric(self, frames): + output_dir = Path(self.scratch.name) / "OUT.autotest" + output_dir.mkdir() + (output_dir / "sol_cavity.cube").write_text(frames, encoding="utf-8") + (output_dir / "sol_eps.cube").write_text(frames, encoding="utf-8") + tool = Path(cube_tool.__file__).resolve() + script = tool.with_name("props_basic.sh") + command = ('source "$1"\nimp_sol=0\nout_sol=1\nassume_isolated=none\n' + 'CUBE_TOOL="$2"\nprops_result_file=result.out\nrun_basic_props_post_ml') + return subprocess.run(["bash", "-e", "-c", command, "solvent-metric-test", + str(script), str(tool)], cwd=self.scratch.name, + capture_output=True, text=True, check=False) + + def test_solvent_metric_invokes_the_final_frame_command(self): + result = self.run_solvent_metric(self.first + self.last) + self.assertEqual(result.returncode, 0, result.stderr) + metric = Path(self.scratch.name) / "result.out" + self.assertEqual(metric.read_text(), "sol_cavity_integralref 18\n") + + def test_solvent_metric_rejects_an_incomplete_tail(self): + result = self.run_solvent_metric(self.first + scalar_frame("4", 2, 2)) + self.assertNotEqual(result.returncode, 0) + self.assertFalse((Path(self.scratch.name) / "result.out").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/01_NAO_generation/qo/postprocess.py b/tools/01_NAO_generation/qo/postprocess.py index d4b7aeae615..f90a2d7238d 100644 --- a/tools/01_NAO_generation/qo/postprocess.py +++ b/tools/01_NAO_generation/qo/postprocess.py @@ -72,7 +72,8 @@ def read_lowf(flowf): np.ndarray: k-vector """ with open(flowf, 'r') as f: - lines = [line.strip() for line in f.readlines()] + # ignore provenance comment lines starting with '#' + lines = [line.strip() for line in f.readlines() if line.strip() and not line.strip().startswith('#')] for line in lines: if line.endswith("(number of bands)"): nband = int(line.split()[0])