pymdkit 1.7.3__tar.gz → 1.7.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {pymdkit-1.7.3 → pymdkit-1.7.4}/CHANGELOG.md +9 -0
  2. {pymdkit-1.7.3/src/pymdkit.egg-info → pymdkit-1.7.4}/PKG-INFO +42 -21
  3. {pymdkit-1.7.3 → pymdkit-1.7.4}/README.md +41 -20
  4. {pymdkit-1.7.3 → pymdkit-1.7.4}/pyproject.toml +1 -1
  5. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/__init__.py +1 -1
  6. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/msd.py +28 -13
  7. {pymdkit-1.7.3 → pymdkit-1.7.4/src/pymdkit.egg-info}/PKG-INFO +42 -21
  8. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_msd_large.py +1 -1
  9. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_msd_phase3.py +132 -6
  10. {pymdkit-1.7.3 → pymdkit-1.7.4}/LICENSE +0 -0
  11. {pymdkit-1.7.3 → pymdkit-1.7.4}/MANIFEST.in +0 -0
  12. {pymdkit-1.7.3 → pymdkit-1.7.4}/setup.cfg +0 -0
  13. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/_config.py +0 -0
  14. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/_logging.py +0 -0
  15. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/__init__.py +0 -0
  16. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_cifio.py +0 -0
  17. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_extxyz.py +0 -0
  18. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_fileio.py +0 -0
  19. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_geometry.py +0 -0
  20. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_gpumd.py +0 -0
  21. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_gpumd_final_structure.py +0 -0
  22. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_gpumd_msd.py +0 -0
  23. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_gpumd_run.py +0 -0
  24. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_gpumd_status.py +0 -0
  25. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_gpumd_thermo.py +0 -0
  26. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_lattice.py +0 -0
  27. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_auto.py +0 -0
  28. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_conductivity.py +0 -0
  29. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_coordinates.py +0 -0
  30. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_fit.py +0 -0
  31. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_io.py +0 -0
  32. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_provenance.py +0 -0
  33. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_range.py +0 -0
  34. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_thermo.py +0 -0
  35. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_msd_uncertainty.py +0 -0
  36. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_oxidation.py +0 -0
  37. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_parallel.py +0 -0
  38. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_precision.py +0 -0
  39. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_pred_msd_plan.py +0 -0
  40. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_pred_msd_reference.py +0 -0
  41. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_reduction.py +0 -0
  42. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_submit.py +0 -0
  43. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_submit_vasp.py +0 -0
  44. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_supercell.py +0 -0
  45. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_symmetry.py +0 -0
  46. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_vasp_ehull.py +0 -0
  47. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_vasp_final_structure.py +0 -0
  48. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_vasp_nep.py +0 -0
  49. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_vasp_output.py +0 -0
  50. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/_vaspset.py +0 -0
  51. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/chemsys_entry.py +0 -0
  52. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/compute_rmsd.py +0 -0
  53. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/config.py +0 -0
  54. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/convert.py +0 -0
  55. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/dope.py +0 -0
  56. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/ewald.py +0 -0
  57. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/gpumd_job.py +0 -0
  58. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/gpumd_out.py +0 -0
  59. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/nep_rmse.py +0 -0
  60. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/perturb.py +0 -0
  61. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/pred_msd_time.py +0 -0
  62. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/rd.py +0 -0
  63. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/realloc_occ.py +0 -0
  64. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/submit.py +0 -0
  65. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/supercell.py +0 -0
  66. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/trjconv.py +0 -0
  67. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/vasp_job.py +0 -0
  68. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/commands/vasp_out.py +0 -0
  69. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit/pymdkit_main.py +0 -0
  70. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit.egg-info/SOURCES.txt +0 -0
  71. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  72. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit.egg-info/entry_points.txt +0 -0
  73. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit.egg-info/requires.txt +0 -0
  74. {pymdkit-1.7.3 → pymdkit-1.7.4}/src/pymdkit.egg-info/top_level.txt +0 -0
  75. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/data/88-Li2YCl3-63-d1.cif +0 -0
  76. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_config_logging.py +0 -0
  77. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_convert.py +0 -0
  78. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_ehull.py +0 -0
  79. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_gpumd_out.py +0 -0
  80. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_lattice_symmetry.py +0 -0
  81. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_msd_auto.py +0 -0
  82. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_msd_fit.py +0 -0
  83. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_msd_range.py +0 -0
  84. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_msd_uncertainty.py +0 -0
  85. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_parallel.py +0 -0
  86. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_pred_msd_time.py +0 -0
  87. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_progress.py +0 -0
  88. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_realloc_occ.py +0 -0
  89. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_rmsd_gpu.py +0 -0
  90. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_rmsd_max.py +0 -0
  91. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_rmsd_two_folder.py +0 -0
  92. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_submit.py +0 -0
  93. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_v165.py +0 -0
  94. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_v170_job_infrastructure.py +0 -0
  95. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_v170_vasp_consolidation.py +0 -0
  96. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_v172_vasp_writing.py +0 -0
  97. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_vasp_out.py +0 -0
  98. {pymdkit-1.7.3 → pymdkit-1.7.4}/tests/test_vaspset.py +0 -0
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.4
4
+
5
+ - `pmk msd -ne` retains finite, positive Einstein diffusion coefficients and
6
+ Nernst–Einstein conductivity when the fit-quality diagnostics would reject the
7
+ window. The result is reported and aggregated as WARNING, with the original
8
+ diagnostic details preserved. This applies to automatic and manual windows.
9
+ - Missing valid regression, failed requested estimators, and invalid NE inputs
10
+ still fail the calculation and preserve existing aggregate summaries.
11
+
3
12
  ## 1.7.3
4
13
 
5
14
  - `pmk msd` analyzes fixed-cell GPUMD NVT trajectories, selects or accepts a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.7.3
3
+ Version: 1.7.4
4
4
  Summary: A Python command-line toolkit for high-throughput atomistic workflows in AI-driven materials design.
5
5
  Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
6
6
  License-Expression: GPL-3.0-or-later
@@ -36,6 +36,15 @@ materials design. Install `pymdkit` once and call its tools from anywhere as
36
36
  Commands expose explicit, documented CLI interfaces designed for reproducible shell,
37
37
  HPC, and AI-agent workflows, and each underlying script is still runnable on its own.
38
38
 
39
+ ## 1.7.4 release notes
40
+
41
+ - `pmk msd -ne` retains valid Einstein diffusion coefficients and conductivity
42
+ as WARNING when fit quality would otherwise be REJECT. This applies to
43
+ automatic and manual windows and preserves the individual diagnostic results.
44
+ - Missing valid regression, failed requested estimators, and invalid NE inputs
45
+ still cause calculation errors. Without `-ne`, fit-quality rejection still
46
+ withholds diffusion coefficients.
47
+
39
48
  ## 1.7.3 release notes
40
49
 
41
50
  - `pmk msd` analyzes GPUMD trajectories from fixed-cell NVT production. OLS is
@@ -97,7 +106,7 @@ installed.
97
106
  On a machine with Internet access, run:
98
107
 
99
108
  ```bash
100
- python -m pip download pymdkit==1.7.3 -d pymdkit-offline
109
+ python -m pip download pymdkit==1.7.4 -d pymdkit-offline
101
110
  ```
102
111
 
103
112
  This downloads `pymdkit` and the dependencies resolved by pip into the
@@ -105,14 +114,14 @@ This downloads `pymdkit` and the dependencies resolved by pip into the
105
114
  system. Then, inside the target pymdkit Python or conda environment, run:
106
115
 
107
116
  ```bash
108
- python -m pip install --no-index --find-links=pymdkit-offline pymdkit==1.7.3
117
+ python -m pip install --no-index --find-links=pymdkit-offline pymdkit==1.7.4
109
118
  ```
110
119
 
111
120
  For optional GPU use, prepare the bundle on a networked machine compatible with
112
121
  the target HPC platform and Python environment:
113
122
 
114
123
  ```bash
115
- python -m pip download "pymdkit[gpu-cuda12]==1.7.3" -d pymdkit-offline
124
+ python -m pip download "pymdkit[gpu-cuda12]==1.7.4" -d pymdkit-offline
116
125
  ```
117
126
 
118
127
  Transfer the whole `pymdkit-offline` directory, then install from it on the
@@ -120,7 +129,7 @@ target system:
120
129
 
121
130
  ```bash
122
131
  python -m pip install --no-index --find-links=pymdkit-offline \
123
- "pymdkit[gpu-cuda12]==1.7.3"
132
+ "pymdkit[gpu-cuda12]==1.7.4"
124
133
  ```
125
134
 
126
135
  This GPU bundle includes the optional `cupy-cuda12x` package. Its wheels must also
@@ -877,22 +886,33 @@ diffusion in cm²/s, `%.2e`) and, only with `-ne`, `msd/sigma.txt` (x/y/z/total
877
886
  Nernst–Einstein conductivity in mS/cm, `%.2f`). Headers are comments. Total
878
887
  diffusion and conductivity are directional arithmetic means before rounding;
879
888
  display rounding may cause last-digit differences. PASS and WARNING write these
880
- coefficients with status in their comment headers. Fit rejection returns nonzero
881
- but preserves `msd/msd.txt`, `fit-windows.txt` (all evaluated candidates, rejection
882
- reasons, beta, directional D, R², intercept, RSS, points and upper-lag origin
883
- count, intercept diagnostics and selected-window segment D values), and
884
- `fit-diagnostic.txt` (settings, selection and sensitivity). Beta-qualified window
885
- sensitivity, all-valid-candidate sensitivity, and three-segment slope spread are
886
- reported separately. Segments and MSD time origins are not independent samples. Rejected
887
- fits write no diffusion or conductivity files. Window-to-window spread is a
888
- sensitivity diagnostic, not a statistical confidence interval. Formal statistical uncertainty is not estimated by default. Existing `msd` directories cause an error: move them aside
889
- before repeating a calculation. Temporary coordinate storage is always removed.
889
+ coefficients with the quality status in their comment headers. `provenance.json`
890
+ records the final quality and full-precision total coefficients.
891
+
892
+ With `-ne`, an automatic or manual window with finite, positive x/y/z/total
893
+ Einstein diffusion coefficients retains D and NE conductivity even when the fit
894
+ quality would otherwise be REJECT. The overall status becomes WARNING. The
895
+ original reason and the beta, intercept-consistency, and three-segment slope
896
+ diagnostics remain in the fit output. A WARNING can indicate that the physical
897
+ diffusion regime failed quality criteria. Without `-ne`, REJECT returns nonzero
898
+ and retains only raw MSD and fit diagnostics. PASS and WARNING retain D.
899
+
900
+ `fit-windows.txt` records evaluated candidates, rejection reasons, beta,
901
+ directional D, R², intercept, RSS, points, upper-lag origin count, intercept
902
+ diagnostics, and selected-window segment D values. `fit-diagnostic.txt` records
903
+ settings, selection, and sensitivity. Beta-qualified window sensitivity,
904
+ all-valid-candidate sensitivity, and three-segment slope spread are reported
905
+ separately. Segments and MSD time origins are correlated. Window-to-window spread
906
+ measures sensitivity. Formal statistical uncertainty requires an explicit
907
+ request. Move an existing `msd` directory aside before repeating a calculation.
908
+ Temporary coordinate storage is removed when the calculation ends.
890
909
 
891
910
  The five-column raw MSD is published immediately after FFT succeeds, before
892
911
  window search, optional uncertainty, or thermodynamic/NE processing. Later errors
893
- retain this curve and any completed fit diagnostics; parsing/FFT failures are
894
- reported separately and do not publish a valid curve. REJECT prints the saved
895
- path and withholds formal diffusion/conductivity files and new aggregates.
912
+ retain this curve and any completed fit diagnostics. A missing valid regression,
913
+ a failed requested WLS/GLS estimator, or invalid required thermodynamic inputs
914
+ returns nonzero and prevents publication of new coefficients and aggregates.
915
+ Parsing and FFT failures occur before the raw curve is published.
896
916
  FFT uses float64 atom chunks sized from an internal 128 MiB workspace budget,
897
917
  with bounded remapping of time slabs to avoid retaining the entire coordinate
898
918
  memmap in RAM. For 100000 frames and 784 selected atoms the temporary coordinate
@@ -962,9 +982,10 @@ missing NVT targets produce an explicit error). Genuine calculation failures ret
962
982
  nonzero and preserve previous summaries; skipped templates are not failures.
963
983
  A successful run without `-ne` removes any stale root `sigma-total.txt`.
964
984
  The companion `msd-quality-total.txt` retains each directory's PASS/WARNING
965
- status without changing the existing three-column numeric summaries. A rejected
966
- directory prevents all new aggregate publication; prior summaries remain prior
967
- results and the terminal/log explicitly reports that they were preserved.
985
+ status. In NE mode, a directory whose valid coefficients were retained despite
986
+ fit-quality rejection appears as WARNING and contributes to both numeric
987
+ summaries. Without `-ne`, a directory with overall REJECT prevents publication
988
+ of new aggregates.
968
989
  See [the automatic-fit report](devtools/MSD_AUTO_FIT_REPORT.md) for validation,
969
990
  and [the intercept/stability update](devtools/MSD_AUTO_FIT_PHASE4A1_REPORT.md)
970
991
  for the current policy, 500 K MSD comparison and limitations.
@@ -7,6 +7,15 @@ materials design. Install `pymdkit` once and call its tools from anywhere as
7
7
  Commands expose explicit, documented CLI interfaces designed for reproducible shell,
8
8
  HPC, and AI-agent workflows, and each underlying script is still runnable on its own.
9
9
 
10
+ ## 1.7.4 release notes
11
+
12
+ - `pmk msd -ne` retains valid Einstein diffusion coefficients and conductivity
13
+ as WARNING when fit quality would otherwise be REJECT. This applies to
14
+ automatic and manual windows and preserves the individual diagnostic results.
15
+ - Missing valid regression, failed requested estimators, and invalid NE inputs
16
+ still cause calculation errors. Without `-ne`, fit-quality rejection still
17
+ withholds diffusion coefficients.
18
+
10
19
  ## 1.7.3 release notes
11
20
 
12
21
  - `pmk msd` analyzes GPUMD trajectories from fixed-cell NVT production. OLS is
@@ -68,7 +77,7 @@ installed.
68
77
  On a machine with Internet access, run:
69
78
 
70
79
  ```bash
71
- python -m pip download pymdkit==1.7.3 -d pymdkit-offline
80
+ python -m pip download pymdkit==1.7.4 -d pymdkit-offline
72
81
  ```
73
82
 
74
83
  This downloads `pymdkit` and the dependencies resolved by pip into the
@@ -76,14 +85,14 @@ This downloads `pymdkit` and the dependencies resolved by pip into the
76
85
  system. Then, inside the target pymdkit Python or conda environment, run:
77
86
 
78
87
  ```bash
79
- python -m pip install --no-index --find-links=pymdkit-offline pymdkit==1.7.3
88
+ python -m pip install --no-index --find-links=pymdkit-offline pymdkit==1.7.4
80
89
  ```
81
90
 
82
91
  For optional GPU use, prepare the bundle on a networked machine compatible with
83
92
  the target HPC platform and Python environment:
84
93
 
85
94
  ```bash
86
- python -m pip download "pymdkit[gpu-cuda12]==1.7.3" -d pymdkit-offline
95
+ python -m pip download "pymdkit[gpu-cuda12]==1.7.4" -d pymdkit-offline
87
96
  ```
88
97
 
89
98
  Transfer the whole `pymdkit-offline` directory, then install from it on the
@@ -91,7 +100,7 @@ target system:
91
100
 
92
101
  ```bash
93
102
  python -m pip install --no-index --find-links=pymdkit-offline \
94
- "pymdkit[gpu-cuda12]==1.7.3"
103
+ "pymdkit[gpu-cuda12]==1.7.4"
95
104
  ```
96
105
 
97
106
  This GPU bundle includes the optional `cupy-cuda12x` package. Its wheels must also
@@ -848,22 +857,33 @@ diffusion in cm²/s, `%.2e`) and, only with `-ne`, `msd/sigma.txt` (x/y/z/total
848
857
  Nernst–Einstein conductivity in mS/cm, `%.2f`). Headers are comments. Total
849
858
  diffusion and conductivity are directional arithmetic means before rounding;
850
859
  display rounding may cause last-digit differences. PASS and WARNING write these
851
- coefficients with status in their comment headers. Fit rejection returns nonzero
852
- but preserves `msd/msd.txt`, `fit-windows.txt` (all evaluated candidates, rejection
853
- reasons, beta, directional D, R², intercept, RSS, points and upper-lag origin
854
- count, intercept diagnostics and selected-window segment D values), and
855
- `fit-diagnostic.txt` (settings, selection and sensitivity). Beta-qualified window
856
- sensitivity, all-valid-candidate sensitivity, and three-segment slope spread are
857
- reported separately. Segments and MSD time origins are not independent samples. Rejected
858
- fits write no diffusion or conductivity files. Window-to-window spread is a
859
- sensitivity diagnostic, not a statistical confidence interval. Formal statistical uncertainty is not estimated by default. Existing `msd` directories cause an error: move them aside
860
- before repeating a calculation. Temporary coordinate storage is always removed.
860
+ coefficients with the quality status in their comment headers. `provenance.json`
861
+ records the final quality and full-precision total coefficients.
862
+
863
+ With `-ne`, an automatic or manual window with finite, positive x/y/z/total
864
+ Einstein diffusion coefficients retains D and NE conductivity even when the fit
865
+ quality would otherwise be REJECT. The overall status becomes WARNING. The
866
+ original reason and the beta, intercept-consistency, and three-segment slope
867
+ diagnostics remain in the fit output. A WARNING can indicate that the physical
868
+ diffusion regime failed quality criteria. Without `-ne`, REJECT returns nonzero
869
+ and retains only raw MSD and fit diagnostics. PASS and WARNING retain D.
870
+
871
+ `fit-windows.txt` records evaluated candidates, rejection reasons, beta,
872
+ directional D, R², intercept, RSS, points, upper-lag origin count, intercept
873
+ diagnostics, and selected-window segment D values. `fit-diagnostic.txt` records
874
+ settings, selection, and sensitivity. Beta-qualified window sensitivity,
875
+ all-valid-candidate sensitivity, and three-segment slope spread are reported
876
+ separately. Segments and MSD time origins are correlated. Window-to-window spread
877
+ measures sensitivity. Formal statistical uncertainty requires an explicit
878
+ request. Move an existing `msd` directory aside before repeating a calculation.
879
+ Temporary coordinate storage is removed when the calculation ends.
861
880
 
862
881
  The five-column raw MSD is published immediately after FFT succeeds, before
863
882
  window search, optional uncertainty, or thermodynamic/NE processing. Later errors
864
- retain this curve and any completed fit diagnostics; parsing/FFT failures are
865
- reported separately and do not publish a valid curve. REJECT prints the saved
866
- path and withholds formal diffusion/conductivity files and new aggregates.
883
+ retain this curve and any completed fit diagnostics. A missing valid regression,
884
+ a failed requested WLS/GLS estimator, or invalid required thermodynamic inputs
885
+ returns nonzero and prevents publication of new coefficients and aggregates.
886
+ Parsing and FFT failures occur before the raw curve is published.
867
887
  FFT uses float64 atom chunks sized from an internal 128 MiB workspace budget,
868
888
  with bounded remapping of time slabs to avoid retaining the entire coordinate
869
889
  memmap in RAM. For 100000 frames and 784 selected atoms the temporary coordinate
@@ -933,9 +953,10 @@ missing NVT targets produce an explicit error). Genuine calculation failures ret
933
953
  nonzero and preserve previous summaries; skipped templates are not failures.
934
954
  A successful run without `-ne` removes any stale root `sigma-total.txt`.
935
955
  The companion `msd-quality-total.txt` retains each directory's PASS/WARNING
936
- status without changing the existing three-column numeric summaries. A rejected
937
- directory prevents all new aggregate publication; prior summaries remain prior
938
- results and the terminal/log explicitly reports that they were preserved.
956
+ status. In NE mode, a directory whose valid coefficients were retained despite
957
+ fit-quality rejection appears as WARNING and contributes to both numeric
958
+ summaries. Without `-ne`, a directory with overall REJECT prevents publication
959
+ of new aggregates.
939
960
  See [the automatic-fit report](devtools/MSD_AUTO_FIT_REPORT.md) for validation,
940
961
  and [the intercept/stability update](devtools/MSD_AUTO_FIT_PHASE4A1_REPORT.md)
941
962
  for the current policy, 500 K MSD comparison and limitations.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pymdkit"
7
- version = "1.7.3"
7
+ version = "1.7.4"
8
8
  description = "A Python command-line toolkit for high-throughput atomistic workflows in AI-driven materials design."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """A Python command-line toolkit for high-throughput atomistic workflows in AI-driven materials design."""
2
2
 
3
- __version__ = "1.7.3"
3
+ __version__ = "1.7.4"
@@ -34,12 +34,12 @@ def production_temperature(production):
34
34
 
35
35
 
36
36
  def add_arguments(parser):
37
- parser.epilog = ('Automatic fits use positive MSD lags below 0.7T; beta, intercept consistency '
38
- 'and three-segment Einstein slopes diagnose the regime. '
39
- 'Einstein regression computes D. PASS/WARNING retain coefficients; REJECT '
40
- 'retains msd.txt, fit-windows.txt and fit-diagnostic.txt only. '
41
- 'Raw beta rejection can become WARNING only with positive-intercept '
42
- 'agreement and acceptable segment slopes. '
37
+ parser.epilog = ('Automatic fits use positive MSD lags below 0.7T. Beta, intercept consistency '
38
+ 'and three-segment Einstein slopes diagnose the diffusion regime. '
39
+ 'Einstein regression calculates D. With -ne, valid Einstein coefficients and '
40
+ 'conductivity are retained as WARNING if fit quality would otherwise be REJECT. '
41
+ 'Without -ne, PASS/WARNING retain D and REJECT retains only raw MSD and fit diagnostics. '
42
+ 'Missing valid regression or invalid NE thermodynamic inputs cause calculation errors. '
43
43
  'Window spread is sensitivity, not a confidence interval. '
44
44
  'Optional --uncertainty covariance reports conditional Bayesian uncertainty separately.')
45
45
  parser.add_argument('--uncertainty', choices=['covariance'])
@@ -196,11 +196,15 @@ def analyze(folder, args):
196
196
  (result/'diagnostics.json').write_text(json.dumps(dict(selection=segment_metadata, status='ERROR', reason=str(exc)), indent=2), encoding='utf-8')
197
197
  raise ValueError(f'fitting-window search failed after MSD calculation; raw MSD retained: {exc}') from exc
198
198
  selected = diagnostic['selected']
199
- if selected is not None and diagnostic['status'] != 'REJECT':
199
+ diffusion = None
200
+ fit_error = None
201
+ if selected is not None and (args.ne or diagnostic['status'] != 'REJECT'):
200
202
  interval = (selected['start'], selected['end'])
201
203
  beta = selected['beta']
202
- diffusion = np.array([selected['D_'+k] for k in ('x', 'y', 'z', 'total')])
203
- if args.fit_method != 'ols':
204
+ diffusion = np.array([selected.get('D_'+k, np.nan) for k in ('x', 'y', 'z', 'total')], dtype=float)
205
+ if args.ne and (not np.isfinite(diffusion).all() or np.any(diffusion <= 0)):
206
+ diffusion = None
207
+ if diffusion is not None and args.fit_method != 'ols':
204
208
  try:
205
209
  fit = fit_directions(mapped, dt, interval, args.fit_method)
206
210
  diffusion = np.array([r['diffusion'] for r in fit['axes'].values()] + [fit['diffusion']])*1e-4
@@ -210,9 +214,19 @@ def analyze(folder, args):
210
214
  diagnostic['method_intercepts_A2'] = {
211
215
  axis: estimate['intercept'] for axis, estimate in fit['axes'].items()
212
216
  }
213
- except (ValueError, RuntimeError, ImportError) as exc:
214
- diagnostic.update(status='REJECT', overall_status='REJECT', reason=str(exc))
215
- selected['overall_status'] = 'REJECT'
217
+ except (ValueError, RuntimeError, ImportError, np.linalg.LinAlgError) as exc:
218
+ fit_error = f'{args.fit_method.upper()} Einstein regression failed: {exc}' if args.ne else str(exc)
219
+ if args.ne and diffusion is None:
220
+ fit_error = f"No valid Einstein regression available. {diagnostic['reason']}"
221
+ if fit_error is not None:
222
+ diagnostic.update(status='REJECT', overall_status='REJECT', reason=fit_error)
223
+ if selected is not None:
224
+ selected['overall_status'] = 'REJECT'
225
+ elif args.ne and diagnostic['status'] == 'REJECT':
226
+ diagnostic.update(status='WARNING', overall_status='WARNING', reason=(
227
+ 'NE mode retained valid Einstein coefficients despite the original fit-quality REJECT. '
228
+ + diagnostic['reason']))
229
+ selected['overall_status'] = 'WARNING'
216
230
  write_diagnostics(result, diagnostic, duration, dt, len(times), len(indices), args.fit_method, mode,
217
231
  getattr(args, 'mobile_site_distance', None))
218
232
  if selection is not None:
@@ -223,7 +237,8 @@ def analyze(folder, args):
223
237
  if diagnostic['status'] == 'REJECT':
224
238
  print(' No self-diffusion or NE conductivity output generated.')
225
239
  saved = 'msd/' if selection is None else str(result)
226
- raise ValueError('REJECT: '+diagnostic['reason']+f'; MSD and diagnostics saved in {saved}')
240
+ failure = 'MSD calculation failed' if args.ne else 'REJECT'
241
+ raise ValueError(f"{failure}: {diagnostic['reason']}; MSD and diagnostics saved in {saved}")
227
242
  uncertainty = None
228
243
  if getattr(args, 'uncertainty', None):
229
244
  from ._msd_uncertainty import estimate, write_outputs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.7.3
3
+ Version: 1.7.4
4
4
  Summary: A Python command-line toolkit for high-throughput atomistic workflows in AI-driven materials design.
5
5
  Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
6
6
  License-Expression: GPL-3.0-or-later
@@ -36,6 +36,15 @@ materials design. Install `pymdkit` once and call its tools from anywhere as
36
36
  Commands expose explicit, documented CLI interfaces designed for reproducible shell,
37
37
  HPC, and AI-agent workflows, and each underlying script is still runnable on its own.
38
38
 
39
+ ## 1.7.4 release notes
40
+
41
+ - `pmk msd -ne` retains valid Einstein diffusion coefficients and conductivity
42
+ as WARNING when fit quality would otherwise be REJECT. This applies to
43
+ automatic and manual windows and preserves the individual diagnostic results.
44
+ - Missing valid regression, failed requested estimators, and invalid NE inputs
45
+ still cause calculation errors. Without `-ne`, fit-quality rejection still
46
+ withholds diffusion coefficients.
47
+
39
48
  ## 1.7.3 release notes
40
49
 
41
50
  - `pmk msd` analyzes GPUMD trajectories from fixed-cell NVT production. OLS is
@@ -97,7 +106,7 @@ installed.
97
106
  On a machine with Internet access, run:
98
107
 
99
108
  ```bash
100
- python -m pip download pymdkit==1.7.3 -d pymdkit-offline
109
+ python -m pip download pymdkit==1.7.4 -d pymdkit-offline
101
110
  ```
102
111
 
103
112
  This downloads `pymdkit` and the dependencies resolved by pip into the
@@ -105,14 +114,14 @@ This downloads `pymdkit` and the dependencies resolved by pip into the
105
114
  system. Then, inside the target pymdkit Python or conda environment, run:
106
115
 
107
116
  ```bash
108
- python -m pip install --no-index --find-links=pymdkit-offline pymdkit==1.7.3
117
+ python -m pip install --no-index --find-links=pymdkit-offline pymdkit==1.7.4
109
118
  ```
110
119
 
111
120
  For optional GPU use, prepare the bundle on a networked machine compatible with
112
121
  the target HPC platform and Python environment:
113
122
 
114
123
  ```bash
115
- python -m pip download "pymdkit[gpu-cuda12]==1.7.3" -d pymdkit-offline
124
+ python -m pip download "pymdkit[gpu-cuda12]==1.7.4" -d pymdkit-offline
116
125
  ```
117
126
 
118
127
  Transfer the whole `pymdkit-offline` directory, then install from it on the
@@ -120,7 +129,7 @@ target system:
120
129
 
121
130
  ```bash
122
131
  python -m pip install --no-index --find-links=pymdkit-offline \
123
- "pymdkit[gpu-cuda12]==1.7.3"
132
+ "pymdkit[gpu-cuda12]==1.7.4"
124
133
  ```
125
134
 
126
135
  This GPU bundle includes the optional `cupy-cuda12x` package. Its wheels must also
@@ -877,22 +886,33 @@ diffusion in cm²/s, `%.2e`) and, only with `-ne`, `msd/sigma.txt` (x/y/z/total
877
886
  Nernst–Einstein conductivity in mS/cm, `%.2f`). Headers are comments. Total
878
887
  diffusion and conductivity are directional arithmetic means before rounding;
879
888
  display rounding may cause last-digit differences. PASS and WARNING write these
880
- coefficients with status in their comment headers. Fit rejection returns nonzero
881
- but preserves `msd/msd.txt`, `fit-windows.txt` (all evaluated candidates, rejection
882
- reasons, beta, directional D, R², intercept, RSS, points and upper-lag origin
883
- count, intercept diagnostics and selected-window segment D values), and
884
- `fit-diagnostic.txt` (settings, selection and sensitivity). Beta-qualified window
885
- sensitivity, all-valid-candidate sensitivity, and three-segment slope spread are
886
- reported separately. Segments and MSD time origins are not independent samples. Rejected
887
- fits write no diffusion or conductivity files. Window-to-window spread is a
888
- sensitivity diagnostic, not a statistical confidence interval. Formal statistical uncertainty is not estimated by default. Existing `msd` directories cause an error: move them aside
889
- before repeating a calculation. Temporary coordinate storage is always removed.
889
+ coefficients with the quality status in their comment headers. `provenance.json`
890
+ records the final quality and full-precision total coefficients.
891
+
892
+ With `-ne`, an automatic or manual window with finite, positive x/y/z/total
893
+ Einstein diffusion coefficients retains D and NE conductivity even when the fit
894
+ quality would otherwise be REJECT. The overall status becomes WARNING. The
895
+ original reason and the beta, intercept-consistency, and three-segment slope
896
+ diagnostics remain in the fit output. A WARNING can indicate that the physical
897
+ diffusion regime failed quality criteria. Without `-ne`, REJECT returns nonzero
898
+ and retains only raw MSD and fit diagnostics. PASS and WARNING retain D.
899
+
900
+ `fit-windows.txt` records evaluated candidates, rejection reasons, beta,
901
+ directional D, R², intercept, RSS, points, upper-lag origin count, intercept
902
+ diagnostics, and selected-window segment D values. `fit-diagnostic.txt` records
903
+ settings, selection, and sensitivity. Beta-qualified window sensitivity,
904
+ all-valid-candidate sensitivity, and three-segment slope spread are reported
905
+ separately. Segments and MSD time origins are correlated. Window-to-window spread
906
+ measures sensitivity. Formal statistical uncertainty requires an explicit
907
+ request. Move an existing `msd` directory aside before repeating a calculation.
908
+ Temporary coordinate storage is removed when the calculation ends.
890
909
 
891
910
  The five-column raw MSD is published immediately after FFT succeeds, before
892
911
  window search, optional uncertainty, or thermodynamic/NE processing. Later errors
893
- retain this curve and any completed fit diagnostics; parsing/FFT failures are
894
- reported separately and do not publish a valid curve. REJECT prints the saved
895
- path and withholds formal diffusion/conductivity files and new aggregates.
912
+ retain this curve and any completed fit diagnostics. A missing valid regression,
913
+ a failed requested WLS/GLS estimator, or invalid required thermodynamic inputs
914
+ returns nonzero and prevents publication of new coefficients and aggregates.
915
+ Parsing and FFT failures occur before the raw curve is published.
896
916
  FFT uses float64 atom chunks sized from an internal 128 MiB workspace budget,
897
917
  with bounded remapping of time slabs to avoid retaining the entire coordinate
898
918
  memmap in RAM. For 100000 frames and 784 selected atoms the temporary coordinate
@@ -962,9 +982,10 @@ missing NVT targets produce an explicit error). Genuine calculation failures ret
962
982
  nonzero and preserve previous summaries; skipped templates are not failures.
963
983
  A successful run without `-ne` removes any stale root `sigma-total.txt`.
964
984
  The companion `msd-quality-total.txt` retains each directory's PASS/WARNING
965
- status without changing the existing three-column numeric summaries. A rejected
966
- directory prevents all new aggregate publication; prior summaries remain prior
967
- results and the terminal/log explicitly reports that they were preserved.
985
+ status. In NE mode, a directory whose valid coefficients were retained despite
986
+ fit-quality rejection appears as WARNING and contributes to both numeric
987
+ summaries. Without `-ne`, a directory with overall REJECT prevents publication
988
+ of new aggregates.
968
989
  See [the automatic-fit report](devtools/MSD_AUTO_FIT_REPORT.md) for validation,
969
990
  and [the intercept/stability update](devtools/MSD_AUTO_FIT_PHASE4A1_REPORT.md)
970
991
  for the current policy, 500 K MSD comparison and limitations.
@@ -64,7 +64,7 @@ class LargeMSDTests(unittest.TestCase):
64
64
  args.uncertainty = 'covariance'
65
65
  args.min_fit_window = 1.
66
66
  with patch('pymdkit.commands._msd_uncertainty.estimate', side_effect=RuntimeError('unavailable')) as estimate:
67
- with self.assertRaisesRegex(ValueError, 'REJECT'):
67
+ with self.assertRaisesRegex(ValueError, 'No valid Einstein regression'):
68
68
  msd.analyze(self.root, args)
69
69
  estimate.assert_not_called()
70
70
  for name in ('msd.txt', 'fit-windows.txt', 'fit-diagnostic.txt'):
@@ -1,3 +1,5 @@
1
+ import argparse
2
+ import json
1
3
  import os
2
4
  from pathlib import Path
3
5
  import subprocess
@@ -13,7 +15,8 @@ from pymdkit.commands._gpumd_run import parse_run, trajectory_schedule
13
15
  from pymdkit.commands._msd_thermo import load_thermo
14
16
  from pymdkit.commands._msd_conductivity import conductivity, ELEMENTARY_CHARGE, BOLTZMANN
15
17
  from pymdkit.commands._msd_coordinates import Coordinates
16
- from pymdkit.commands import msd
18
+ from pymdkit.commands import _msd_auto, _msd_fit, msd
19
+ from pymdkit.commands._pred_msd_plan import read_plan
17
20
 
18
21
 
19
22
  HPC_RUN = '''time_step 1.0
@@ -258,24 +261,24 @@ class Phase3Tests(unittest.TestCase):
258
261
  with self.assertRaisesRegex(ValueError, 'ordering'):
259
262
  p.update(f)
260
263
 
261
- def fixture(self, folder):
264
+ def fixture(self, folder, *, retention=1., step_scale=.05, temperature=300):
262
265
  folder.mkdir(exist_ok=True)
263
266
  n, count = 129, 64
264
267
  model = Atoms('Li'*count+'O'*count, positions=np.zeros((count*2, 3)), cell=[100]*3, pbc=True)
265
268
  model.arrays['group'] = np.r_[np.zeros(count, dtype=int), np.ones(count, dtype=int)]
266
269
  write(folder/'model.xyz', model)
267
- (folder/'run.in').write_text(f'time_step 1\nensemble nvt_nhc 300 300 100\ndump_thermo 1\ndump_exyz 1 1 1\nrun {n}\n')
270
+ (folder/'run.in').write_text(f'time_step 1\nensemble nvt_nhc {temperature} {temperature} 100\ndump_thermo 1\ndump_exyz 1 1 1\nrun {n}\n')
268
271
  rng = np.random.default_rng(17)
269
272
  raw = np.zeros((count*2, 3))
270
273
  for i in range(n):
271
- raw[:count] += rng.normal(0, .05, (count, 3))
274
+ raw[:count] = retention*raw[:count] + rng.normal(0, step_scale, (count, 3))
272
275
  f = model.copy()
273
276
  f.positions = raw % 100
274
277
  f.arrays['unwrapped_position'] = raw.copy()
275
278
  f.info['Time'] = i+1
276
279
  write(folder/'dump.xyz', f, append=i>0)
277
280
  data = np.zeros((n, 18))
278
- data[:, 0] = 300
281
+ data[:, 0] = temperature
279
282
  data[:, 9:] = np.tile(np.diag([100]*3).ravel(), (n, 1))
280
283
  np.savetxt(folder/'thermo.out', data)
281
284
 
@@ -310,7 +313,7 @@ class Phase3Tests(unittest.TestCase):
310
313
  args = type('Args', (), dict(group=0, ne=True, charge=1., fit_range=None,
311
314
  fit_method='ols', min_fit_window=1.))()
312
315
  with patch.object(_msd_fit, 'fft_msd', wraps=_msd_fit.fft_msd) as fft:
313
- with self.assertRaisesRegex(ValueError, 'REJECT'):
316
+ with self.assertRaisesRegex(ValueError, 'No valid Einstein regression'):
314
317
  msd.analyze(self.root, args)
315
318
  self.assertEqual(fft.call_count, 1)
316
319
  for name in ('msd.txt', 'fit-windows.txt', 'fit-diagnostic.txt'):
@@ -347,6 +350,129 @@ class Phase3Tests(unittest.TestCase):
347
350
  for name in ('self-diff.txt', 'sigma.txt'):
348
351
  self.assertIn('quality=WARNING', (self.root/'msd'/name).read_text())
349
352
 
353
+ def test_quality_rejection_with_valid_regression(self):
354
+ t = np.arange(129)*.001
355
+ values = t[:, None]**.6*np.array([.5, .6, .7])
356
+ parser = argparse.ArgumentParser()
357
+ msd.add_arguments(parser)
358
+ for name, flags in (
359
+ ('non-ne', []),
360
+ ('auto-ne', ['-ne']),
361
+ ('manual-ne', ['-ne', '-fit-range', '.01', '.075', '-trajectory-range', '0', '.000129']),
362
+ ):
363
+ with self.subTest(mode=name):
364
+ folder = self.root/name
365
+ self.fixture(folder)
366
+ args = parser.parse_args(['-group', '0', *flags])
367
+ original = _msd_auto.search(t, values, .129, interval=args.fit_range)
368
+ self.assertEqual(original['status'], 'REJECT')
369
+ self.assertIsNotNone(original['selected'])
370
+ directions = ('x', 'y', 'z', 'total')
371
+ expected = np.array([original['selected']['D_'+axis] for axis in directions])
372
+ self.assertTrue(np.all(np.isfinite(expected) & (expected > 0)))
373
+ with patch.object(Path, 'cwd', return_value=self.root), patch.object(
374
+ _msd_fit, 'fft_msd', return_value=values):
375
+ if args.ne:
376
+ result = msd.analyze(folder, args)
377
+ else:
378
+ with self.assertRaisesRegex(ValueError, '^REJECT: Raw beta'):
379
+ msd.analyze(folder, args)
380
+
381
+ out = folder/'msd'
382
+ if args.trajectory_range:
383
+ out = out/'segments/0-0.000129ns'
384
+ fit = json.loads((out/'diagnostics.json').read_text())['fit']
385
+ self.assertEqual(fit['status'], 'WARNING')
386
+ self.assertEqual(fit['overall_status'], 'WARNING')
387
+ self.assertEqual(fit['selected']['overall_status'], 'WARNING')
388
+ self.assertEqual(fit['segments'], original['segments'])
389
+ self.assertTrue((out/'msd.txt').is_file())
390
+ windows = (out/'fit-windows.txt').read_text()
391
+ diagnostic = (out/'fit-diagnostic.txt').read_text()
392
+ for key in ('beta_status', 'intercept_status', 'einstein_slope_status'):
393
+ self.assertIn(f'\n{key}: {original[key]}\n', diagnostic)
394
+ self.assertIn(original['reason'], diagnostic)
395
+ if not args.ne:
396
+ self.assertIn('\nstatus: REJECT\n', diagnostic)
397
+ for output in ('self-diff.txt', 'sigma.txt', 'provenance.json'):
398
+ self.assertFalse((out/output).exists())
399
+ continue
400
+ self.assertEqual(result.quality, 'WARNING')
401
+ self.assertIn('\nstatus: WARNING\n', diagnostic)
402
+ self.assertIn('\noverall_status: WARNING\n', diagnostic)
403
+ self.assertIn('NE mode retained valid Einstein coefficients', diagnostic)
404
+ self.assertTrue(any(row.endswith('\tWARNING') for row in windows.splitlines()))
405
+ expected_sigma = conductivity(expected, 64, 1, 1e6, 300, ['Li'])
406
+ self.assertAlmostEqual(result.diffusion, expected[-1])
407
+ self.assertAlmostEqual(result.conductivity, expected_sigma[-1])
408
+ for output, prefix, coefficients, fmt in (
409
+ ('self-diff.txt', 'D', expected, '.2e'),
410
+ ('sigma.txt', 'sigma', expected_sigma, '.2f'),
411
+ ):
412
+ header, *rows = (out/output).read_text().splitlines()
413
+ self.assertIn('quality=WARNING', header)
414
+ self.assertEqual([row.split() for row in rows], [
415
+ [f'{prefix}_{axis}', format(value, fmt)] for axis, value in zip(directions, coefficients)])
416
+ provenance = json.loads((out/'provenance.json').read_text())
417
+ self.assertEqual(provenance['quality'], 'WARNING')
418
+ self.assertEqual(provenance['diffusion_cm2_s'], result.diffusion)
419
+ self.assertEqual(provenance['conductivity_mS_cm'], result.conductivity)
420
+
421
+ def test_ne_rejected_quality_does_not_hide_weighted_estimator_failure(self):
422
+ t = np.arange(129)*.001
423
+ values = t[:, None]**.6*np.array([.5, .6, .7])
424
+ for method in ('wls', 'gls'):
425
+ with self.subTest(method=method):
426
+ folder = self.root/method
427
+ self.fixture(folder)
428
+ args = argparse.Namespace(group=0, ne=True, charge=1., fit_range=[.013, .064], fit_method=method)
429
+ self.assertEqual(_msd_auto.search(t, values, .129, interval=args.fit_range)['status'], 'REJECT')
430
+ with patch.object(_msd_fit, 'fft_msd', return_value=values):
431
+ with self.assertRaisesRegex(ValueError, f'{method.upper()} Einstein regression failed: .*binary points'):
432
+ msd.analyze(folder, args)
433
+ for output in ('msd.txt', 'fit-windows.txt', 'fit-diagnostic.txt'):
434
+ self.assertTrue((folder/'msd'/output).is_file())
435
+ for output in ('self-diff.txt', 'sigma.txt', 'provenance.json'):
436
+ self.assertFalse((folder/'msd'/output).exists())
437
+
438
+ def test_cli_ne_aggregates_rejected_quality_and_predicts_from_warning(self):
439
+ confined, brownian = self.root/'confined', self.root/'brownian'
440
+ self.fixture(confined, retention=.96)
441
+ self.fixture(brownian, temperature=500)
442
+ result = self.cli('msd', '-group', '0', '-ne')
443
+ self.assertEqual(result.returncode, 0, result.stdout+result.stderr)
444
+ for folder in (confined, brownian):
445
+ for output in ('self-diff.txt', 'sigma.txt', 'provenance.json'):
446
+ self.assertTrue((folder/'msd'/output).is_file())
447
+ for output in ('self-diff-total.txt', 'sigma-total.txt'):
448
+ _, *rows = (self.root/output).read_text().splitlines()
449
+ self.assertEqual([row.split()[0] for row in rows], ['confined', 'brownian'])
450
+ self.assertIn('confined WARNING', (self.root/'msd-quality-total.txt').read_text())
451
+ diagnostic = (confined/'msd/fit-diagnostic.txt').read_text()
452
+ self.assertIn('NE mode retained valid Einstein coefficients', diagnostic)
453
+ result = self.cli('pred-msd-time', '-ea', '.2', '-temp-interval', '50', '-max-temp', '350')
454
+ self.assertEqual(result.returncode, 0, result.stdout+result.stderr)
455
+ plan = read_plan(self.root/'pred-msd-time.log')
456
+ self.assertEqual(plan['reference']['temperature_K'], 300)
457
+ self.assertEqual(plan['reference']['quality'], 'WARNING')
458
+ self.assertTrue(any('BASELINE WARNING' in warning for warning in plan['warnings']))
459
+
460
+ def test_cli_ne_zero_msd_preserves_diagnostics_and_aggregate_summaries(self):
461
+ self.fixture(self.root, step_scale=0.)
462
+ msd.write_summaries(self.root, [msd.AnalysisResult('prior', 300., 1.e-5, 1.)], ne=True)
463
+ summaries = {p: p.read_bytes() for p in self.root.glob('*-total.txt')}
464
+ result = self.cli('msd', '-group', '0', '-ne')
465
+ self.assertNotEqual(result.returncode, 0, result.stdout)
466
+ self.assertIn('No valid Einstein regression', result.stdout)
467
+ self.assertIn('existing summaries preserved', result.stdout)
468
+ out = self.root/'msd'
469
+ np.testing.assert_array_equal(np.loadtxt(out/'msd.txt')[:, 1:], 0.)
470
+ for output in ('fit-windows.txt', 'fit-diagnostic.txt'):
471
+ self.assertTrue((out/output).is_file())
472
+ for output in ('self-diff.txt', 'sigma.txt', 'provenance.json'):
473
+ self.assertFalse((out/output).exists())
474
+ self.assertEqual({p: p.read_bytes() for p in summaries}, summaries)
475
+
350
476
  def test_cli_multi_temperature_templates_and_failure_preserves_summary(self):
351
477
  for name, target, measured in [('hot', 1000, 1001), ('cold-b', 300, 301), ('cold-a', 300, 301)]:
352
478
  folder = self.root/name
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes