makepst 0.2.0__tar.gz → 0.3.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 (70) hide show
  1. {makepst-0.2.0 → makepst-0.3.0}/CHANGELOG.md +16 -0
  2. {makepst-0.2.0 → makepst-0.3.0}/CITATION.cff +2 -2
  3. {makepst-0.2.0 → makepst-0.3.0}/PKG-INFO +80 -9
  4. {makepst-0.2.0 → makepst-0.3.0}/README.md +79 -8
  5. {makepst-0.2.0 → makepst-0.3.0}/makepst/__init__.py +3 -2
  6. {makepst-0.2.0 → makepst-0.3.0}/makepst/cli.py +124 -7
  7. {makepst-0.2.0 → makepst-0.3.0}/makepst/excel.py +41 -10
  8. makepst-0.3.0/makepst/hpstart.py +64 -0
  9. makepst-0.3.0/makepst/provenance.py +251 -0
  10. {makepst-0.2.0 → makepst-0.3.0}/makepst.egg-info/PKG-INFO +80 -9
  11. {makepst-0.2.0 → makepst-0.3.0}/makepst.egg-info/SOURCES.txt +4 -0
  12. {makepst-0.2.0 → makepst-0.3.0}/pyproject.toml +1 -1
  13. makepst-0.3.0/tests/test_convenience.py +60 -0
  14. makepst-0.3.0/tests/test_hpstart.py +68 -0
  15. makepst-0.3.0/tests/test_log_bundle.py +98 -0
  16. {makepst-0.2.0 → makepst-0.3.0}/tests/test_provenance.py +1 -1
  17. makepst-0.2.0/makepst/provenance.py +0 -89
  18. {makepst-0.2.0 → makepst-0.3.0}/LICENSE +0 -0
  19. {makepst-0.2.0 → makepst-0.3.0}/MANIFEST.in +0 -0
  20. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/control.csv +0 -0
  21. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/flow.ins +0 -0
  22. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/flow.out +0 -0
  23. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/heads.ins +0 -0
  24. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/heads.out +0 -0
  25. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/io.csv +0 -0
  26. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/model.in +0 -0
  27. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/model.par +0 -0
  28. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/model.py +0 -0
  29. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/model.res +0 -0
  30. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/model.tpl +0 -0
  31. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/obs.csv +0 -0
  32. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/par.csv +0 -0
  33. {makepst-0.2.0 → makepst-0.3.0}/examples/minimal/pargp.csv +0 -0
  34. {makepst-0.2.0 → makepst-0.3.0}/makepst/__main__.py +0 -0
  35. {makepst-0.2.0 → makepst-0.3.0}/makepst/checks.py +0 -0
  36. {makepst-0.2.0 → makepst-0.3.0}/makepst/diff.py +0 -0
  37. {makepst-0.2.0 → makepst-0.3.0}/makepst/model_files.py +0 -0
  38. {makepst-0.2.0 → makepst-0.3.0}/makepst/phi.py +0 -0
  39. {makepst-0.2.0 → makepst-0.3.0}/makepst/pst.py +0 -0
  40. {makepst-0.2.0 → makepst-0.3.0}/makepst/pyemu_bridge.py +0 -0
  41. {makepst-0.2.0 → makepst-0.3.0}/makepst/reader.py +0 -0
  42. {makepst-0.2.0 → makepst-0.3.0}/makepst/rules.py +0 -0
  43. {makepst-0.2.0 → makepst-0.3.0}/makepst/sections.py +0 -0
  44. {makepst-0.2.0 → makepst-0.3.0}/makepst/starter.py +0 -0
  45. {makepst-0.2.0 → makepst-0.3.0}/makepst/writer.py +0 -0
  46. {makepst-0.2.0 → makepst-0.3.0}/makepst.egg-info/dependency_links.txt +0 -0
  47. {makepst-0.2.0 → makepst-0.3.0}/makepst.egg-info/entry_points.txt +0 -0
  48. {makepst-0.2.0 → makepst-0.3.0}/makepst.egg-info/requires.txt +0 -0
  49. {makepst-0.2.0 → makepst-0.3.0}/makepst.egg-info/top_level.txt +0 -0
  50. {makepst-0.2.0 → makepst-0.3.0}/setup.cfg +0 -0
  51. {makepst-0.2.0 → makepst-0.3.0}/tests/data/demo.3.obs.csv +0 -0
  52. {makepst-0.2.0 → makepst-0.3.0}/tests/data/demo.3.par.csv +0 -0
  53. {makepst-0.2.0 → makepst-0.3.0}/tests/data/demo.par +0 -0
  54. {makepst-0.2.0 → makepst-0.3.0}/tests/data/demo.phi.actual.csv +0 -0
  55. {makepst-0.2.0 → makepst-0.3.0}/tests/data/demo.pst +0 -0
  56. {makepst-0.2.0 → makepst-0.3.0}/tests/data/demo.res +0 -0
  57. {makepst-0.2.0 → makepst-0.3.0}/tests/data/demo.xlsx +0 -0
  58. {makepst-0.2.0 → makepst-0.3.0}/tests/make_fixture.py +0 -0
  59. {makepst-0.2.0 → makepst-0.3.0}/tests/test_checks.py +0 -0
  60. {makepst-0.2.0 → makepst-0.3.0}/tests/test_diff.py +0 -0
  61. {makepst-0.2.0 → makepst-0.3.0}/tests/test_examples.py +0 -0
  62. {makepst-0.2.0 → makepst-0.3.0}/tests/test_fixture.py +0 -0
  63. {makepst-0.2.0 → makepst-0.3.0}/tests/test_makepst.py +0 -0
  64. {makepst-0.2.0 → makepst-0.3.0}/tests/test_model_files.py +0 -0
  65. {makepst-0.2.0 → makepst-0.3.0}/tests/test_phi.py +0 -0
  66. {makepst-0.2.0 → makepst-0.3.0}/tests/test_pyemu.py +0 -0
  67. {makepst-0.2.0 → makepst-0.3.0}/tests/test_rules.py +0 -0
  68. {makepst-0.2.0 → makepst-0.3.0}/tests/test_starter.py +0 -0
  69. {makepst-0.2.0 → makepst-0.3.0}/tests/test_v2.py +0 -0
  70. {makepst-0.2.0 → makepst-0.3.0}/tests/test_version.py +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-09-21)
4
+
5
+ - `log`: ledger of every manifest under a folder — when, which command, which output from
6
+ which sources, dimensions, version; `--file` filters by file name, path or sha256 prefix
7
+ ("which runs used that version of the workbook?"), `--command`, `--last`, `--check`
8
+ verifies every recorded hash
9
+ - `bundle`: zip a run for reproduction or review — control file, manifest, templates,
10
+ instruction files, model inputs, command-line files; `--sources`, `--outputs`, `--extra`,
11
+ `--list`, `--strict`; the zip gets its own manifest
12
+ - `provenance`: compare the hashes recorded in a manifest with the files on disk
13
+ (`unchanged` / `changed` / `missing`; exit 1 unless all unchanged)
14
+ - `update --dry_run`: preview matched names, target columns and skipped cells without writing
15
+ - `build` warns when a referenced sheet has formulas without cached values (a workbook saved
16
+ by openpyxl and not yet recalculated in Excel)
17
+ - `hpstart`: a PEST_HP `.hp` file with observation values from residuals or an IES realization
18
+
3
19
  ## 0.2.0 (2026-09-19)
4
20
 
5
21
  - `update`: the xlwings backend reads only headers, name and target columns, writes contiguous
@@ -6,8 +6,8 @@ authors:
6
6
  - family-names: Ou
7
7
  given-names: Michael
8
8
  email: ougengxin@gmail.com
9
- version: 0.2.0
10
- date-released: 2026-09-19
9
+ version: 0.3.0
10
+ date-released: 2026-09-21
11
11
  repository-code: "https://github.com/ougx/makePst"
12
12
  license: MIT
13
13
  keywords:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: makepst
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Build PEST / PEST++ / PEST_HP control files from spreadsheet tables, read them back, and push results into the workbook
5
5
  Author-email: Michael Ou <ougengxin@gmail.com>
6
6
  License: MIT
@@ -45,8 +45,11 @@ reads control files back into editable workbooks, and returns calibration result
45
45
  Excel / CSV tables -> .pst makepst build
46
46
  .pst -> editable workbook makepst dump
47
47
  .par / .res / .rei / IES ensemble -> existing workbook makepst update
48
- .par or IES realization (+ tweaks) -> new .pst makepst parrep
48
+ .par or IES realization (+ tweaks) -> new .pst makepst parrep
49
+ .res/.rei or IES obs + existing .hp -> new .hp makepst hpstart
49
50
  two .pst / workbooks -> what changed makepst diff
51
+ manifests in a folder -> run ledger makepst log
52
+ .pst + everything it references -> zip for review makepst bundle
50
53
  ```
51
54
 
52
55
  See [What round-tripping preserves](#what-round-tripping-preserves) for the exact guarantees
@@ -223,6 +226,7 @@ makepst update book.xlsm --res run.res # adds MODELLED
223
226
  makepst update book.xlsm --pst run.pst --par_cols PARVAL1,PARTRANS,PARLBND,PARUBND
224
227
  makepst update book.xlsm --par run.3.par.csv --real best # PESTPP-IES ensemble
225
228
  makepst update book.xlsm --obs_csv run.3.obs.csv --pst run.pst # IES simulated values
229
+ makepst update book.xlsm --par run.par --dry_run # preview; write nothing
226
230
  [--sheet "PAR_*"] [--group hk,sy] [--overwrite_formulas] [--out copy.xlsm] [--backend openpyxl|xlwings]
227
231
  ```
228
232
 
@@ -260,8 +264,13 @@ Both sheets are rewritten on each run (`--no_phi` to skip).
260
264
 
261
265
  Two backends: **xlwings** (used when installed; drives Excel invisibly, so everything in the
262
266
  workbook is preserved and formulas recalculate; Windows/macOS with Excel) or **openpyxl**
263
- (no Excel needed; keeps VBA macros; drops charts and images; formula results are stale until
264
- Excel next opens the file). The backend used is printed.
267
+ (no Excel needed; keeps VBA macros; drops charts and images; does not recalculate formulas).
268
+ Before building from a workbook updated with openpyxl, recalculate and save it in Excel;
269
+ otherwise the build may read missing or stale cached formula values. The backend used is printed.
270
+ `--dry_run` uses openpyxl to preview matched names, target columns, skipped formulas and
271
+ unmatched names without saving a workbook or manifest. It does not test whether Excel will
272
+ refuse individual writes. During `build`, missing cached formula results in referenced
273
+ tables produce a warning; existing cached values may still be stale and cannot be verified.
265
274
 
266
275
  ### parrep — .par values → new .pst
267
276
 
@@ -283,6 +292,26 @@ to that workbook; other relative names resolve against the workbook's folder. No
283
292
  route rebuilds from the workbook's *current* state, so any edits made since the original
284
293
  control file was written are picked up too.
285
294
 
295
+ ### hpstart — modeled observations → new PEST_HP .hp file
296
+
297
+ ```
298
+ makepst hpstart case.pst start.hp --template case.hp --res case.res
299
+ makepst hpstart case.pst start.hp --template case.hp --obs_csv case.3.obs.csv --real best
300
+ makepst hpstart case.pst start.hp --template case.hp --res case.rei --par case.par
301
+ ```
302
+
303
+ By default, parameter values are copied unchanged from the template `.hp`, **not** from
304
+ `PARVAL1` in the `.pst`. Use `--par` to replace them with values from a PEST `.par` file
305
+ or a selected IES parameter realization. The `.pst` supplies parameter and observation
306
+ names and order; the template supplies PEST_HP instruction metadata that a residual or
307
+ IES observation file cannot provide. `hpstart` replaces the template's reference
308
+ modeled-observation array in `.pst` observation order. Every required observation must
309
+ be present with a finite value; extra result rows are ignored. The template must come
310
+ from the same control-file layout and
311
+ observation/parameter order. The binary format stores counts but not names, so matching
312
+ counts alone cannot establish that the template has the right order. The original `.hp`
313
+ is never modified, and the new file receives a provenance manifest by default.
314
+
286
315
  ### tempchek — templates → model input files
287
316
 
288
317
  ```
@@ -333,6 +362,39 @@ tolerance, so `49.7377` vs `49.73775` shows up at the default `1e-9` and disappe
333
362
  `--rtol 1e-5`. Prints a table per section and a one-line summary; `--xlsx` writes the same
334
363
  tables to a workbook for review. Exits 1 when there are differences, like `diff`.
335
364
 
365
+ ### log — the run ledger
366
+
367
+ ```
368
+ makepst log [FOLDER ...] [--file tr13.xlsm] [--command build] [--last 10] [--check] [--no_recursive]
369
+ ```
370
+
371
+ Every manifest under the folders (default: the current one, recursively), oldest first, one
372
+ line each: when, which command, which output, from which sources, the dimensions, the
373
+ makePst version. `--file` keeps the entries whose output or sources include a file — by name,
374
+ path or **sha256 prefix**, so `--file b43fc509` answers "which runs used *that* version of
375
+ the workbook?" even after the file was renamed or overwritten. `--check` adds a column from
376
+ `provenance`: `unchanged` / `changed` / `missing`.
377
+
378
+ ```
379
+ 2026-09-19 21:14 build pest/tr13.pst <- tr13.xlsm [1068 par 41747 obs 159 prior] makepst 0.3.0
380
+ 2026-09-21 08:10 update tr13-xlwings.xlsm <- tr13.xlsm, tr13.par [1068 rows in 8 sheets] makepst 0.3.0
381
+ ```
382
+
383
+ ### bundle — a run, zipped for review
384
+
385
+ ```
386
+ makepst bundle model.pst [run.zip] [--sources] [--outputs] [--extra "bin/**"] [--list] [--strict]
387
+ ```
388
+
389
+ Zips the control file with its manifest, every template and instruction file, the model
390
+ input files the templates write, and any file named on the model command line, with paths
391
+ relative to the run directory so the archive unzips runnable. `--sources` adds the manifest's
392
+ sources (the workbook, the `.par` ...), `--outputs` the model output files, `--extra` any
393
+ glob relative to the run directory (the model binaries, say). Files the control file
394
+ references but that don't exist are listed as `MISSING` (exit 1 with `--strict`); `--list`
395
+ shows what would be bundled without writing. The zip gets its own manifest, so `log` shows
396
+ who bundled what and when.
397
+
336
398
  ## What round-tripping preserves
337
399
 
338
400
  `dump` → `build` and `read_pst` → `write_pst` are **semantically lossless for supported
@@ -439,7 +501,7 @@ from?" after the fact:
439
501
 
440
502
  ```json
441
503
  {
442
- "makepst": "0.2.0",
504
+ "makepst": "0.3.0",
443
505
  "command": "build",
444
506
  "argv": ["build", "tr13.pst", "regul", "--set_ctl_xls", "tr13.xlsm,CONTROL", "..."],
445
507
  "created": "2026-09-19T13:51:39-04:00",
@@ -451,7 +513,7 @@ from?" after the fact:
451
513
  "sheets": ["CONTROL", "PARGP", "PAR_HK", "PAR_VK", "...", "PPglm"]}
452
514
  ],
453
515
  "output": {"path": "D:\\...\\tr13.pst", "sha256": "d93f317d...", "size": 2163331,
454
- "npar": 1068, "nobs": 41747, "npargp": 12, "nprior": 185, "nobsgp": 12,
516
+ "npar": 1068, "nobs": 41747, "npargp": 12, "nprior": 159, "nobsgp": 12,
455
517
  "ntplfle": 18, "ninsfle": 7, "pestmode": "regularisation"}
456
518
  }
457
519
  ```
@@ -463,6 +525,15 @@ realization and `--set` values (and, from a workbook, the resolved build argumen
463
525
  left alone, and the backend. A hash mismatch between a manifest and the workbook on disk is
464
526
  the signal that the control file no longer corresponds to the spreadsheet.
465
527
 
528
+ Run `makepst provenance model.pst` (or pass `model.pst.manifest.json`) to compare every
529
+ recorded input and output hash with the files at their recorded paths. It reports
530
+ `unchanged`, `changed`, `missing`, or `not recorded` and exits with status 1 unless all
531
+ records are unchanged. This checks file identity, not the validity of PEST tables; use
532
+ `makepst validate` for the latter. Moving files requires updating their recorded paths.
533
+ `makepst log` lists the manifests of a whole project folder as a ledger (with `--check`,
534
+ each one verified), and `makepst bundle` zips a run with everything its control file
535
+ references — see the two commands above.
536
+
466
537
  ## Relationship to pyEMU
467
538
 
468
539
  [pyEMU](https://github.com/pypest/pyemu) is the broader toolkit: it constructs PEST++
@@ -532,14 +603,14 @@ makepst/
532
603
  writer.py Pst -> .pst text (+ dump.tpl)
533
604
  reader.py .pst text -> Pst
534
605
  excel.py sheets -> Pst; Pst -> workbook; results -> existing workbook
535
- cli.py init / validate / build / dump / update / parrep / diff / tempchek / inschek
606
+ cli.py init / validate / build / dump / update / parrep / diff / tempchek / inschek / log / bundle
536
607
  diff.py semantic comparison of two Pst objects
537
608
  phi.py objective function by group from residuals; IES realization phis
538
609
  pyemu_bridge.py to_pyemu / from_pyemu
539
610
  checks.py the validate report: table checks, template / instruction cross-checks, instruction interpreter
540
611
  rules.py pestchek's rules (parameters, groups, observations, prior information, control variables)
541
612
  model_files.py PEST's number writer, template filling, .obf files (tempchek / inschek)
542
- provenance.py the <output>.manifest.json sidecar
613
+ provenance.py the <output>.manifest.json sidecar; provenance / log / bundle
543
614
  starter.py the `init` workbook: control-variable descriptions, example rows, drop-downs
544
615
  examples/minimal/ the tutorial inputs
545
616
  tests/ suite + fixture generator
@@ -552,7 +623,7 @@ stored anywhere. To release: bump the version in `pyproject.toml` and the fallba
552
623
  `makepst/__init__.py` (a test keeps them equal), add a `CHANGELOG.md` entry, commit, then
553
624
 
554
625
  ```
555
- git tag v0.2.0 && git push origin main v0.2.0
626
+ git tag v0.3.0 && git push origin main v0.3.0
556
627
  ```
557
628
 
558
629
  The `publish` workflow checks that the tag matches the package version, runs the tests, builds
@@ -14,8 +14,11 @@ reads control files back into editable workbooks, and returns calibration result
14
14
  Excel / CSV tables -> .pst makepst build
15
15
  .pst -> editable workbook makepst dump
16
16
  .par / .res / .rei / IES ensemble -> existing workbook makepst update
17
- .par or IES realization (+ tweaks) -> new .pst makepst parrep
17
+ .par or IES realization (+ tweaks) -> new .pst makepst parrep
18
+ .res/.rei or IES obs + existing .hp -> new .hp makepst hpstart
18
19
  two .pst / workbooks -> what changed makepst diff
20
+ manifests in a folder -> run ledger makepst log
21
+ .pst + everything it references -> zip for review makepst bundle
19
22
  ```
20
23
 
21
24
  See [What round-tripping preserves](#what-round-tripping-preserves) for the exact guarantees
@@ -192,6 +195,7 @@ makepst update book.xlsm --res run.res # adds MODELLED
192
195
  makepst update book.xlsm --pst run.pst --par_cols PARVAL1,PARTRANS,PARLBND,PARUBND
193
196
  makepst update book.xlsm --par run.3.par.csv --real best # PESTPP-IES ensemble
194
197
  makepst update book.xlsm --obs_csv run.3.obs.csv --pst run.pst # IES simulated values
198
+ makepst update book.xlsm --par run.par --dry_run # preview; write nothing
195
199
  [--sheet "PAR_*"] [--group hk,sy] [--overwrite_formulas] [--out copy.xlsm] [--backend openpyxl|xlwings]
196
200
  ```
197
201
 
@@ -229,8 +233,13 @@ Both sheets are rewritten on each run (`--no_phi` to skip).
229
233
 
230
234
  Two backends: **xlwings** (used when installed; drives Excel invisibly, so everything in the
231
235
  workbook is preserved and formulas recalculate; Windows/macOS with Excel) or **openpyxl**
232
- (no Excel needed; keeps VBA macros; drops charts and images; formula results are stale until
233
- Excel next opens the file). The backend used is printed.
236
+ (no Excel needed; keeps VBA macros; drops charts and images; does not recalculate formulas).
237
+ Before building from a workbook updated with openpyxl, recalculate and save it in Excel;
238
+ otherwise the build may read missing or stale cached formula values. The backend used is printed.
239
+ `--dry_run` uses openpyxl to preview matched names, target columns, skipped formulas and
240
+ unmatched names without saving a workbook or manifest. It does not test whether Excel will
241
+ refuse individual writes. During `build`, missing cached formula results in referenced
242
+ tables produce a warning; existing cached values may still be stale and cannot be verified.
234
243
 
235
244
  ### parrep — .par values → new .pst
236
245
 
@@ -252,6 +261,26 @@ to that workbook; other relative names resolve against the workbook's folder. No
252
261
  route rebuilds from the workbook's *current* state, so any edits made since the original
253
262
  control file was written are picked up too.
254
263
 
264
+ ### hpstart — modeled observations → new PEST_HP .hp file
265
+
266
+ ```
267
+ makepst hpstart case.pst start.hp --template case.hp --res case.res
268
+ makepst hpstart case.pst start.hp --template case.hp --obs_csv case.3.obs.csv --real best
269
+ makepst hpstart case.pst start.hp --template case.hp --res case.rei --par case.par
270
+ ```
271
+
272
+ By default, parameter values are copied unchanged from the template `.hp`, **not** from
273
+ `PARVAL1` in the `.pst`. Use `--par` to replace them with values from a PEST `.par` file
274
+ or a selected IES parameter realization. The `.pst` supplies parameter and observation
275
+ names and order; the template supplies PEST_HP instruction metadata that a residual or
276
+ IES observation file cannot provide. `hpstart` replaces the template's reference
277
+ modeled-observation array in `.pst` observation order. Every required observation must
278
+ be present with a finite value; extra result rows are ignored. The template must come
279
+ from the same control-file layout and
280
+ observation/parameter order. The binary format stores counts but not names, so matching
281
+ counts alone cannot establish that the template has the right order. The original `.hp`
282
+ is never modified, and the new file receives a provenance manifest by default.
283
+
255
284
  ### tempchek — templates → model input files
256
285
 
257
286
  ```
@@ -302,6 +331,39 @@ tolerance, so `49.7377` vs `49.73775` shows up at the default `1e-9` and disappe
302
331
  `--rtol 1e-5`. Prints a table per section and a one-line summary; `--xlsx` writes the same
303
332
  tables to a workbook for review. Exits 1 when there are differences, like `diff`.
304
333
 
334
+ ### log — the run ledger
335
+
336
+ ```
337
+ makepst log [FOLDER ...] [--file tr13.xlsm] [--command build] [--last 10] [--check] [--no_recursive]
338
+ ```
339
+
340
+ Every manifest under the folders (default: the current one, recursively), oldest first, one
341
+ line each: when, which command, which output, from which sources, the dimensions, the
342
+ makePst version. `--file` keeps the entries whose output or sources include a file — by name,
343
+ path or **sha256 prefix**, so `--file b43fc509` answers "which runs used *that* version of
344
+ the workbook?" even after the file was renamed or overwritten. `--check` adds a column from
345
+ `provenance`: `unchanged` / `changed` / `missing`.
346
+
347
+ ```
348
+ 2026-09-19 21:14 build pest/tr13.pst <- tr13.xlsm [1068 par 41747 obs 159 prior] makepst 0.3.0
349
+ 2026-09-21 08:10 update tr13-xlwings.xlsm <- tr13.xlsm, tr13.par [1068 rows in 8 sheets] makepst 0.3.0
350
+ ```
351
+
352
+ ### bundle — a run, zipped for review
353
+
354
+ ```
355
+ makepst bundle model.pst [run.zip] [--sources] [--outputs] [--extra "bin/**"] [--list] [--strict]
356
+ ```
357
+
358
+ Zips the control file with its manifest, every template and instruction file, the model
359
+ input files the templates write, and any file named on the model command line, with paths
360
+ relative to the run directory so the archive unzips runnable. `--sources` adds the manifest's
361
+ sources (the workbook, the `.par` ...), `--outputs` the model output files, `--extra` any
362
+ glob relative to the run directory (the model binaries, say). Files the control file
363
+ references but that don't exist are listed as `MISSING` (exit 1 with `--strict`); `--list`
364
+ shows what would be bundled without writing. The zip gets its own manifest, so `log` shows
365
+ who bundled what and when.
366
+
305
367
  ## What round-tripping preserves
306
368
 
307
369
  `dump` → `build` and `read_pst` → `write_pst` are **semantically lossless for supported
@@ -408,7 +470,7 @@ from?" after the fact:
408
470
 
409
471
  ```json
410
472
  {
411
- "makepst": "0.2.0",
473
+ "makepst": "0.3.0",
412
474
  "command": "build",
413
475
  "argv": ["build", "tr13.pst", "regul", "--set_ctl_xls", "tr13.xlsm,CONTROL", "..."],
414
476
  "created": "2026-09-19T13:51:39-04:00",
@@ -420,7 +482,7 @@ from?" after the fact:
420
482
  "sheets": ["CONTROL", "PARGP", "PAR_HK", "PAR_VK", "...", "PPglm"]}
421
483
  ],
422
484
  "output": {"path": "D:\\...\\tr13.pst", "sha256": "d93f317d...", "size": 2163331,
423
- "npar": 1068, "nobs": 41747, "npargp": 12, "nprior": 185, "nobsgp": 12,
485
+ "npar": 1068, "nobs": 41747, "npargp": 12, "nprior": 159, "nobsgp": 12,
424
486
  "ntplfle": 18, "ninsfle": 7, "pestmode": "regularisation"}
425
487
  }
426
488
  ```
@@ -432,6 +494,15 @@ realization and `--set` values (and, from a workbook, the resolved build argumen
432
494
  left alone, and the backend. A hash mismatch between a manifest and the workbook on disk is
433
495
  the signal that the control file no longer corresponds to the spreadsheet.
434
496
 
497
+ Run `makepst provenance model.pst` (or pass `model.pst.manifest.json`) to compare every
498
+ recorded input and output hash with the files at their recorded paths. It reports
499
+ `unchanged`, `changed`, `missing`, or `not recorded` and exits with status 1 unless all
500
+ records are unchanged. This checks file identity, not the validity of PEST tables; use
501
+ `makepst validate` for the latter. Moving files requires updating their recorded paths.
502
+ `makepst log` lists the manifests of a whole project folder as a ledger (with `--check`,
503
+ each one verified), and `makepst bundle` zips a run with everything its control file
504
+ references — see the two commands above.
505
+
435
506
  ## Relationship to pyEMU
436
507
 
437
508
  [pyEMU](https://github.com/pypest/pyemu) is the broader toolkit: it constructs PEST++
@@ -501,14 +572,14 @@ makepst/
501
572
  writer.py Pst -> .pst text (+ dump.tpl)
502
573
  reader.py .pst text -> Pst
503
574
  excel.py sheets -> Pst; Pst -> workbook; results -> existing workbook
504
- cli.py init / validate / build / dump / update / parrep / diff / tempchek / inschek
575
+ cli.py init / validate / build / dump / update / parrep / diff / tempchek / inschek / log / bundle
505
576
  diff.py semantic comparison of two Pst objects
506
577
  phi.py objective function by group from residuals; IES realization phis
507
578
  pyemu_bridge.py to_pyemu / from_pyemu
508
579
  checks.py the validate report: table checks, template / instruction cross-checks, instruction interpreter
509
580
  rules.py pestchek's rules (parameters, groups, observations, prior information, control variables)
510
581
  model_files.py PEST's number writer, template filling, .obf files (tempchek / inschek)
511
- provenance.py the <output>.manifest.json sidecar
582
+ provenance.py the <output>.manifest.json sidecar; provenance / log / bundle
512
583
  starter.py the `init` workbook: control-variable descriptions, example rows, drop-downs
513
584
  examples/minimal/ the tutorial inputs
514
585
  tests/ suite + fixture generator
@@ -521,7 +592,7 @@ stored anywhere. To release: bump the version in `pyproject.toml` and the fallba
521
592
  `makepst/__init__.py` (a test keeps them equal), add a `CHANGELOG.md` entry, commit, then
522
593
 
523
594
  ```
524
- git tag v0.2.0 && git push origin main v0.2.0
595
+ git tag v0.3.0 && git push origin main v0.3.0
525
596
  ```
526
597
 
527
598
  The `publish` workflow checks that the tag matches the package version, runs the tests, builds
@@ -3,9 +3,10 @@ try: # the installed package's metadata (
3
3
  from importlib.metadata import version as _dist_version
4
4
  __version__ = _dist_version('makepst')
5
5
  except Exception: # a checkout run without installing
6
- __version__ = '0.2.0'
6
+ __version__ = '0.3.0'
7
7
 
8
8
  from .excel import load_table, to_workbook, update_workbook
9
+ from .hpstart import write_hpstart
9
10
  from .model_files import fill_template, write_number
10
11
  from .provenance import Manifest
11
12
  from .pst import Pst, read_ensemble, read_obs_ensemble, read_par, read_res
@@ -24,4 +25,4 @@ def from_pyemu(ppst):
24
25
 
25
26
  __all__ = ['Pst', 'read_pst', 'from_text', 'write_pst', 'to_text',
26
27
  'load_table', 'to_workbook', 'update_workbook', 'read_par', 'read_res', 'read_ensemble', 'read_obs_ensemble',
27
- 'fill_template', 'write_number', 'Manifest', '__version__', 'to_pyemu', 'from_pyemu']
28
+ 'fill_template', 'write_number', 'write_hpstart', 'Manifest', '__version__', 'to_pyemu', 'from_pyemu']
@@ -1,6 +1,7 @@
1
1
  """Command line: build (Excel/CSV -> pst), dump (pst -> Excel), update (values -> existing workbook),
2
2
  parrep (.par values -> pst), init (starter workbook), validate (pestchek-style checks), diff (compare two),
3
- tempchek (templates -> model input files), inschek (model output files -> observation values)."""
3
+ tempchek (templates -> model input files), inschek (model output files -> observation values),
4
+ provenance / log / bundle (manifests: verify, list as a ledger, zip a run)."""
4
5
  import argparse
5
6
  import os
6
7
  import shlex
@@ -8,7 +9,7 @@ import sys
8
9
 
9
10
  from . import __version__
10
11
  from .excel import expand_spec, load_table, to_workbook, update_workbook
11
- from .provenance import Manifest
12
+ from .provenance import Manifest, check_manifest
12
13
  from .pst import Pst, read_par
13
14
  from .reader import read_pst
14
15
  from .writer import write_pst
@@ -445,7 +446,7 @@ def update(args):
445
446
  def flat(items): # repeated flags, each possibly a comma list
446
447
  return [x.strip() for s in items for x in s.split(',') if x.strip()] or None
447
448
 
448
- manifest = None if args.no_manifest else Manifest('update', args._argv)
449
+ manifest = None if args.no_manifest or args.dry_run else Manifest('update', args._argv)
449
450
  if manifest is not None:
450
451
  manifest.add_source(args.workbook, role='workbook')
451
452
  for path, role in ((args.par, 'parameter values'), (args.pst, 'control file'),
@@ -456,7 +457,8 @@ def update(args):
456
457
  real=args.real, pst=pst, out=args.out, backend=args.backend,
457
458
  par_cols=args.par_cols.upper().split(','), obs_cols=args.obs_cols.upper().split(','),
458
459
  overwrite_formulas=args.overwrite_formulas,
459
- sheets=flat(args.sheet), groups=flat(args.group), phi=not args.no_phi)
460
+ sheets=flat(args.sheet), groups=flat(args.group), phi=not args.no_phi,
461
+ dry_run=args.dry_run)
460
462
  if manifest is not None:
461
463
  manifest.set_output(args.out or args.workbook, sheets=result['sheets'], backend=result['backend'])
462
464
  manifest.add(realization=args.real, par_cols=args.par_cols, obs_cols=args.obs_cols,
@@ -464,11 +466,87 @@ def update(args):
464
466
  print(f'manifest written to {manifest.write()}')
465
467
 
466
468
 
469
+ def provenance(args):
470
+ path = args.target if args.target.endswith('.manifest.json') else args.target + '.manifest.json'
471
+ for status, name in check_manifest(path):
472
+ print(f'{status:12s} {name}')
473
+ if status != 'unchanged':
474
+ args._failed = True
475
+ if getattr(args, '_failed', False):
476
+ raise SystemExit(1)
477
+
478
+
479
+ def log(args):
480
+ """The ledger: every manifest under the folders, oldest first — what produced which file from what."""
481
+ from .provenance import find_manifests, log_line, mentions
482
+ rows = find_manifests(args.folder or ['.'], recursive=not args.no_recursive)
483
+ if args.file:
484
+ rows = [r for r in rows if any(mentions(r, f) for f in args.file)]
485
+ if args.command:
486
+ rows = [r for r in rows if r.get('command') in args.command]
487
+ if args.last:
488
+ rows = rows[-args.last:]
489
+ for r in rows:
490
+ print(log_line(r, check=args.check))
491
+ if not rows:
492
+ print('no manifests found')
493
+
494
+
495
+ def bundle(args):
496
+ """Zip a run so it can be reproduced or reviewed elsewhere (control file, manifest, templates,
497
+ instruction files, model inputs, command files; --sources / --outputs / --extra for more)."""
498
+ from .provenance import bundle_run
499
+ pst = read_pst(args.pstfile)
500
+ base = args.base_dir or guess_base_dir(pst, args.pstfile)
501
+ manifest = None if (args.no_manifest or args.list) else Manifest('bundle', args._argv)
502
+ out = None if args.list else (args.out or os.path.splitext(args.pstfile)[0] + '.zip')
503
+ included, missing = bundle_run(pst, args.pstfile, out, base_dir=base, sources=args.sources,
504
+ outputs=args.outputs, extra=args.extra, manifest=manifest)
505
+
506
+ def show(path):
507
+ rel = os.path.relpath(path, base)
508
+ return path if rel.startswith('..') else rel
509
+
510
+ for role, path in included:
511
+ print(f'{"would add" if args.list else "added":10s} {role:18s} {show(path)}')
512
+ for role, path in missing:
513
+ print(f'MISSING {role:18s} {show(path)}')
514
+ if args.list:
515
+ print(f'{len(included)} files, {len(missing)} missing (nothing written)')
516
+ return
517
+ size = os.path.getsize(out) / 1e6
518
+ print(f'written {out}: {len(included)} files, {size:.1f} MB' + (f', {len(missing)} missing' if missing else ''))
519
+ if manifest is not None:
520
+ manifest.set_output(out, files=len(included), missing=[p for _, p in missing])
521
+ print(f'manifest written to {manifest.write()}')
522
+ if missing and args.strict:
523
+ raise SystemExit(1)
524
+
525
+
526
+ def hpstart(args):
527
+ from .hpstart import write_hpstart
528
+ pst = read_pst(args.pstfile)
529
+ manifest = None if args.no_manifest else Manifest('hpstart', args._argv)
530
+ if manifest is not None:
531
+ for path, role in ((args.pstfile, 'control file'), (args.template, 'hp template'),
532
+ (args.res, 'residuals'), (args.obs_csv, 'observation ensemble'),
533
+ (args.par, 'parameter values')):
534
+ if path:
535
+ manifest.add_source(path, role=role)
536
+ counts = write_hpstart(pst, args.template, args.out, res=args.res,
537
+ obs_csv=args.obs_csv, real=args.real, par=args.par)
538
+ print(f'written {args.out}: {counts["npar"]} parameters, {counts["nobs"]} observations')
539
+ if manifest is not None:
540
+ manifest.set_output(args.out, **counts)
541
+ manifest.add(realization=args.real)
542
+ print(f'manifest written to {manifest.write()}')
543
+
544
+
467
545
  def main(argv=None):
468
546
  argv = list(sys.argv[1:] if argv is None else argv)
469
547
  # legacy form: makePst.py out.pst mode --add_par_xls ... (no subcommand)
470
- if argv and argv[0] not in ('build', 'dump', 'update', 'parrep', 'init', 'validate', 'diff', 'tempchek', 'inschek',
471
- '-h', '--help', '-v', '--version'):
548
+ if argv and argv[0] not in ('build', 'dump', 'update', 'parrep', 'init', 'validate', 'provenance', 'log', 'bundle',
549
+ 'diff', 'tempchek', 'inschek', 'hpstart', '-h', '--help', '-v', '--version'):
472
550
  argv.insert(0, 'build')
473
551
 
474
552
  ap = argparse.ArgumentParser(prog='makepst', description=__doc__)
@@ -500,6 +578,17 @@ def main(argv=None):
500
578
  rf.add_argument('--v2', action='store_true', help='write a PEST++ version-2 file')
501
579
  rf.add_argument('--v1', action='store_true', help='write the classic format (default: same as the input)')
502
580
 
581
+ h = sub.add_parser('hpstart', help='update a PEST_HP .hp file from residual or IES observation values')
582
+ h.add_argument('pstfile', help='control file defining observation and parameter order')
583
+ h.add_argument('out', help='new .hp file (must differ from the template)')
584
+ h.add_argument('--template', required=True, help='existing .hp with matching order; instruction metadata is preserved')
585
+ hs = h.add_mutually_exclusive_group(required=True)
586
+ hs.add_argument('--res', help='PEST .res/.rei file providing MODELLED values')
587
+ hs.add_argument('--obs_csv', help='PESTPP-IES case.N.obs.csv providing modeled values')
588
+ h.add_argument('--real', help='IES realization name, or best (default base)')
589
+ h.add_argument('--par', help='optional .par or IES parameter file to replace template parameter values')
590
+ h.add_argument('--no_manifest', action='store_true', help="don't write <out>.manifest.json")
591
+
503
592
  v = sub.add_parser('validate', help='pestchek-style checks on a control file or a workbook')
504
593
  v.add_argument('target', help='control file, or a workbook with a BUILD sheet')
505
594
  v.add_argument('--outputs', action='store_true',
@@ -509,6 +598,31 @@ def main(argv=None):
509
598
  v.add_argument('--strict', action='store_true', help='exit 1 on warnings as well as errors')
510
599
  v.add_argument('--quiet', action='store_true', help='hide informational lines')
511
600
 
601
+ q = sub.add_parser('provenance', help='compare files against a saved manifest')
602
+ q.add_argument('target', help='manifest path, or output path with a .manifest.json sidecar')
603
+
604
+ g = sub.add_parser('log', help='ledger of every manifest under a folder: what produced which file from what')
605
+ g.add_argument('folder', nargs='*', help='folders (or manifest files) to search; default the current one')
606
+ g.add_argument('--file', action='append', default=[], metavar='NAME',
607
+ help='only entries whose output or sources include this file name, path or sha256 prefix')
608
+ g.add_argument('--command', action='append', default=[], metavar='CMD', help='only this command (repeatable)')
609
+ g.add_argument('--last', type=int, metavar='N', help='only the N most recent entries')
610
+ g.add_argument('--check', action='store_true', help='also compare every recorded hash with the file on disk')
611
+ g.add_argument('--no_recursive', action='store_true', help='do not descend into subfolders')
612
+
613
+ b = sub.add_parser('bundle', help='zip a run for reproduction or review: control file, manifest, templates, '
614
+ 'instruction files, model inputs, command files')
615
+ b.add_argument('pstfile', help='control file')
616
+ b.add_argument('out', nargs='?', help='zip file to write (default: the control file name with .zip)')
617
+ b.add_argument('--sources', action='store_true', help="also the manifest's sources (the workbook, .par, ...)")
618
+ b.add_argument('--outputs', action='store_true', help='also the model output files the instruction files read')
619
+ b.add_argument('--extra', action='append', default=[], metavar='GLOB',
620
+ help='more files, relative to the run directory (repeatable; ** allowed)')
621
+ b.add_argument('--base_dir', metavar='DIR', help='the directory PEST runs in (default: guessed as validate does)')
622
+ b.add_argument('--list', action='store_true', help='show what would be bundled, write nothing')
623
+ b.add_argument('--strict', action='store_true', help='exit 1 when a referenced file is missing')
624
+ b.add_argument('--no_manifest', action='store_true', help="don't write <zip>.manifest.json")
625
+
512
626
  t = sub.add_parser('tempchek', help='check a template file, or write model input files from parameter values (like TEMPCHEK)')
513
627
  t.add_argument('target', help='template file, or a control file / workbook (all of its templates)')
514
628
  t.add_argument('modfile', nargs='?', help='with a template: model input file to write')
@@ -552,6 +666,7 @@ def main(argv=None):
552
666
  u.add_argument('--overwrite_formulas', action='store_true',
553
667
  help='also write into cells that currently hold formulas (default: leave them alone)')
554
668
  u.add_argument('--out', help='save to this file instead of in place')
669
+ u.add_argument('--dry_run', action='store_true', help='preview matches and skipped cells without writing files')
555
670
  u.add_argument('--backend', choices=['xlwings', 'openpyxl'], help='default: xlwings if installed')
556
671
  u.add_argument('--no_manifest', action='store_true', help="don't write <workbook>.manifest.json")
557
672
  u.add_argument('--no_phi', action='store_true', help="don't write the PHI / PHI_IES sheets with --res / --obs_csv")
@@ -559,7 +674,9 @@ def main(argv=None):
559
674
  args = ap.parse_args(argv)
560
675
  args._argv = argv # the command line as given, for the manifest
561
676
  {'build': build, 'dump': dump, 'update': update, 'parrep': parrep, 'init': init,
562
- 'validate': validate, 'diff': diff, 'tempchek': tempchek, 'inschek': inschek}[args.cmd](args)
677
+ 'validate': validate, 'provenance': provenance, 'log': log, 'bundle': bundle, 'diff': diff,
678
+ 'tempchek': tempchek, 'inschek': inschek,
679
+ 'hpstart': hpstart}[args.cmd](args)
563
680
 
564
681
 
565
682
  if __name__ == '__main__':