goPEST 0.0.11__py3-none-any.whl
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.
- goPEST-0.0.11.dist-info/LICENSE +11 -0
- goPEST-0.0.11.dist-info/METADATA +95 -0
- goPEST-0.0.11.dist-info/RECORD +30 -0
- goPEST-0.0.11.dist-info/WHEEL +5 -0
- goPEST-0.0.11.dist-info/entry_points.txt +2 -0
- goPEST-0.0.11.dist-info/top_level.txt +1 -0
- gopest/__init__.py +11 -0
- gopest/_version.py +16 -0
- gopest/check_slaves.py +402 -0
- gopest/commands.py +80 -0
- gopest/common.py +194 -0
- gopest/data/case.pst +67 -0
- gopest/data/goPESTconfig.aut2.toml +95 -0
- gopest/data/goPESTconfig.toml +94 -0
- gopest/data/goPESTobs.list +793 -0
- gopest/data/goPESTpar.list +95 -0
- gopest/make_case_pst.py +229 -0
- gopest/obs.py +297 -0
- gopest/obs_def.py +2086 -0
- gopest/par.py +332 -0
- gopest/par_def.py +313 -0
- gopest/pest_model.py +245 -0
- gopest/rename_latest_files.py +35 -0
- gopest/run_beopest.py +205 -0
- gopest/run_ns_pr.py +617 -0
- gopest/submit_beopest.py +931 -0
- gopest/utils/__init__.py +0 -0
- gopest/utils/gener_groups.py +192 -0
- gopest/utils/t2listingh5.py +376 -0
- gopest/utils/waiwera_listing.py +587 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Copyright 2022 Angus Yeh, University of Auckland, New Zealand
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
|
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
8
|
+
|
|
9
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
10
|
+
|
|
11
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: goPEST
|
|
3
|
+
Version: 0.0.11
|
|
4
|
+
Summary: A TOUGH2/Waiwera interface to PEST
|
|
5
|
+
Author-email: Angus Yeh <a.yeh@auckland.ac.nz>
|
|
6
|
+
License: BSD 3-Clause License
|
|
7
|
+
Keywords: goPEST,PEST,Waiwera,TOUGH2,AUTOUGH2
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=2.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: PyTOUGH
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: scipy
|
|
19
|
+
Requires-Dist: h5py
|
|
20
|
+
Requires-Dist: pyyaml
|
|
21
|
+
Requires-Dist: tomlkit
|
|
22
|
+
Requires-Dist: xlwt
|
|
23
|
+
Requires-Dist: setuptools-scm
|
|
24
|
+
|
|
25
|
+
# **goPEST**
|
|
26
|
+
|
|
27
|
+
goPEST is a set of utilities used to interface PEST with Waiwera and (AU)TOUGH2 simulators.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
The easiest way to install goPEST is:
|
|
33
|
+
|
|
34
|
+
```python -m pip install gopest```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Input files
|
|
38
|
+
|
|
39
|
+
User needs to prepare a few files for goPEST to work:
|
|
40
|
+
|
|
41
|
+
- `goPESTconfig.toml`, this file contains all settings/configurations related to the running of goPEST. It is written in TOML file format. The file should be placed in the project folder, where user runs gopest commands from. It is possible to let goPEST generate one with default values. The file tries to be self-explanatory with comments.
|
|
42
|
+
|
|
43
|
+
- `goPESTpar.list` is where user specifies model parameters for PEST
|
|
44
|
+
|
|
45
|
+
- `goPESTobs.list` is where user specifies model observations for PEST
|
|
46
|
+
|
|
47
|
+
- `goPESTuser.py` is optional if user wish do perform customised setups when the simulation transit from one stage to the next stage in sequence of simulator runs (eg. usually `ns` and `pr` normally natural state then followed by production history).
|
|
48
|
+
|
|
49
|
+
## Basic usage
|
|
50
|
+
|
|
51
|
+
The main CLI script `gopest` is to be followed by COMMAND and associated arguments:
|
|
52
|
+
|
|
53
|
+
```gopest COMMAND [ARGUMENTS]```
|
|
54
|
+
|
|
55
|
+
To get a list of supported COMMANDs, type `gopest help`.
|
|
56
|
+
|
|
57
|
+
The first step is to initialise the working directory:
|
|
58
|
+
|
|
59
|
+
```gopest init```
|
|
60
|
+
|
|
61
|
+
will setup the current folder. If all goes well, user can simply run the command:
|
|
62
|
+
|
|
63
|
+
```gopest submit``` or
|
|
64
|
+
```gopest run```
|
|
65
|
+
|
|
66
|
+
to start the PEST run. Command `submit` is for the NeSI cluster environment using SLURM. Job(s) will be submitted to the cluster queue. PEST master and agents will be launched automatically. On a local machine where user has full access `gopest run` is used.
|
|
67
|
+
|
|
68
|
+
## How goPEST works
|
|
69
|
+
|
|
70
|
+
Several tasks were performed by the `init` command:
|
|
71
|
+
|
|
72
|
+
- copy user's model files into what goPEST uses internally, these are the `real_model_xxx.*` files, in the current folder, which is also the master folder where PEST is expected to work on.
|
|
73
|
+
|
|
74
|
+
- go through `goPESTpar.list`, extract and set up parameter data in the PEST control file (usually `case.pst`). The corresponding `.tpl` files etc required by PEST will be set up. Note the parameters used in the real model will be extracted and used as the initial parameters in the PEST.
|
|
75
|
+
|
|
76
|
+
- go through `goPESTobs.list`, and set up observation data in the PEST control file. Corresponding PEST instruction file `.ins` will also be set up automatically.
|
|
77
|
+
|
|
78
|
+
## Development notes
|
|
79
|
+
|
|
80
|
+
- install an editable version of goPEST:
|
|
81
|
+
|
|
82
|
+
```python -m pip install -e /path/to/repo/root```
|
|
83
|
+
|
|
84
|
+
- run tests at the root of the repo:
|
|
85
|
+
|
|
86
|
+
```python -m pytest```
|
|
87
|
+
|
|
88
|
+
- generalised model sequence runner? now loads user goPESTuser.py, but internal needs to generalise to have more than two run sequence
|
|
89
|
+
|
|
90
|
+
- in run_ns_pr, code shouldn't worry about nesi/cluster related things, maybe
|
|
91
|
+
not running local vs nesi either
|
|
92
|
+
|
|
93
|
+
- remove obsreref related things, use pest_hp now
|
|
94
|
+
|
|
95
|
+
- I have checked a few PEST related Python libraries. I am looking for something small and pure for basic editing of PEST control file. But these are too big for my liking. I should reconsider about using them.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
gopest/__init__.py,sha256=sQIeyIA_BvmrEsXURFJskDg6rlPDNJLjFoFJcHn-MkQ,374
|
|
2
|
+
gopest/_version.py,sha256=TLLlZVpoPCYB-ad4nVSzPokzjT6xk0F2KolI_iEJXtA,429
|
|
3
|
+
gopest/check_slaves.py,sha256=PdpDHRjHHl1fJBwhXb8mAhSt6QXK1cJQ03goUXjyumE,13769
|
|
4
|
+
gopest/commands.py,sha256=xXERkb5hb_hbfUZMy1qdD78q1ZakHH5x-1WSyKxzs14,2651
|
|
5
|
+
gopest/common.py,sha256=BT_DqgpqvqnM8obpyt9IOg5OuyqSN1aWuBKqfrUYyu4,7664
|
|
6
|
+
gopest/make_case_pst.py,sha256=DNb1Ta_HE2uYwzlFti9tZRwF80pyvWyavws77LAbSfM,8760
|
|
7
|
+
gopest/obs.py,sha256=rFnWIddecp4Kwx_OHDIbt5uxHTnYI_C3YcaWoznY8cs,10047
|
|
8
|
+
gopest/obs_def.py,sha256=oFYOD8S7HnA1bSRVLRrlKtqBlpwxbcWu_izf88U6ZZo,78308
|
|
9
|
+
gopest/par.py,sha256=lm21mqbwL58I9gLv67mCrVvMC_6BVK7L_4kblTuvgtc,13043
|
|
10
|
+
gopest/par_def.py,sha256=gT-7BTk7zYTYzc3U8WKIRxXAfxIlykTYpvtJbNKBGxM,10841
|
|
11
|
+
gopest/pest_model.py,sha256=KqL1mSrOqP-T3ljyE8UiLsa7FvlEgohnEvai8HiT-Q4,8666
|
|
12
|
+
gopest/rename_latest_files.py,sha256=gZwbZMwni0ZQPjCgOL3C6JG65jO9gFPR9vN9QftqRFY,1095
|
|
13
|
+
gopest/run_beopest.py,sha256=VeuLYI2Wr2leocV92Rowm_s38B_04eJ1dcV1WiMFCe0,7077
|
|
14
|
+
gopest/run_ns_pr.py,sha256=tC5Umidn1gZBHRm2XoM-7FSUlMwWKa9WRxA0C1yfG0U,22721
|
|
15
|
+
gopest/submit_beopest.py,sha256=Q5Z-o6Lz4uuGN8di7NojtiUlRUNHZI6KY_Mmhs30reE,35142
|
|
16
|
+
gopest/data/case.pst,sha256=xGNftyacW3twbqmPUN8V3ZQuNaMyqoGJB0o7Y327fso,2506
|
|
17
|
+
gopest/data/goPESTconfig.aut2.toml,sha256=mrMtwqKy1_4R6qQAPNqLBcMxqVKzrdmSbPpg6rEzD9g,2782
|
|
18
|
+
gopest/data/goPESTconfig.toml,sha256=GbcWFMzStaIEwgStPVyxE7RaIBMVCpZKbMk98r8YdZU,2767
|
|
19
|
+
gopest/data/goPESTobs.list,sha256=aNdlcoP8MQVvsgHaInCnQ_bktFvtiBFCWiPwWI_lkEs,10351
|
|
20
|
+
gopest/data/goPESTpar.list,sha256=Ne-OEsOVh_JtLBVF8HMG5EOmYl8g_SD5mWsbExD2oq4,1852
|
|
21
|
+
gopest/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
gopest/utils/gener_groups.py,sha256=VtbX35VWKE0V0wVBJnUXC0KtcfY-fIz4XrV3OBSYngI,6624
|
|
23
|
+
gopest/utils/t2listingh5.py,sha256=36X-XUdakSkRSn26ZdU8uoAKEfsFydiT0p3xzsobjGk,16791
|
|
24
|
+
gopest/utils/waiwera_listing.py,sha256=c7moQY8DQEH_MqLUuhrinnkEHnDMKWx4srChffXoK6I,27787
|
|
25
|
+
goPEST-0.0.11.dist-info/LICENSE,sha256=ptsyzynWmtAQdzQN1KIqWAkMbJMyKG03PzYniyCa1TE,1490
|
|
26
|
+
goPEST-0.0.11.dist-info/METADATA,sha256=8OBaPfrqRIAxA0-qAuic1YCoTohqWuJemiR4IZL1Zkc,3885
|
|
27
|
+
goPEST-0.0.11.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
28
|
+
goPEST-0.0.11.dist-info/entry_points.txt,sha256=lQZ0lnYLOBrcoQehLV6TZsLFu1OJPLcw-PCOUs0_SpA,54
|
|
29
|
+
goPEST-0.0.11.dist-info/top_level.txt,sha256=g3FO8XhhpDnQXWy5B9lv4uMKMEilq1TKv8SAk5x8_eU,7
|
|
30
|
+
goPEST-0.0.11.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gopest
|
gopest/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
try:
|
|
2
|
+
# works if calling from an repo or an pip install -e installation
|
|
3
|
+
from setuptools_scm import get_version
|
|
4
|
+
__version__ = get_version()
|
|
5
|
+
except LookupError:
|
|
6
|
+
try:
|
|
7
|
+
# works if calling from other pip install methods
|
|
8
|
+
from ._version import version as __version__
|
|
9
|
+
except ImportError:
|
|
10
|
+
# worst case scenario
|
|
11
|
+
__version__ = '0.0.0'
|
gopest/_version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.0.11'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 0, 11)
|
gopest/check_slaves.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
from multiprocessing import Pool, cpu_count
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import os.path
|
|
6
|
+
import sys
|
|
7
|
+
import shutil
|
|
8
|
+
import glob
|
|
9
|
+
import subprocess
|
|
10
|
+
import collections.abc
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
import tomlkit
|
|
14
|
+
import xlwt
|
|
15
|
+
|
|
16
|
+
from gopest.common import config
|
|
17
|
+
from gopest.common import runtime
|
|
18
|
+
|
|
19
|
+
def nested_dict_update(d, u):
|
|
20
|
+
""" update a nested dict object with an update dict
|
|
21
|
+
https://stackoverflow.com/a/3233356/2368167
|
|
22
|
+
"""
|
|
23
|
+
for k, v in u.items():
|
|
24
|
+
if isinstance(v, collections.abc.Mapping):
|
|
25
|
+
d[k] = nested_dict_update(d.get(k, {}), v)
|
|
26
|
+
else:
|
|
27
|
+
d[k] = v
|
|
28
|
+
return d
|
|
29
|
+
|
|
30
|
+
def unix2dos(fname):
|
|
31
|
+
import sys
|
|
32
|
+
with open(fname, 'rb') as infile:
|
|
33
|
+
instr = infile.read()
|
|
34
|
+
outstr = instr.replace( b"\r\n", b"\n" ).replace( b"\r", b"\n" ).replace( b"\n", b"\r\n" )
|
|
35
|
+
if len(outstr) == len(instr):
|
|
36
|
+
return
|
|
37
|
+
with open(fname, 'wb') as outfile:
|
|
38
|
+
outfile.write( outstr )
|
|
39
|
+
|
|
40
|
+
def fixpcf_noptmax(fpst, noptmax=0):
|
|
41
|
+
""" update PEST case file (.pst) with NOPTMAX, number of optimisation
|
|
42
|
+
iterations
|
|
43
|
+
"""
|
|
44
|
+
fpst_bk = fpst + '.backup'
|
|
45
|
+
if not os.path.isfile(fpst):
|
|
46
|
+
print('Error: %s does not exist.' % fpst)
|
|
47
|
+
exit(1)
|
|
48
|
+
if os.path.isfile(fpst_bk):
|
|
49
|
+
os.remove(fpst_bk)
|
|
50
|
+
os.rename(fpst, fpst_bk)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
with open(fpst_bk,'r') as fin, open(fpst, 'w') as fout:
|
|
54
|
+
linesep = None # use current file's line ending
|
|
55
|
+
isec = 7 # start with 7, so that it will never be 7 again
|
|
56
|
+
for line in fin:
|
|
57
|
+
# detect current file's line ending first
|
|
58
|
+
if linesep is None:
|
|
59
|
+
if line[-2] == '\r' and line[-1] == '\n':
|
|
60
|
+
linesep = '\r\n'
|
|
61
|
+
elif line[-1] == '\n':
|
|
62
|
+
linesep = '\n'
|
|
63
|
+
# find correct line, replace
|
|
64
|
+
isec += 1
|
|
65
|
+
oline = line
|
|
66
|
+
if line.strip().startswith('*') and 'control data' in line:
|
|
67
|
+
isec = 0
|
|
68
|
+
if isec == 7:
|
|
69
|
+
# replace first item in line (NOPTMAX)
|
|
70
|
+
sps = line.split()
|
|
71
|
+
oline = ' ' + ' '.join([str(noptmax)] + sps[1:] + [linesep])
|
|
72
|
+
fout.write(oline)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
fin.close()
|
|
75
|
+
fout.close()
|
|
76
|
+
print('fixpcf_noptmax() failed to proceed, restoring...')
|
|
77
|
+
os.remove(fpst)
|
|
78
|
+
os.rename(fpst_bk, fpst)
|
|
79
|
+
raise(e)
|
|
80
|
+
|
|
81
|
+
def read_rec(rec_file):
|
|
82
|
+
values = {}
|
|
83
|
+
with open(rec_file, 'r') as f:
|
|
84
|
+
reading = False
|
|
85
|
+
for line in f:
|
|
86
|
+
if reading:
|
|
87
|
+
if empty_line:
|
|
88
|
+
empty_line = False
|
|
89
|
+
continue
|
|
90
|
+
if not line.strip():
|
|
91
|
+
continue
|
|
92
|
+
if '----->' in line:
|
|
93
|
+
break
|
|
94
|
+
sps = line.strip().split('=')
|
|
95
|
+
v = float(sps[-1])
|
|
96
|
+
s = sps[0].strip()
|
|
97
|
+
if s == 'Sum of squared weighted residuals (ie phi)':
|
|
98
|
+
k = 'phi'
|
|
99
|
+
elif s == 'Optimised measurement objective function':
|
|
100
|
+
k = 'phi measured'
|
|
101
|
+
elif s == 'Optimised regularisation objective function':
|
|
102
|
+
k = 'phi regularisation'
|
|
103
|
+
elif '"' in s:
|
|
104
|
+
k = s.split('"')[1]
|
|
105
|
+
else:
|
|
106
|
+
k, v = None, None
|
|
107
|
+
if k is not None:
|
|
108
|
+
values[k] = v
|
|
109
|
+
if line.startswith('Objective function ----->'):
|
|
110
|
+
reading, empty_line = True, True
|
|
111
|
+
return values
|
|
112
|
+
|
|
113
|
+
class message(object):
|
|
114
|
+
""" A Waiwera log (.yaml) message object """
|
|
115
|
+
def __init__(self, msg):
|
|
116
|
+
self._msg = msg
|
|
117
|
+
self.level, self.source, self.event, self.data = msg
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def check_sim_ends(spath):
|
|
121
|
+
""" extract summary of waiwera simulation from yaml log """
|
|
122
|
+
def waiwera_ends(fyaml):
|
|
123
|
+
""" return elapsed time in seconds of waiwera simulation time, None if not
|
|
124
|
+
successful """
|
|
125
|
+
try:
|
|
126
|
+
with open(fyaml) as f:
|
|
127
|
+
data = yaml.load(f, Loader=yaml.CSafeLoader)
|
|
128
|
+
r = {''}
|
|
129
|
+
if data[-3][:3] == ['info', 'timestep', 'end']:
|
|
130
|
+
time = data[-3][3]['time']
|
|
131
|
+
size = data[-3][3]['size']
|
|
132
|
+
if data[-1][:3] == ['info', 'simulation', 'destroy']:
|
|
133
|
+
elapsed = data[-1][3]['elapsed_seconds']
|
|
134
|
+
return {'endtime': time, 'stepsize': size, 'elapsed': elapsed}
|
|
135
|
+
except:
|
|
136
|
+
return {}
|
|
137
|
+
cwd = os.getcwd()
|
|
138
|
+
os.chdir(spath)
|
|
139
|
+
print('Working in %s...' % spath)
|
|
140
|
+
|
|
141
|
+
results = {'run': {}}
|
|
142
|
+
sequence = config['model']['sequence']
|
|
143
|
+
input_typ = config['simulator']['input-type']
|
|
144
|
+
flsts = runtime['filename']['lst_seq']
|
|
145
|
+
for i,seq in enumerate(sequence):
|
|
146
|
+
results['run'][seq] = {}
|
|
147
|
+
if input_typ== 'waiwera':
|
|
148
|
+
fyaml = flsts[i].replace('.h5', '.yaml')
|
|
149
|
+
results['run'][seq] = waiwera_ends(fyaml)
|
|
150
|
+
else:
|
|
151
|
+
raise NotImplementedError()
|
|
152
|
+
|
|
153
|
+
os.chdir(cwd)
|
|
154
|
+
# print('Restored directory %s' % cwd)
|
|
155
|
+
return results
|
|
156
|
+
|
|
157
|
+
def check_run_status(spath):
|
|
158
|
+
""" extract summary of waiwera simulation from yaml log """
|
|
159
|
+
cwd = os.getcwd()
|
|
160
|
+
os.chdir(spath)
|
|
161
|
+
print('Working in %s...' % spath)
|
|
162
|
+
|
|
163
|
+
results = {'run': {}}
|
|
164
|
+
sequence = config['model']['sequence']
|
|
165
|
+
input_typ = config['simulator']['input-type']
|
|
166
|
+
flsts = runtime['filename']['lst_seq']
|
|
167
|
+
for i,seq in enumerate(sequence):
|
|
168
|
+
results['run'][seq] = {}
|
|
169
|
+
results['run'][seq]['exists'] = os.path.exists(flsts[i])
|
|
170
|
+
|
|
171
|
+
os.chdir(cwd)
|
|
172
|
+
# print('Restored directory %s' % cwd)
|
|
173
|
+
return results
|
|
174
|
+
|
|
175
|
+
def get_obj_fn(spath):
|
|
176
|
+
""" Return PEST calculated objective function values by modifying config and
|
|
177
|
+
PEST case file to run a dummy run. Then read results from case .rec file.
|
|
178
|
+
|
|
179
|
+
This is more destructive, but I have decided to leave those modification
|
|
180
|
+
in place after running the command for a couple of reasons:
|
|
181
|
+
1. (lazy) no need to handle undo the mod if exception is raised
|
|
182
|
+
2. (safe) it's probably safer to leave as modified here, reduce possibility
|
|
183
|
+
for user to wipe out existing model output files, which can be costly to
|
|
184
|
+
run
|
|
185
|
+
|
|
186
|
+
Unfortunately I need to modify the config.toml file, because gopest run-
|
|
187
|
+
pest-model is involked by PEST, not this script directly.
|
|
188
|
+
"""
|
|
189
|
+
cwd = os.getcwd()
|
|
190
|
+
os.chdir(spath)
|
|
191
|
+
print('Working in %s...' % spath)
|
|
192
|
+
|
|
193
|
+
# only works if the final real model output file exists
|
|
194
|
+
flst = runtime['filename']['lst_seq'][-1]
|
|
195
|
+
if not os.path.exists(flst):
|
|
196
|
+
print('Real model output file %s is missing, cannot get obj. fn.' % flst)
|
|
197
|
+
os.chdir(cwd)
|
|
198
|
+
return {}
|
|
199
|
+
|
|
200
|
+
# modify config to skip model run
|
|
201
|
+
with open('goPESTconfig.toml', 'r') as f:
|
|
202
|
+
cfg = tomlkit.load(f)
|
|
203
|
+
cfg['model']['skip'] = True
|
|
204
|
+
with open('goPESTconfig.toml', 'w') as f:
|
|
205
|
+
tomlkit.dump(cfg, f)
|
|
206
|
+
|
|
207
|
+
# modify NOPTMAX in control data .pst file
|
|
208
|
+
fpst = config['pest']['case-name'] + '.pst'
|
|
209
|
+
fixpcf_noptmax(fpst, 0)
|
|
210
|
+
|
|
211
|
+
cmd = [
|
|
212
|
+
os.path.join(config['pest']['dir'], config['pest']['executable']),
|
|
213
|
+
config['pest']['case-name'],
|
|
214
|
+
]
|
|
215
|
+
print('Running: ', cmd)
|
|
216
|
+
results = {}
|
|
217
|
+
subprocess.call(cmd)
|
|
218
|
+
results['obj-fn'] = read_rec('case_reg.rec')
|
|
219
|
+
|
|
220
|
+
os.chdir(cwd)
|
|
221
|
+
# print('Restored directory %s' % cwd)
|
|
222
|
+
return results
|
|
223
|
+
|
|
224
|
+
def export_xls(data):
|
|
225
|
+
phis, regs, others = [], [], []
|
|
226
|
+
for sln,sl in data.items():
|
|
227
|
+
if 'obj-fn' in sl:
|
|
228
|
+
for n in sorted(sl['obj-fn'].keys()):
|
|
229
|
+
if n.startswith('phi'):
|
|
230
|
+
phis.append(n)
|
|
231
|
+
elif n.startswith('regul_'):
|
|
232
|
+
regs.append(n)
|
|
233
|
+
else:
|
|
234
|
+
others.append(n)
|
|
235
|
+
phis, regs, others = sorted(phis), sorted(regs), sorted(others)
|
|
236
|
+
break
|
|
237
|
+
runcs = []
|
|
238
|
+
for sln,sl in data.items():
|
|
239
|
+
if 'run' in sl:
|
|
240
|
+
for rn,r in sl['run'].items():
|
|
241
|
+
runcs += [rn+'.'+c for c in sorted(r.keys())]
|
|
242
|
+
break
|
|
243
|
+
cols = ['slave'] + phis + others + regs + runcs
|
|
244
|
+
|
|
245
|
+
wb = xlwt.Workbook()
|
|
246
|
+
ws = wb.add_sheet('slaves')
|
|
247
|
+
|
|
248
|
+
i = 0
|
|
249
|
+
for j,c in enumerate(cols):
|
|
250
|
+
ws.write(i, j, c)
|
|
251
|
+
|
|
252
|
+
for sln,sl in data.items():
|
|
253
|
+
if 'obj-fn' in sl:
|
|
254
|
+
i += 1
|
|
255
|
+
j = 0
|
|
256
|
+
ws.write(i, j, sln)
|
|
257
|
+
for p in (phis + others + regs):
|
|
258
|
+
j += 1
|
|
259
|
+
if p in sl['obj-fn']:
|
|
260
|
+
ws.write(i, j, sl['obj-fn'][p])
|
|
261
|
+
for r in runcs:
|
|
262
|
+
j += 1
|
|
263
|
+
rn,sn = r.split('.')
|
|
264
|
+
if 'run' in sl and rn in sl['run'] and sn in sl['run'][rn]:
|
|
265
|
+
ws.write(i, j, sl['run'][rn][sn])
|
|
266
|
+
wb.save('goPESTslaves.xls')
|
|
267
|
+
|
|
268
|
+
def init_slave(cfg, rt):
|
|
269
|
+
""" used in multiprocessing Pool to initialise threads with the the updated
|
|
270
|
+
gopest.common.config and gopest.common.runtime
|
|
271
|
+
"""
|
|
272
|
+
print('Updating local thread common.config and common.runtime.')
|
|
273
|
+
global config, runtime
|
|
274
|
+
config = cfg
|
|
275
|
+
runtime = rt
|
|
276
|
+
|
|
277
|
+
hlp = '''
|
|
278
|
+
Usage: gopest check-slaves [--help] [--status] [--end-time] [--obj-fn]
|
|
279
|
+
[--dir path_to_slaves] [--pest-exe pest_executable]
|
|
280
|
+
[--export-xls]
|
|
281
|
+
|
|
282
|
+
The check-slaves command searches through slave directories and obtain/collect
|
|
283
|
+
their running status etc. By default, the pest.slave_dirs property from
|
|
284
|
+
goPESTconfig.toml is used. It is possible to specify a different directory by
|
|
285
|
+
using argument "--dir".
|
|
286
|
+
|
|
287
|
+
Extracted information will be dumped into a JSON file "goPESTslaves.json". Note
|
|
288
|
+
that the command will only update/append results into the JSON file if exists.
|
|
289
|
+
It tries not to destroy whatever that is already in the file.
|
|
290
|
+
|
|
291
|
+
"--status" is the fastest option, which only checks if model output files exist
|
|
292
|
+
in the lsave directories. (ie. did the runs produce any output files at all)
|
|
293
|
+
|
|
294
|
+
"--end-time" enables extraction of simulation end time (from output YAML file if
|
|
295
|
+
running waiwera as simulator).
|
|
296
|
+
|
|
297
|
+
"--obj-fn" is more destructive. Within each slave directory, it will run PEST
|
|
298
|
+
(with the NOPTMAX set to 0) once with the internal settings set to skip actual
|
|
299
|
+
model runs. This essentially runs PEST so that observations and objective
|
|
300
|
+
function will be extracted from model outputs within the slave directory.
|
|
301
|
+
|
|
302
|
+
"--pest-exe" is useful with the "--obj-fn" option when user have the slaves
|
|
303
|
+
directories in a different environment than where it was originally run.
|
|
304
|
+
pest_executable here can include the path to the executable if it's not already
|
|
305
|
+
in the system's PATH.
|
|
306
|
+
|
|
307
|
+
"--export-xls" is used to export contents of goPESTslaves.json into spreadsheet
|
|
308
|
+
file goPESTslaves.xls. This command can be used alone as it will load existing
|
|
309
|
+
goPESTslaves.json.
|
|
310
|
+
|
|
311
|
+
This command runs locally, and does not utilise any slurm/srun/queue facilities.
|
|
312
|
+
|
|
313
|
+
This message can be printed with the "--help" argument. It is possible to
|
|
314
|
+
include more than one option at a time.
|
|
315
|
+
'''
|
|
316
|
+
|
|
317
|
+
def check_slaves_cli(argv=[]):
|
|
318
|
+
if '--help' in argv or len(argv) <= 1:
|
|
319
|
+
print(hlp)
|
|
320
|
+
exit(0)
|
|
321
|
+
else:
|
|
322
|
+
# overwrite slave_dirs
|
|
323
|
+
if '--dir' in argv:
|
|
324
|
+
iarg = argv.index('--dir') + 1
|
|
325
|
+
try:
|
|
326
|
+
spath = argv[iarg]
|
|
327
|
+
except IndexError:
|
|
328
|
+
raise Exception('--dir argument needs to be followed by path_to_slaves')
|
|
329
|
+
else:
|
|
330
|
+
spath = config['pest']['slave_dirs']
|
|
331
|
+
# overwrite PEST exe
|
|
332
|
+
if "--pest-exe" in argv:
|
|
333
|
+
iarg = argv.index('--pest-exe') + 1
|
|
334
|
+
try:
|
|
335
|
+
pexe = argv[iarg]
|
|
336
|
+
except IndexError:
|
|
337
|
+
raise Exception('--pest-exe argument needs to be followed by pest_executable')
|
|
338
|
+
# modify the shared module variable
|
|
339
|
+
config['pest']['dir'] = ''
|
|
340
|
+
config['pest']['executable'] = pexe
|
|
341
|
+
else:
|
|
342
|
+
# do nothing, keeps original setting
|
|
343
|
+
pass
|
|
344
|
+
# tasks
|
|
345
|
+
nopts, tasks = 0, []
|
|
346
|
+
if "--status" in argv:
|
|
347
|
+
nopts += 1
|
|
348
|
+
tasks.append('status')
|
|
349
|
+
if "--end-time" in argv:
|
|
350
|
+
nopts += 1
|
|
351
|
+
tasks.append('end-time')
|
|
352
|
+
if "--obj-fn" in argv:
|
|
353
|
+
nopts += 1
|
|
354
|
+
tasks.append('obj-fn')
|
|
355
|
+
xls = False
|
|
356
|
+
if "--export-xls" in argv:
|
|
357
|
+
nopts += 1
|
|
358
|
+
xls = True
|
|
359
|
+
if nopts == 0:
|
|
360
|
+
print(hlp)
|
|
361
|
+
print('Please specify at least one task to perform.')
|
|
362
|
+
exit(0)
|
|
363
|
+
|
|
364
|
+
start_time = time.time()
|
|
365
|
+
if not all([os.path.exists(spath), os.path.isdir(spath)]):
|
|
366
|
+
raise Exception('Specified path_to_slaves needs to be a valid directory: %s' % spath)
|
|
367
|
+
|
|
368
|
+
task_fn = {
|
|
369
|
+
'end-time': check_sim_ends,
|
|
370
|
+
'status': check_run_status,
|
|
371
|
+
'obj-fn': get_obj_fn,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
slave_paths = sorted([p for p in glob.glob(os.path.join(spath, '*')) if os.path.isdir(p)])
|
|
375
|
+
slave_names = [os.path.basename(sp) for sp in slave_paths]
|
|
376
|
+
|
|
377
|
+
ncpu = max(1, cpu_count() - 1)
|
|
378
|
+
print('Starting %i workers to process %i slaves' % (ncpu, len(slave_paths)))
|
|
379
|
+
pool = Pool(ncpu, initializer=init_slave, initargs=(config, runtime))
|
|
380
|
+
|
|
381
|
+
fout = 'goPESTslaves.json'
|
|
382
|
+
if os.path.exists(fout):
|
|
383
|
+
with open(fout, 'r') as f:
|
|
384
|
+
data = json.load(f)
|
|
385
|
+
else:
|
|
386
|
+
data = {}
|
|
387
|
+
|
|
388
|
+
for task in tasks:
|
|
389
|
+
print('Checking %s...' % task)
|
|
390
|
+
results = pool.map(task_fn[task], slave_paths)
|
|
391
|
+
data = nested_dict_update(data, dict(zip(slave_names, results)))
|
|
392
|
+
|
|
393
|
+
with open(fout, 'w') as f:
|
|
394
|
+
json.dump(data, f, indent=4, sort_keys=True)
|
|
395
|
+
|
|
396
|
+
if xls:
|
|
397
|
+
export_xls(data)
|
|
398
|
+
|
|
399
|
+
print('Finished after %f seconds' % (time.time() - start_time))
|
|
400
|
+
|
|
401
|
+
if __name__ == '__main__':
|
|
402
|
+
check_slaves_cli(sys.argv)
|
gopest/commands.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from gopest import __version__
|
|
4
|
+
|
|
5
|
+
title = """
|
|
6
|
+
goPEST - Interfacing PEST with Waiwera and (AU)TOUGH2 simulators
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
version = """Version: (%s)
|
|
10
|
+
""" % __version__
|
|
11
|
+
|
|
12
|
+
hlp = """
|
|
13
|
+
Usage: gopest COMMAND [ARGUMENTS]
|
|
14
|
+
|
|
15
|
+
Supported COMMANDs:
|
|
16
|
+
help
|
|
17
|
+
init [--no-copy][--no-par][--no-obs] (make_case_pst)
|
|
18
|
+
submit (submit_beopest)
|
|
19
|
+
run (run_beopest)
|
|
20
|
+
par (goPESTpar)
|
|
21
|
+
obs (goPESTobs)
|
|
22
|
+
run-pest-model (pest_model)
|
|
23
|
+
run-forward (run_ns_pr)
|
|
24
|
+
save-iter-files (rename_latest_files)
|
|
25
|
+
check-slaves (check_slaves)
|
|
26
|
+
|
|
27
|
+
Important files for goPEST to work:
|
|
28
|
+
goPESTconfig.toml
|
|
29
|
+
This is the main configuration file.
|
|
30
|
+
goPESTpar.list
|
|
31
|
+
This controls model parameters included for PEST.
|
|
32
|
+
goPESTobs.list
|
|
33
|
+
This controls observations for PEST.
|
|
34
|
+
|
|
35
|
+
University of Auckland, 2012, 2022
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def gopest_cli():
|
|
39
|
+
print(title)
|
|
40
|
+
argc = len(sys.argv)
|
|
41
|
+
if argc < 2:
|
|
42
|
+
print(version + hlp)
|
|
43
|
+
else:
|
|
44
|
+
if sys.argv[1] == 'help':
|
|
45
|
+
print(version + hlp)
|
|
46
|
+
else:
|
|
47
|
+
# NOTE loading gopest.common checks goPESTconfig.toml
|
|
48
|
+
import gopest.common
|
|
49
|
+
import gopest.par
|
|
50
|
+
import gopest.obs
|
|
51
|
+
import gopest.pest_model
|
|
52
|
+
import gopest.run_ns_pr
|
|
53
|
+
import gopest.submit_beopest
|
|
54
|
+
import gopest.run_beopest
|
|
55
|
+
import gopest.make_case_pst
|
|
56
|
+
import gopest.rename_latest_files
|
|
57
|
+
import gopest.check_slaves
|
|
58
|
+
cmds = {
|
|
59
|
+
'par': gopest.par.goPESTpar,
|
|
60
|
+
'obs': gopest.obs.goPESTobs,
|
|
61
|
+
'run-pest-model': gopest.pest_model.main_cli,
|
|
62
|
+
'run-forward': gopest.run_ns_pr.main_cli,
|
|
63
|
+
'submit': gopest.submit_beopest.submit_cli,
|
|
64
|
+
'run': gopest.run_beopest.run_cli,
|
|
65
|
+
'init': gopest.make_case_pst.make_case_cli,
|
|
66
|
+
'save-iter-files': gopest.rename_latest_files.rename_latest_files,
|
|
67
|
+
'check-slaves': gopest.check_slaves.check_slaves_cli,
|
|
68
|
+
}
|
|
69
|
+
if sys.argv[1] not in cmds:
|
|
70
|
+
print(version + hlp)
|
|
71
|
+
print('Error! COMMAND not recognised.')
|
|
72
|
+
exit(1)
|
|
73
|
+
cmds[sys.argv[1]](sys.argv[1:])
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
- NOTE good reference on designing CLI command names:
|
|
77
|
+
https://smallstep.com/blog/the-poetics-of-cli-command-names/
|
|
78
|
+
|
|
79
|
+
- TODO it's probably not necessary to have so many commands
|
|
80
|
+
"""
|