makepst 0.1.0__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 (61) hide show
  1. makepst-0.1.0/CHANGELOG.md +32 -0
  2. makepst-0.1.0/CITATION.cff +19 -0
  3. makepst-0.1.0/LICENSE +21 -0
  4. makepst-0.1.0/MANIFEST.in +4 -0
  5. makepst-0.1.0/PKG-INFO +514 -0
  6. makepst-0.1.0/README.md +483 -0
  7. makepst-0.1.0/examples/minimal/control.csv +5 -0
  8. makepst-0.1.0/examples/minimal/flow.ins +2 -0
  9. makepst-0.1.0/examples/minimal/flow.out +1 -0
  10. makepst-0.1.0/examples/minimal/heads.ins +4 -0
  11. makepst-0.1.0/examples/minimal/heads.out +4 -0
  12. makepst-0.1.0/examples/minimal/io.csv +5 -0
  13. makepst-0.1.0/examples/minimal/model.in +4 -0
  14. makepst-0.1.0/examples/minimal/model.par +5 -0
  15. makepst-0.1.0/examples/minimal/model.py +12 -0
  16. makepst-0.1.0/examples/minimal/model.res +5 -0
  17. makepst-0.1.0/examples/minimal/model.tpl +5 -0
  18. makepst-0.1.0/examples/minimal/obs.csv +5 -0
  19. makepst-0.1.0/examples/minimal/par.csv +5 -0
  20. makepst-0.1.0/examples/minimal/pargp.csv +3 -0
  21. makepst-0.1.0/makepst/__init__.py +25 -0
  22. makepst-0.1.0/makepst/__main__.py +3 -0
  23. makepst-0.1.0/makepst/checks.py +411 -0
  24. makepst-0.1.0/makepst/cli.py +351 -0
  25. makepst-0.1.0/makepst/diff.py +151 -0
  26. makepst-0.1.0/makepst/excel.py +336 -0
  27. makepst-0.1.0/makepst/phi.py +89 -0
  28. makepst-0.1.0/makepst/provenance.py +89 -0
  29. makepst-0.1.0/makepst/pst.py +449 -0
  30. makepst-0.1.0/makepst/pyemu_bridge.py +39 -0
  31. makepst-0.1.0/makepst/reader.py +236 -0
  32. makepst-0.1.0/makepst/sections.py +264 -0
  33. makepst-0.1.0/makepst/starter.py +295 -0
  34. makepst-0.1.0/makepst/writer.py +164 -0
  35. makepst-0.1.0/makepst.egg-info/PKG-INFO +514 -0
  36. makepst-0.1.0/makepst.egg-info/SOURCES.txt +59 -0
  37. makepst-0.1.0/makepst.egg-info/dependency_links.txt +1 -0
  38. makepst-0.1.0/makepst.egg-info/entry_points.txt +2 -0
  39. makepst-0.1.0/makepst.egg-info/requires.txt +12 -0
  40. makepst-0.1.0/makepst.egg-info/top_level.txt +1 -0
  41. makepst-0.1.0/pyproject.toml +46 -0
  42. makepst-0.1.0/setup.cfg +4 -0
  43. makepst-0.1.0/tests/data/demo.3.obs.csv +4 -0
  44. makepst-0.1.0/tests/data/demo.3.par.csv +4 -0
  45. makepst-0.1.0/tests/data/demo.par +12 -0
  46. makepst-0.1.0/tests/data/demo.phi.actual.csv +3 -0
  47. makepst-0.1.0/tests/data/demo.pst +70 -0
  48. makepst-0.1.0/tests/data/demo.res +7 -0
  49. makepst-0.1.0/tests/data/demo.xlsx +0 -0
  50. makepst-0.1.0/tests/make_fixture.py +190 -0
  51. makepst-0.1.0/tests/test_checks.py +185 -0
  52. makepst-0.1.0/tests/test_diff.py +78 -0
  53. makepst-0.1.0/tests/test_examples.py +68 -0
  54. makepst-0.1.0/tests/test_fixture.py +409 -0
  55. makepst-0.1.0/tests/test_makepst.py +242 -0
  56. makepst-0.1.0/tests/test_phi.py +75 -0
  57. makepst-0.1.0/tests/test_provenance.py +104 -0
  58. makepst-0.1.0/tests/test_pyemu.py +52 -0
  59. makepst-0.1.0/tests/test_starter.py +105 -0
  60. makepst-0.1.0/tests/test_v2.py +115 -0
  61. makepst-0.1.0/tests/test_version.py +19 -0
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-09-19)
4
+
5
+ First packaged release; replaces the single-file `makePst.py` script.
6
+
7
+ - `init`: starter workbook with headers, defaults, descriptions, drop-down lists, comments and
8
+ a `BUILD` sheet
9
+ - `build`: Excel/CSV tables -> control file; sheet globs (`book,PAR_*`); a workbook with a
10
+ `BUILD` sheet as the only argument; counts computed; PEST_HP keyed tokens and
11
+ `++` options; regularisation equations from `PRIOR`/`WEIGHT` columns; validation of tied
12
+ targets, groups, prior references and bounds
13
+ - `dump`: control file -> workbook with a `BUILD` sheet that rebuilds it
14
+ - `update`: `.par` / `.res` / `.rei` / PESTPP-IES ensembles into an existing workbook, matched
15
+ by name across sheets; formula cells protected; `--sheet` / `--group` filters; `--real`;
16
+ `PHI` sheet (objective function by observation group) and `PHI_IES` (realization phis)
17
+ whenever residuals are available
18
+ - `validate`: pestchek-style report on a control file or workbook: table checks, missing
19
+ files, parameter / observation names against template and instruction files, optional run
20
+ of instruction files against model outputs; exit 1 on errors
21
+ - PEST++ version-2 control files (`pcf version=2`, keyword control data, external csv
22
+ tables) are read and written (`--v2`); observation covariance files are kept
23
+ - `diff`: semantic comparison of two control files / workbooks (tables, effective control
24
+ values, options, io, comments), text report or workbook; exit 1 on differences
25
+ - pyEMU bridge: `to_pyemu()` / `from_pyemu()` via a temporary control file (`pyemu` extra)
26
+ - provenance: every producing command writes `<output>.manifest.json` (version, command
27
+ line, source hashes and sheets, output hash and counts)
28
+ - `parrep`: `.par` or IES realization into a control file (or a workbook with a `BUILD` sheet),
29
+ with `--set NAME=VALUE`
30
+ - Fixes over the old script: `jacupdate` was never written; values were truncated to 6
31
+ significant digits; `phistopthresh` could not be set; SVD / regularisation values in the
32
+ CONTROL sheet were ignored; prior information in estimation mode produced an invalid file
@@ -0,0 +1,19 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use makepst, please cite it as below."
3
+ title: "makepst: PEST control files from spreadsheet tables, and back"
4
+ type: software
5
+ authors:
6
+ - family-names: Ou
7
+ given-names: Michael
8
+ email: ougengxin@gmail.com
9
+ version: 0.1.0
10
+ date-released: 2026-09-19
11
+ repository-code: "https://github.com/ougx/makePst"
12
+ license: MIT
13
+ keywords:
14
+ - PEST
15
+ - PEST++
16
+ - PEST_HP
17
+ - groundwater
18
+ - calibration
19
+ - Excel
makepst-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Ou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include LICENSE README.md CHANGELOG.md CITATION.cff
2
+ recursive-include tests *.py *.xlsx *.pst *.par *.res *.csv
3
+ recursive-include examples *.csv *.py *.tpl *.ins *.in *.out *.par *.res
4
+ global-exclude __pycache__ *.py[cod] *.manifest.json
makepst-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,514 @@
1
+ Metadata-Version: 2.4
2
+ Name: makepst
3
+ Version: 0.1.0
4
+ Summary: Build PEST / PEST++ / PEST_HP control files from spreadsheet tables, read them back, and push results into the workbook
5
+ Author-email: Michael Ou <ougengxin@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ougx/makePst
8
+ Project-URL: Repository, https://github.com/ougx/makePst
9
+ Project-URL: Issues, https://github.com/ougx/makePst/issues
10
+ Keywords: PEST,PEST++,PEST_HP,groundwater,calibration,MODFLOW,Excel
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Scientific/Engineering :: Hydrology
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: numpy
22
+ Requires-Dist: pandas>=2.0
23
+ Requires-Dist: openpyxl>=3.1
24
+ Provides-Extra: excel
25
+ Requires-Dist: xlwings; extra == "excel"
26
+ Provides-Extra: pyemu
27
+ Requires-Dist: pyemu; extra == "pyemu"
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest; extra == "test"
30
+ Dynamic: license-file
31
+
32
+ # makePst
33
+
34
+ [![tests](https://github.com/ougx/makePst/actions/workflows/tests.yml/badge.svg)](https://github.com/ougx/makePst/actions/workflows/tests.yml)
35
+
36
+ *makePst: A Reproducible and Auditable Spreadsheet Workflow for PEST Model Calibration*
37
+
38
+ `makePst` builds PEST / PEST_HP / PEST++ control files (`.pst`) from Excel or CSV tables,
39
+ reads control files back into editable workbooks, and returns calibration results
40
+ (`.par`, `.res`/`.rei`, PESTPP-IES ensembles) into the workbook you already maintain.
41
+
42
+ ```
43
+ (nothing) -> starter workbook makepst init
44
+ .pst or workbook + tpl/ins files -> pestchek-style report makepst validate
45
+ Excel / CSV tables -> .pst makepst build
46
+ .pst -> editable workbook makepst dump
47
+ .par / .res / .rei / IES ensemble -> existing workbook makepst update
48
+ .par or IES realization (+ tweaks) -> new .pst makepst parrep
49
+ two .pst / workbooks -> what changed makepst diff
50
+ ```
51
+
52
+ See [What round-tripping preserves](#what-round-tripping-preserves) for the exact guarantees
53
+ and the [compatibility table](#compatibility) for which PEST dialects and sections are covered.
54
+
55
+ ## Why makePst?
56
+
57
+ A PEST setup of a few thousand parameters and tens of thousands of observations is easier to
58
+ review in a spreadsheet than in a 40,000-line text file. In practice most calibration teams
59
+ already keep one: the workbook holds the parameter tables per group, weights computed by
60
+ formulas, helper columns (layer, pilot-point index, native values), and a run history. The
61
+ control file is derived from it.
62
+
63
+ `makePst` makes that derivation deterministic and safe:
64
+
65
+ - **The workbook is the source of truth.** The control file is rebuilt from it with one
66
+ command; the build command itself can live in the workbook (`BUILD` sheet).
67
+ - **Counts are computed, never typed.** `npar`, `nobs`, `npargp`, `nprior`, `nobsgp`,
68
+ `ntplfle`, `ninsfle` always match the tables.
69
+ - **Validation before PEST sees the file.** Duplicate names, undefined parameter groups,
70
+ tie targets that don't exist, regularisation equations that reference fixed or tied
71
+ parameters, initial values outside bounds — reported by name, not as a PEST run-time error.
72
+ - **Results flow back.** Best parameters, residuals or an IES realization are written into
73
+ the existing workbook by name, across however many sheets the tables are split over,
74
+ without touching formulas, macros or helper columns.
75
+ - **PEST_HP tokens** (`win_mrun_hours=`, `uptestmin=`, `absparmax(1)=…`) and `++` PEST++
76
+ options are first-class, so no post-editing of the generated file.
77
+ - **Fits existing workflows.** The `build` command line is the same one the original
78
+ single-file script accepted; batch files keep working.
79
+
80
+ ## Install
81
+
82
+ ```
83
+ pip install makepst # adds the makepst command
84
+ pip install "makepst[excel]" # + xlwings, for Excel-faithful workbook updates (see update)
85
+ pip install "makepst[pyemu]" # + pyEMU, for to_pyemu / from_pyemu (see the bridge)
86
+ ```
87
+
88
+ The development version: `pip install git+https://github.com/ougx/makePst`.
89
+
90
+ Requires Python ≥ 3.9, pandas ≥ 2.0, numpy, openpyxl ≥ 3.1. Installing adds the `makepst`
91
+ command; `python -m makepst` is equivalent, and `python makepst.py` works from a checkout
92
+ without installing.
93
+
94
+ ## Starting from nothing
95
+
96
+ ```
97
+ makepst init project.xlsx
98
+ ```
99
+
100
+ writes a workbook that already builds a valid control file: every sheet with the headers
101
+ `build` expects and a few example rows, a `CONTROL` sheet listing every PEST variable with its
102
+ default and a one-line description, drop-down lists where PEST allows only fixed words
103
+ (`PARTRANS`, `INCTYP`, `FORCEN`, `DERMTHD`, `IO!TYPE`, the on/off control variables), header
104
+ comments explaining each column, frozen header rows, and a `BUILD` sheet with the build
105
+ command. Replace the example rows with your own and run that command.
106
+
107
+ ## Two-minute tutorial
108
+
109
+ `examples/minimal/` holds a four-parameter, four-observation setup as CSV tables, its
110
+ template / instruction files, a toy `model.py`, and a fake `model.par` and `model.res` (this
111
+ is also run by the test suite):
112
+
113
+ ```bash
114
+ cd examples/minimal # on Windows cmd, replace the trailing \ with ^
115
+
116
+ # 1. tables -> control file (also writes dump.tpl, a template that echoes every parameter)
117
+ makepst build model.pst regul --set_ctl_csv control.csv --add_pargp_csv pargp.csv \
118
+ --add_par_csv par.csv --add_obs_csv obs.csv --add_io_csv io.csv --add_comment "minimal example"
119
+
120
+ # 2. check it against the template / instruction / output files, like pestchek
121
+ makepst validate model.pst --outputs
122
+
123
+ # 3. control file -> workbook (CONTROL / PARGP / PAR / OBS / PRIOR / IO sheets + a BUILD sheet)
124
+ makepst dump model.pst model.xlsx
125
+
126
+ # 4. results -> a copy of the workbook: PARVAL1 from .par, MODELLED / RESIDUAL from .res
127
+ makepst update model.xlsx --par model.par --res model.res --out model-results.xlsx
128
+
129
+ # 5. best parameters -> a new control file for a final run
130
+ makepst parrep model.pst model.par model-final.pst --set noptmax=0
131
+ ```
132
+
133
+ Open `model.pst` to see what the tables became: `hk3` is tied to `hk1` (the `TIETO` column),
134
+ `hk1` and `hk2` got regularisation equations from their `PRIOR`/`WEIGHT` columns
135
+ (`log(hk1) = log10(25)`, `log(hk2) − log(hk1) = 0`), `lamforgive` and `maxsing 3` came from
136
+ `control.csv`, and every count on the control-data lines was computed.
137
+
138
+ ## Commands
139
+
140
+ ### init — starter workbook
141
+
142
+ ```
143
+ makepst init project.xlsx [--name case] [--force]
144
+ ```
145
+
146
+ See [Starting from nothing](#starting-from-nothing). `--name` sets the control-file name in
147
+ the `BUILD` command (default: the workbook's name). Existing files are not overwritten
148
+ without `--force`.
149
+
150
+ ### validate — pestchek-style checks
151
+
152
+ ```
153
+ makepst validate model.pst --outputs
154
+ makepst validate book.xlsm --strict
155
+ ```
156
+
157
+ See [Validation](#validation). Template and instruction paths are resolved relative to the
158
+ control file (or workbook); `--base_dir` overrides that. `--strict` also fails on warnings,
159
+ `--quiet` hides the informational lines.
160
+
161
+ ### build — tables → .pst
162
+
163
+ ```
164
+ makepst build out.pst [estimation|regularisation|prediction|pareto]
165
+ --set_ctl_xls book.xlsm,CONTROL (or --set_ctl_csv file.csv)
166
+ --add_pargp_xls book.xlsm,PARGP
167
+ --add_par_xls book.xlsm,PAR_* (repeatable; sheet names may be globs)
168
+ --add_obs_xls book.xlsm,OBS_*
169
+ --add_io_xls book.xlsm,IO --add_pp_xls book.xlsm,PPcntl
170
+ [--add_tied_xls ...] [--add_prior_xls ...] [--add_comment "text"] [--add_comment_xls ...]
171
+ [--fill_parval run.par [--real NAME]] [--ss] [--no_dump_tpl] [--v2]
172
+ ```
173
+
174
+ Each `--add_*` takes `book,SHEET` (`_xls`) or a csv path (`_csv`) and may be repeated;
175
+ tables split across sheets are concatenated. The sheet name may be a glob:
176
+ `--add_par_xls tr13.xlsm,PAR_*` adds every `PAR_…` sheet in workbook order. Headers are
177
+ case-insensitive. `makepst build book.xlsm [--out x.pst]` runs the command stored in the
178
+ workbook's `BUILD` sheet instead. The mode argument
179
+ overrides `pestmode` in the CONTROL sheet; adding prior information switches to regularisation.
180
+ `--ss` drops `ss*`/`sy*` parameters and groups (steady-state runs of a transient setup).
181
+
182
+ | table | columns |
183
+ |---------|---------|
184
+ | CONTROL | `NAME`, `VALUE` in the first four columns (`LINE`, `NAME`, `DEFAULT`, `VALUE`); blank values use the built-in default. Any variable of the control data, SVD, LSQR, AUI, SVD-assist or regularisation sections: `noptmax`, `jacupdate`, `lamforgive`, `win_mrun_hours`, `absparmax(1)=0.1 absparmax(2)=20`, `svdmode`, `phimlim`, … Counts are ignored (computed). |
185
+ | PARGP | `PARGPNME INCTYP DERINC DERINCLB FORCEN DERINCMUL DERMTHD` (+ optional `SPLITTHRESH SPLITRELDIFF SPLITACTION`) |
186
+ | PAR | `PARNME PARTRANS PARCHGLIM PARVAL1 PARLBND PARUBND PARGP SCALE OFFSET DERCOM`; optional `TIETO`, and `PRIOR` / `WEIGHT` (regularisation, below). Other columns are ignored, so helper columns are fine. |
187
+ | TIED | two columns: parameter, parameter it is tied to (alternative to a `TIETO` column) |
188
+ | OBS | `OBSNME OBSVAL WEIGHT OBGNME` |
189
+ | OBSGP | `OBGNME`, optional `COVFILE` — observation-group order and covariance matrix files (rarely needed) |
190
+ | PRIOR | `PINME EQ WEIGHT OBGNME` — explicit prior-information equations |
191
+ | IO | `TYPE` (`cmd` / `tpl` / `ins`), `IN`, `OUT` |
192
+ | PP | first two columns: PEST++ option name, value → `++name(value)` |
193
+
194
+ **Regularisation from the parameter table.** In regularisation mode each adjustable
195
+ parameter with a `PRIOR` and a positive `WEIGHT` gets an equation: a numeric `PRIOR` gives
196
+ `1.0 * log(p) = log10(v)` (or `1.0 * p = v` for untransformed parameters); a `PRIOR` naming
197
+ another parameter gives `1.0 * log(p) − 1.0 * log(q) = 0`. The group is `regul` + parameter
198
+ group, truncated to 12 characters.
199
+
200
+ ### dump — .pst → workbook
201
+
202
+ ```
203
+ makepst dump run.pst run.xlsx [--split]
204
+ ```
205
+
206
+ Writes `CONTROL`, `PARGP`, `PAR` (with `TIETO`), `OBS`, `PRIOR`, `IO`, `PP`, `NOTES` (the
207
+ header comments) and `BUILD` — the command that rebuilds the control file from this
208
+ workbook. `--split` writes one `PAR_<group>` and `OBS_<group>` sheet per group instead of
209
+ `PAR` and `OBS`. Use it to bring an inherited control file into the spreadsheet workflow, or
210
+ to recover a workbook that was lost.
211
+
212
+ ### update — results → existing workbook
213
+
214
+ ```
215
+ makepst update book.xlsm --par run.par # PARVAL1
216
+ makepst update book.xlsm --res run.res # adds MODELLED / RESIDUAL
217
+ makepst update book.xlsm --pst run.pst --par_cols PARVAL1,PARTRANS,PARLBND,PARUBND
218
+ makepst update book.xlsm --par run.3.par.csv --real best # PESTPP-IES ensemble
219
+ makepst update book.xlsm --obs_csv run.3.obs.csv --pst run.pst # IES simulated values
220
+ [--sheet "PAR_*"] [--group hk,sy] [--overwrite_formulas] [--out copy.xlsm] [--backend openpyxl|xlwings]
221
+ ```
222
+
223
+ > **`update` saves in place unless `--out` is given.** Use `--out` the first time, and keep
224
+ > the workbook closed in Excel while it runs.
225
+
226
+ Every worksheet whose header row contains `PARNME` (or `OBSNME`) is matched row by row on the
227
+ name, so tables spread over several sheets are all found; only the requested columns are
228
+ written and only for names present in the source. Cells that hold formulas are left alone
229
+ unless `--overwrite_formulas`. `--sheet` (glob, case-insensitive) restricts the worksheets;
230
+ `--group` restricts the parameter / observation groups (a `.par` file carries no groups, so
231
+ add `--pst` to supply them).
232
+
233
+ PESTPP-IES ensembles (`case.N.par.csv`, `case.N.obs.csv`) are recognised by their
234
+ `real_name` header. `--real` picks the realization: a name (`base`, `17`) or `best` — the
235
+ lowest phi for that iteration in `case.phi.actual.csv` next to the file. Default `base`. An
236
+ observation ensemble writes `MODELLED`; with `--pst` also `RESIDUAL` (measured − modelled).
237
+
238
+ With residuals (`--res`, or `--obs_csv` plus `--pst`) `update` also writes a **`PHI`** sheet —
239
+ the objective function by observation group as PEST reports it at the end of a run: number of
240
+ observations and of weighted ones, phi (Σ(w·r)²) and its fraction of the total, RMS / mean /
241
+ max-abs residual of the weighted observations in their own units, and the worst observation,
242
+ with a `TOTAL` row. For an IES ensemble with `case.phi.actual.csv` beside it, a **`PHI_IES`**
243
+ sheet lists every realization's phi for that iteration, sorted, with mean / std / min / max.
244
+ Both sheets are rewritten on each run (`--no_phi` to skip).
245
+
246
+ Two backends: **xlwings** (used when installed; drives Excel invisibly, so everything in the
247
+ workbook is preserved and formulas recalculate; Windows/macOS with Excel) or **openpyxl**
248
+ (no Excel needed; keeps VBA macros; drops charts and images; formula results are stale until
249
+ Excel next opens the file). The backend used is printed.
250
+
251
+ ### parrep — .par values → new .pst
252
+
253
+ ```
254
+ makepst parrep run.pst run.par next.pst [--set noptmax=0] [--set NAME=VALUE ...] [--v1|--v2]
255
+ makepst parrep run.pst run.3.par.csv next.pst --real best
256
+ makepst parrep book.xlsm run.par next.pst # base is the workbook
257
+ ```
258
+
259
+ Like PEST's PARREP: `PARVAL1` is replaced from a `.par` file or an IES realization and a new
260
+ control file is written; `--set` changes control values on the way. All supported content of
261
+ the input control file is preserved semantically (see below); adjustable parameters that end
262
+ up outside their bounds are reported.
263
+
264
+ With a **workbook** as the base, the control file is first built from the command in the
265
+ workbook's `BUILD` sheet (`dump` writes one; for a hand-made workbook paste the build command
266
+ into column A, one option per row). File names in it that match the workbook's own name refer
267
+ to that workbook; other relative names resolve against the workbook's folder. Note that this
268
+ route rebuilds from the workbook's *current* state, so any edits made since the original
269
+ control file was written are picked up too.
270
+
271
+ ### diff — what changed
272
+
273
+ ```
274
+ makepst diff tr12.pst tr13.pst [--xlsx changes.xlsx] [--rtol 1e-9] [--max_rows 50]
275
+ makepst diff tr13.xlsm tr13.pst # is the control file still what the workbook says?
276
+ ```
277
+
278
+ Compares the tables, not the text: parameters and observations added, removed or changed
279
+ (column by column), prior-information equations, parameter groups, effective control values
280
+ (defaults filled in, so a file and a workbook compare fairly), `++` options, template /
281
+ instruction pairs, command lines and header comments. Numbers are compared with a relative
282
+ tolerance, so `49.7377` vs `49.73775` shows up at the default `1e-9` and disappears at
283
+ `--rtol 1e-5`. Prints a table per section and a one-line summary; `--xlsx` writes the same
284
+ tables to a workbook for review. Exits 1 when there are differences, like `diff`.
285
+
286
+ ## What round-tripping preserves
287
+
288
+ `dump` → `build` and `read_pst` → `write_pst` are **semantically lossless for supported
289
+ content**, not byte-identical: `read → write → read` is an identity, and `dump` → `build`
290
+ reproduces the same text as `read → write`. Concretely:
291
+
292
+ Preserved: every parameter, group, observation, prior-information equation, tied pair,
293
+ template/instruction pair, command line, `++` option, header comment, control-data value
294
+ (including PEST_HP keyed tokens), SVD / LSQR / AUI / SVD-assist / regularisation values, and
295
+ the verbatim text of `* sensitivity reuse`, `* derivatives command line`, `* predictive
296
+ analysis` and `* pareto`.
297
+
298
+ Normalised: parameter, observation and group names are lower-cased (PEST is
299
+ case-insensitive); numbers are written with 11 significant digits; whitespace and column
300
+ alignment are makePst's own; comments *inside* sections are dropped; observation groups are
301
+ listed in first-use order (or the order of the source file when reading a `.pst`).
302
+
303
+ Also preserved: covariance-file references in `* observation groups` (an `OBSGP` sheet on
304
+ `dump`), and the control-file **format version**: a PEST++ version-2 file (`pcf version=2`,
305
+ `* control data keyword`, `* … external` csv tables) reads into the same tables and is
306
+ written back as version 2 unless `--v1` is given; `--v2` writes any control file in that
307
+ format, with `case.par_data.csv` / `obs_data` / `pargp_data` / `prior_data` beside it. Both
308
+ versions of the same content are identical after reading (`makepst diff` says so).
309
+
310
+ Not preserved: any unrecognised section (dropped with a warning naming it).
311
+
312
+ ## Compatibility
313
+
314
+ | | |
315
+ |---|---|
316
+ | PEST dialects | PEST, PEST_HP (`win_mrun_hours=`, `uptestmin=`, `uptestlim=`, `absparmax(n)=`), PEST++ (`++name(value)` options) |
317
+ | Control-file sections | control data, singular value decomposition, lsqr, automatic user intervention, svd assist, parameter groups, parameter data (incl. tied pairs), observation groups, observation data, model command line, model input/output, prior information (with `&` continuation lines), regularisation. Kept verbatim: sensitivity reuse, derivatives command line, predictive analysis, pareto. |
318
+ | File formats | classic (version 1) and PEST++ version 2 (`pcf version=2`, `* control data keyword` with control variables and `++` options, `* … external` csv tables with `sep=` / `missing_values=`, `partied` column); observation covariance files in `* observation groups` |
319
+ | Not supported | unknown sections — dropped with a warning |
320
+ | Result files | `.par`, `.res` / `.rei`, PESTPP-IES `case.N.par.csv` / `case.N.obs.csv` / `case.phi.actual.csv` |
321
+ | Table inputs | `.xlsx` / `.xlsm` sheets (openpyxl), `.csv` |
322
+ | Workbook update | openpyxl backend on any platform (macros kept, charts/images dropped, no recalculation); xlwings backend on Windows/macOS with Excel (everything kept, recalculated) |
323
+ | Platforms | CI runs the suite on Ubuntu 24.04 and Windows Server 2022 for Python 3.9, 3.11 and 3.13. macOS is expected to work but is not exercised in CI. |
324
+ | Python | ≥ 3.9; pandas ≥ 2.0 (tested with 2.3 and 3.0), numpy, openpyxl ≥ 3.1; optional xlwings, pyemu, pytest |
325
+
326
+ ## Validation
327
+
328
+ Two layers. `Pst.validate()` runs before every write (`build`, `dump`, `parrep`, `write_pst`)
329
+ and enforces **internal consistency** of the tables, changing what it can and refusing what it
330
+ cannot:
331
+
332
+ | check | outcome |
333
+ |---|---|
334
+ | no parameters / no observations | error |
335
+ | duplicate parameter, observation or prior-information names | error |
336
+ | parameter group used but not defined | error |
337
+ | tied parameter whose target is missing, or tied to a tied parameter | error |
338
+ | tied to a fixed parameter | parameter becomes fixed (message) |
339
+ | parameter group defined but unused | dropped from the file |
340
+ | prior equation referencing a fixed, tied or missing parameter | dropped (message) |
341
+ | adjustable `PARVAL1` outside `[PARLBND, PARUBND]` | warning |
342
+ | control value that is computed (`npar` …) or unknown | ignored (warning) |
343
+
344
+ `makepst validate` is the **pestchek-style report**: it changes nothing, looks beyond the
345
+ tables at the files the control file points to, and exits 1 on errors so a batch file can
346
+ stop:
347
+
348
+ ```
349
+ makepst validate model.pst [--outputs] [--strict] [--quiet] [--base_dir DIR]
350
+ makepst validate tr13.xlsm # a workbook with a BUILD sheet is built first, in memory
351
+ ```
352
+
353
+ | check | severity |
354
+ |---|---|
355
+ | everything in the table above, reported rather than fixed | error / warning |
356
+ | `PARTRANS` not log/none/fixed/tied; non-numeric values; `PARLBND` > `PARUBND`; log parameter with a non-positive lower bound | error |
357
+ | negative observation weight; all weights zero | error / warning |
358
+ | malformed prior equation; non-numeric right-hand side; `log()` used on a non-log parameter or vice versa | error |
359
+ | `DERCOM` beyond the number of model command lines | error |
360
+ | name longer than PEST's limit (12 parameter / 20 observation / 12 group; PEST++ allows 200) | warning |
361
+ | section in the `.pst` that makePst drops on read | warning |
362
+ | template or instruction file missing; bad `ptf` / `pif` line | error |
363
+ | parameter cited in no template; template citing an unknown parameter | error |
364
+ | observation read by no instruction file, by two files, or twice in one; unknown observation in an instruction file | error |
365
+ | model command / model-input folder not found next to the control file | warning |
366
+ | `--outputs`: each instruction file run against its model output file when present | note (success) / warning (failure) |
367
+
368
+ The `--outputs` interpreter follows the PEST manual (primary and secondary markers, `l`, `w`,
369
+ `t`, `!name!`, `[name]c1:c2`, `(name)c1:c2`, `dum`, `&` continuation) and catches the classic
370
+ mistakes — a `w` too few so a label is read instead of a number, a marker that never appears
371
+ — but it is not PEST. Keep running PEST's own checker before a long run:
372
+
373
+ ```
374
+ pestchek model # PEST / PEST_HP
375
+ pestpp-glm model.pst # PEST++ reports problems on start-up
376
+ ```
377
+
378
+ ## Audit trail: the manifest
379
+
380
+ `build`, `parrep`, `dump` and `update` each write a sidecar `<output>.manifest.json` next to
381
+ what they produce (`--no_manifest` to skip). It answers "where did this control file come
382
+ from?" after the fact:
383
+
384
+ ```json
385
+ {
386
+ "makepst": "0.1.0",
387
+ "command": "build",
388
+ "argv": ["build", "tr13.pst", "regul", "--set_ctl_xls", "tr13.xlsm,CONTROL", "..."],
389
+ "created": "2026-09-19T13:51:39-04:00",
390
+ "user": "hydro", "host": "OU13700", "cwd": "D:\\...\\0023-makePst",
391
+ "python": "3.12.12", "pandas": "3.0.5", "platform": "Windows-11-10.0.26200-SP0",
392
+ "sources": [
393
+ {"path": "D:\\...\\tr13.xlsm", "sha256": "9d641a2f...", "size": 3089317,
394
+ "modified": "2026-07-12T03:32:10-04:00", "role": "control",
395
+ "sheets": ["CONTROL", "PARGP", "PAR_HK", "PAR_VK", "...", "PPglm"]}
396
+ ],
397
+ "output": {"path": "D:\\...\\tr13.pst", "sha256": "d93f317d...", "size": 2163331,
398
+ "npar": 1068, "nobs": 41747, "npargp": 12, "nprior": 185, "nobsgp": 12,
399
+ "ntplfle": 18, "ninsfle": 7, "pestmode": "regularisation"}
400
+ }
401
+ ```
402
+
403
+ Every input file appears once with its SHA-256, size, modification time, role and the sheets
404
+ read from it; the output carries its own hash and the section counts. `parrep` adds the
405
+ realization and `--set` values (and, from a workbook, the resolved build arguments);
406
+ `update` records which sheets were written, how many rows each, how many formula cells were
407
+ left alone, and the backend. A hash mismatch between a manifest and the workbook on disk is
408
+ the signal that the control file no longer corresponds to the spreadsheet.
409
+
410
+ ## Relationship to pyEMU
411
+
412
+ [pyEMU](https://github.com/pypest/pyemu) is the broader toolkit: it constructs PEST++
413
+ interfaces programmatically (`PstFrom`), runs linear and ensemble-based uncertainty
414
+ analysis, and provides geostatistics. `makePst` does one thing pyEMU does not: it treats a
415
+ spreadsheet the modeler already maintains as the source of a control file and keeps the two
416
+ in sync in both directions, including PEST_HP keywords that pyEMU's control-data model does
417
+ not carry. They are complementary — a control file written by `makePst` loads in pyEMU, and
418
+ pyEMU's outputs (`.par`, `.res`, ensembles) load into `makepst update`.
419
+
420
+ There is a direct bridge (the `pyemu` extra):
421
+
422
+ ```python
423
+ from makepst import read_pst, to_pyemu, from_pyemu
424
+
425
+ ppst = to_pyemu(read_pst('tr13.pst')) # a pyemu.Pst: use pyEMU's Schur, ensembles, plotting, ...
426
+ pst = from_pyemu(ppst) # back to a makePst Pst, e.g. to dump into a workbook
427
+ ```
428
+
429
+ Both directions go through a temporary classic control file, so they depend only on the file
430
+ format the two agree on. The tables, `++` options and standard control values survive the
431
+ round trip; what does not is exactly what pyEMU's control-data model lacks — PEST_HP keyed
432
+ tokens (`absparmax(n)=`, `uptestmin=`, `win_mrun_hours=`), header comments, and the
433
+ regularisation section, which pyEMU rewrites with its own defaults. `makepst diff` shows the
434
+ difference, and the test suite pins it.
435
+
436
+ ## Python API
437
+
438
+ ```python
439
+ from makepst import Pst, read_pst, write_pst, to_workbook, update_workbook, load_table, read_par
440
+
441
+ pst = read_pst('run.pst') # Pst: .par/.obs/.pargp/.prior DataFrames, .control dict, ...
442
+ pst.fill_parval('run.3.par.csv', real='best')
443
+ pst.set_control({'noptmax': 0})
444
+ write_pst(pst, 'run_final.pst', dump_tpl=False)
445
+
446
+ to_workbook(pst, 'run.xlsx', split=True)
447
+ update_workbook('book.xlsm', par='run.par', res='run.res', out='book_results.xlsm',
448
+ sheets=['PAR_*'], groups=['hk'])
449
+ ```
450
+
451
+ ## Testing
452
+
453
+ ```
454
+ python -m pytest tests -q
455
+ ```
456
+
457
+ 95 tests, no external data needed: a synthetic workbook in the real project layout
458
+ (`tests/data/demo.xlsx`, generated by `tests/make_fixture.py`) with a golden control file,
459
+ `.par`, `.res` and IES ensemble files. They cover golden-file builds from Excel and CSV, the
460
+ `read → write → read` identity, `dump → build` byte equality, workbook updates (multi-sheet
461
+ matching, formula protection, sheet/group filters, ensembles), `parrep` from `.par`, ensembles
462
+ and workbooks, the `init` starter workbook, `validate` (table checks, template / instruction
463
+ cross-checks, the instruction interpreter), the control-section parser, validation errors,
464
+ and the README tutorial above.
465
+ Two extra tests run against a real 1,000-parameter / 40,000-observation project when its
466
+ files are present locally, and two pyEMU bridge tests run when pyemu is installed. CI runs
467
+ everything on Linux and Windows.
468
+
469
+ ## Project layout
470
+
471
+ ```
472
+ makepst.py entry point for a checkout (same arguments as `makepst`)
473
+ makepst/
474
+ sections.py control-style sections: field order, defaults, PEST_HP keyed tokens; one table drives render and parse
475
+ pst.py Pst data model and validate(); .par / .res / ensemble readers
476
+ writer.py Pst -> .pst text (+ dump.tpl)
477
+ reader.py .pst text -> Pst
478
+ excel.py sheets -> Pst; Pst -> workbook; results -> existing workbook
479
+ cli.py init / validate / build / dump / update / parrep / diff
480
+ diff.py semantic comparison of two Pst objects
481
+ phi.py objective function by group from residuals; IES realization phis
482
+ pyemu_bridge.py to_pyemu / from_pyemu
483
+ checks.py the validate report: table checks, template / instruction cross-checks, instruction interpreter
484
+ provenance.py the <output>.manifest.json sidecar
485
+ starter.py the `init` workbook: control-variable descriptions, example rows, drop-downs
486
+ examples/minimal/ the tutorial inputs
487
+ tests/ suite + fixture generator
488
+ ```
489
+
490
+ ## Releasing
491
+
492
+ Releases are published to PyPI by GitHub Actions through trusted publishing, so no token is
493
+ stored anywhere. To release: bump the version in `pyproject.toml` and the fallback in
494
+ `makepst/__init__.py` (a test keeps them equal), add a `CHANGELOG.md` entry, commit, then
495
+
496
+ ```
497
+ git tag v0.2.0 && git push origin main v0.2.0
498
+ ```
499
+
500
+ The `publish` workflow checks that the tag matches the package version, runs the tests, builds
501
+ the sdist and wheel, uploads them to PyPI, and attaches them to a GitHub release. PyPI never
502
+ accepts the same version twice, so a mistake means a new version, not a re-upload.
503
+
504
+ ## Contributing, issues, citation
505
+
506
+ Bug reports and feature requests: [GitHub issues](https://github.com/ougx/makePst/issues).
507
+ A control file that `makePst` misreads, plus the command used, is the most useful report.
508
+ Pull requests should keep `python -m pytest tests -q` green and regenerate the fixture with
509
+ `python tests/make_fixture.py` when the writer's output changes.
510
+
511
+ Changes are listed in [CHANGELOG.md](CHANGELOG.md). To cite, use [CITATION.cff](CITATION.cff)
512
+ (GitHub's "Cite this repository" button).
513
+
514
+ License: MIT.