• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

timcera / hspf_utils / 4170394047

pending completion
4170394047

push

github

Tim Cera
build: update dependency versions

215 of 368 relevant lines covered (58.42%)

2.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.66
/src/hspf_utils/hspf_utils.py
1
"""Utility functions to work with HSPF models for mass balance tables."""
2

3
import os
4✔
4
import re
4✔
5
import sys
4✔
6
import warnings
4✔
7

8
import numpy as np
4✔
9
import pandas as pd
4✔
10
from cltoolbox import command, main
4✔
11
from hspfbintoolbox.hspfbintoolbox import extract
4✔
12
from toolbox_utils import tsutils
4✔
13

14
docstrings = {
4✔
15
    "hbn": r"""hbn : str
16
        This is the binary output file containing PERLND and IMPLND
17
        information.  This should be the binary output file created by the
18
        `uci` file.""",
19
    "uci": r"""uci
20
        [optional, defaults to None]
21

22
        This uci file will be read to determine all of the areas and other
23
        aspects of the model.  If available it will read the land cover names
24
        from the PERLND GEN-INFO table.
25

26
        The `uci` keyword and file is required if you want the water balance
27
        area-weighted between land covers.
28

29
        WARNING:  The areas used come only from the SCHEMATIC block and if
30
        areas are adjusted by SPECIAL ACTIONS those changes are not used in the
31
        mass balance.""",
32
    "year": r"""year
33
        [optional, defaults to None]
34

35
        If None the water balance would cover the period of simulation.
36
        Otherwise the year for the water balance.""",
37
    "ofilename": r"""ofilename
38
        [optional, defaults to '']
39

40
        If empty string '', then prints to stdout, else prints to
41
        `ofilename`.""",
42
    "modulus": r"""modulus : int
43
        [optional, defaults to 20]
44

45
        Usual setup of a HSPF model has PERLND 1, 21, 41, ...etc. represent
46
        land cover 1 in different sub-watersheds and 2, 22, 42, ...etc
47
        represent land cover 2 in different sub-watersheds, ...etc.
48

49
        The remainder of the PERLND label divided by the modulus is the land
50
        cover number.""",
51
    "tablefmt": r"""tablefmt : str
52
        [optional, default is 'cvs_nos']
53

54
        The table format.  Can be one of 'csv', 'tsv', 'csv_nos', 'tsv_nos',
55
        'plain', 'simple', 'github', 'grid', 'fancy_grid', 'pipe', 'orgtbl',
56
        'jira', 'presto', 'psql', 'rst', 'mediawiki', 'moinmoin', 'youtrack',
57
        'html', 'latex', 'latex_raw', 'latex_booktabs' and
58
        'textile'.""",
59
    "float_format": r"""float_format : str
60
        [optional, default is '.2f']
61

62
        The format for floating point numbers in the output table.""",
63
    "index_prefix": r"""index_prefix
64
        [optional, defaults to '']
65

66
        A string prepended to the PERLND code, which would allow being
67
        run on different models and collected into one dataset by
68
        creating a unique ID.""",
69
    "index_delimiter": r"""index_delimiter: str
70
        [optional, defaults to '-']
71

72
        Useful to separate the `index_prefix` from the PERLND/IMPLND number.
73
        """,
74
    "constituent": r"""constituent: str
75
        [optional, defaults to 'flow']
76

77
        The constituent to summarize in the table.
78

79
        Currently available constituents are: 'flow' for PWATER/IWATER and
80
        'qual' for PQUAL/IQUAL.
81

82
        if 'qual' is chosen, then the option 'qualnames' specifies the names to
83
        be found in the HBN file.
84
        """,
85
    "qualnames": r"""qualnames : str
86
        [optional, defaults to '']
87

88
        If 'constituent' is 'qual, then this is a comma-separated
89
        list of constituent names to be found in the HBN file.
90

91
        Example:
92
            --qualnames 'TOTAL N','TOTAL P'
93

94
        This will find PQUAL/IQUAL variables named 'SOQUAL-TOTAL N', etc, which
95
        occurs if the QUALID in QUAL-PROPS is 'TOTAL N'.
96
        """,
97
}
98

99
_mass_balance = {
4✔
100
    ("flow", "detailed", True): (
101
        ["SUPY", [("SUPY", "PERLND"), ("SUPY", "IMPLND"), ("IRRAPP6", "PERLND")]],
102
        ["SURLI", [("SURLI", "PERLND")]],
103
        ["UZLI", [("UZLI", "PERLND")]],
104
        ["LZLI", [("LZLI", "PERLND")]],
105
        ["", [("", "")]],
106
        ["SURO: PERVIOUS", [("SURO", "PERLND")]],
107
        ["SURO: IMPERVIOUS", [("SURO", "IMPLND")]],
108
        ["SURO: COMBINED", [("SURO", "PERLND"), ("SURO", "IMPLND")]],
109
        ["IFWO", [("IFWO", "PERLND")]],
110
        ["AGWO", [("AGWO", "PERLND")]],
111
        ["", [("", "")]],
112
        ["AGWI", [("AGWI", "PERLND")]],
113
        ["IGWI", [("IGWI", "PERLND")]],
114
        ["", [("", "")]],
115
        ["CEPE", [("CEPE", "PERLND")]],
116
        ["UZET", [("UZET", "PERLND")]],
117
        ["LZET", [("LZET", "PERLND")]],
118
        ["AGWET", [("AGWET", "PERLND")]],
119
        ["BASET", [("BASET", "PERLND")]],
120
        ["SURET", [("SURET", "PERLND")]],
121
        ["", [("", "")]],
122
        ["PERO", [("PERO", "PERLND")]],
123
        ["IGWI", [("IGWI", "PERLND")]],
124
        ["TAET: PERVIOUS", [("TAET", "PERLND")]],
125
        ["IMPEV: IMPERVIOUS", [("IMPEV", "IMPLND")]],
126
        ["ET: COMBINED", [("TAET", "PERLND"), ("IMPEV", "IMPLND")]],
127
        ["", [("", "")]],
128
        ["PET", [("PET", "PERLND"), ("PET", "IMPLND")]],
129
        ["", [("", "")]],
130
        ["PERS", [("PERS", "PERLND")]],
131
    ),
132
    ("flow", "detailed", False): (
133
        ["SUPY", [("SUPY", "PERLND")]],
134
        ["SURLI", [("SURLI", "PERLND")]],
135
        ["UZLI", [("UZLI", "PERLND")]],
136
        ["LZLI", [("LZLI", "PERLND")]],
137
        ["", [("", "")]],
138
        ["SURO: PERVIOUS", [("SURO", "PERLND")]],
139
        ["SURO: IMPERVIOUS", [("SURO", "IMPLND")]],
140
        ["IFWO", [("IFWO", "PERLND")]],
141
        ["AGWO", [("AGWO", "PERLND")]],
142
        ["", [("", "")]],
143
        ["AGWI", [("AGWI", "PERLND")]],
144
        ["IGWI", [("IGWI", "PERLND")]],
145
        ["", [("", "")]],
146
        ["CEPE", [("CEPE", "PERLND")]],
147
        ["UZET", [("UZET", "PERLND")]],
148
        ["LZET", [("LZET", "PERLND")]],
149
        ["AGWET", [("AGWET", "PERLND")]],
150
        ["BASET", [("BASET", "PERLND")]],
151
        ["SURET", [("SURET", "PERLND")]],
152
        ["", [("", "")]],
153
        ["PERO", [("PERO", "PERLND")]],
154
        ["IGWI", [("IGWI", "PERLND")]],
155
        ["TAET: PERVIOUS", [("TAET", "PERLND")]],
156
        ["IMPEV: IMPERVIOUS", [("IMPEV", "IMPLND")]],
157
        ["", [("", "")]],
158
        ["PET", [("PET", "PERLND")]],
159
        ["", [("", "")]],
160
        ["PERS", [("PERS", "PERLND")]],
161
    ),
162
    ("flow", "summary", True): (
163
        [
164
            "Rainfall and irrigation",
165
            [
166
                ("SUPY", "PERLND"),
167
                ("SUPY", "IMPLND"),
168
                ("SURLI", "PERLND"),
169
                ("UZLI", "PERLND"),
170
                ("LZLI", "PERLND"),
171
            ],
172
        ],
173
        ["", [("", "")]],
174
        [
175
            "Runoff:Pervious",
176
            [("PERO", "PERLND")],
177
        ],
178
        ["Runoff:Impervious", [("SURO", "IMPLND")]],
179
        [
180
            "Runoff:Combined",
181
            [
182
                ("PERO", "PERLND"),
183
                ("SURO", "IMPLND"),
184
            ],
185
        ],
186
        ["", [("", "")]],
187
        ["Deep recharge", [("IGWI", "PERLND")]],
188
        ["", [("", "")]],
189
        ["Evaporation:Pervious", [("TAET", "PERLND")]],
190
        ["Evaporation:Impervious", [("IMPEV", "IMPLND")]],
191
        ["Evaporation:Combined", [("TAET", "PERLND"), ("IMPEV", "IMPLND")]],
192
    ),
193
    ("flow", "summary", False): (
194
        [
195
            "Rainfall and irrigation",
196
            [
197
                ("SUPY", "PERLND"),
198
                ("SUPY", "IMPLND"),
199
                ("IRRAPP6", "PERLND"),
200
            ],
201
        ],
202
        ["", [("", "")]],
203
        [
204
            "Runoff:Pervious",
205
            [("PERO", "PERLND")],
206
        ],
207
        ["Runoff:Impervious", [("SURO", "IMPLND")]],
208
        ["", [("", "")]],
209
        ["Deep recharge", [("IGWI", "PERLND")]],
210
        ["", [("", "")]],
211
        ["Evaporation:Pervious", [("TAET", "PERLND")]],
212
        ["Evaporation:Impervious", [("IMPEV", "IMPLND")]],
213
    ),
214
    ("qual", "detailed", False): (
215
        # ["SOQO: PERVIOUS", [("SOQO", "PERLND")]],
216
        # ["SOQO: IMPERVIOUS", [("SOQO", "IMPLND")]],
217
        # ["WASHQS: PERVIOUS", [("WASHQS", "PERLND")]],
218
        # ["WASHQS: IMPERVIOUS", [("WASHQS", "IMPLND")]],
219
        # ["SOQS: PERVIOUS", [("SOQS", "PERLND")]],
220
        # ["SOQS: IMPERVIOUS", [("SOQS", "IMPLND")]],
221
        ["SOQUAL: PERVIOUS", [("SOQUAL", "PERLND")]],
222
        ["SOQUAL: IMPERVIOUS", [("SOQUAL", "IMPLND")]],
223
        ["IOQUAL", [("IOQUAL", "PERLND")]],
224
        ["AOQUAL", [("AOQUAL", "PERLND")]],
225
        ["POQUAL", [("POQUAL", "PERLND")]],
226
    ),
227
    ("qual", "detailed", True): (
228
        # ["SOQO: PERVIOUS", [("SOQO", "PERLND")]],
229
        # ["SOQO: IMPERVIOUS", [("SOQO", "IMPLND")]],
230
        # ["SOQO: COMBINED", [("SOQO", "PERLND"),("SOQO", "IMPLND")]],
231
        # ["WASHQS: PERVIOUS", [("WASHQS", "PERLND")]],
232
        # ["WASHQS: IMPERVIOUS", [("WASHQS", "IMPLND")]],
233
        # ["WASHQS: COMBINED", [("WASHQS", "PERLND"),("WASHQS", "IMPLND")]],
234
        # ["SCRQS", [("SCRQS", "PERLND")]],
235
        # ["SOQS: PERVIOUS", [("SOQS", "PERLND")]],
236
        # ["SOQS: IMPERVIOUS", [("SOQS", "IMPLND")]],
237
        # ["SOQS: COMBINED", [("SOQS", "PERLND"),("SOQS", "IMPLND")]],
238
        ["SOQUAL: PERVIOUS", [("SOQUAL", "PERLND")]],
239
        ["SOQUAL: IMPERVIOUS", [("SOQUAL", "IMPLND")]],
240
        ["SOQUAL: COMBINED", [("SOQUAL", "PERLND"), ("SOQUAL", "IMPLND")]],
241
        ["IOQUAL", [("IOQUAL", "PERLND")]],
242
        ["AOQUAL", [("AOQUAL", "PERLND")]],
243
        ["POQUAL", [("POQUAL", "PERLND")]],
244
    ),
245
    ("qual", "summary", False): (
246
        ["SOQUAL: PERVIOUS", [("SOQUAL", "PERLND")]],
247
        ["SOQUAL: IMPERVIOUS", [("SOQUAL", "IMPLND")]],
248
        ["IOQUAL", [("IOQUAL", "PERLND")]],
249
        ["AOQUAL", [("AOQUAL", "PERLND")]],
250
        ["POQUAL", [("POQUAL", "PERLND")]],
251
    ),
252
    ("qual", "summary", True): (
253
        ["SOQUAL: PERVIOUS", [("SOQUAL", "PERLND")]],
254
        ["SOQUAL: IMPERVIOUS", [("SOQUAL", "IMPLND")]],
255
        ["SOQUAL: COMBINED", [("SOQUAL", "PERLND"), ("SOQUAL", "IMPLND")]],
256
        ["IOQUAL", [("IOQUAL", "PERLND")]],
257
        ["AOQUAL", [("AOQUAL", "PERLND")]],
258
        ["POQUAL", [("POQUAL", "PERLND")]],
259
    ),
260
}
261

262

263
@command()
4✔
264
def about():
4✔
265
    """Display version number and system information."""
266
    tsutils.about(__name__)
×
267

268

269
def _give_negative_warning(df):
4✔
270
    testpdf = df < 0
4✔
271
    if testpdf.any(None):
4✔
272
        warnings.warn(
×
273
            tsutils.error_wrapper(
274
                f"""
275
            This may be OK, but FYI there are negative values at:
276

277
            {df.loc[testpdf.any(1), testpdf.any(0)]}
278
            """
279
            )
280
        )
281

282

283
def process(uci, hbn, elements, year, ofilename, modulus):
4✔
284
    from hspfbintoolbox.hspfbintoolbox import extract
4✔
285

286
    if ofilename:
4✔
287
        sys.stdout = open(ofilename, "w")
×
288

289
    try:
4✔
290
        year = int(year)
4✔
291
    except TypeError:
4✔
292
        pass
4✔
293

294
    lcnames = dict(zip(range(modulus + 1, 1), zip(range(modulus + 1, 1))))
4✔
295
    inverse_lcnames = dict(zip(range(modulus + 1, 1), zip(range(modulus + 1, 1))))
4✔
296
    inverse_lc = {}
4✔
297

298
    lnds = {}
4✔
299

300
    if uci is not None:
4✔
301
        with open(uci) as fp:
4✔
302
            content = fp.readlines()
4✔
303

304
        if not os.path.exists(hbn):
4✔
305
            raise ValueError(
×
306
                f"""
307
*
308
*   File {hbn} does not exist.
309
*
310
"""
311
            )
312

313
        content = [i[:80] for i in content]
4✔
314
        content = [i.rstrip() for i in content]
4✔
315

316
        schematic_start = content.index("SCHEMATIC")
4✔
317
        schematic_end = content.index("END SCHEMATIC")
4✔
318
        schematic = content[schematic_start : schematic_end + 1]
4✔
319

320
        perlnd_start = content.index("PERLND")
4✔
321
        perlnd_end = content.index("END PERLND")
4✔
322
        perlnd = content[perlnd_start : perlnd_end + 1]
4✔
323

324
        pgeninfo_start = perlnd.index("  GEN-INFO")
4✔
325
        pgeninfo_end = perlnd.index("  END GEN-INFO")
4✔
326
        pgeninfo = perlnd[pgeninfo_start : pgeninfo_end + 1]
4✔
327

328
        masslink_start = content.index("MASS-LINK")
4✔
329
        masslink_end = content.index("END MASS-LINK")
4✔
330
        masslink = content[masslink_start : masslink_end + 1]
4✔
331

332
        lcnames = {}
4✔
333
        inverse_lcnames = {}
4✔
334
        inverse_lc = {}
4✔
335
        for line in pgeninfo[1:-1]:
4✔
336
            if "***" in line:
4✔
337
                continue
4✔
338
            if "" == line.strip():
4✔
339
                continue
×
340
            try:
4✔
341
                _ = int(line[5:10])
4✔
342
                continue
4✔
343
            except ValueError:
×
344
                pass
×
345
            lcnames.setdefault(line[10:30].strip(), []).append(int(line[:5]))
×
346
            inverse_lcnames[int(line[:5])] = line[10:30].strip()
×
347
            inverse_lc[int(line[:5]) % modulus] = line[10:30].strip()
×
348

349
        masslink = [i for i in masslink if "***" not in i]
4✔
350
        masslink = [i for i in masslink if len(i.strip()) > 0]
4✔
351
        masslink = " ".join(masslink)
4✔
352
        mlgroups = re.findall(
4✔
353
            r"  MASS-LINK +?([0-9]+).*?LND     [PI]WATER [PS][EU]RO.*?  END MASS-LINK +?\1 ",
354
            masslink,
355
        )
356

357
        for line in schematic[3:-1]:
4✔
358
            if "***" in line:
4✔
359
                continue
4✔
360
            if "" == line:
4✔
361
                continue
4✔
362
            words = line.split()
4✔
363
            if words[0] in ["PERLND", "IMPLND"] and words[5] in mlgroups:
4✔
364
                lnds[(words[0], int(words[1]))] = lnds.setdefault(
4✔
365
                    (words[0], int(words[1])), 0.0
366
                ) + float(words[2])
367

368
    try:
4✔
369
        pdf = extract(hbn, "yearly", ",,,")
4✔
370
    except ValueError:
×
371
        raise ValueError(
×
372
            tsutils.error_wrapper(
373
                f"""
374
                The binary file "{hbn}" does not have consistent ending months
375
                between PERLND and IMPLND.  This could be caused by the BYREND
376
                (Binary YeaR END) being set differently in the
377
                PERLND:BINARY-INFO and IMPLND:BINARY-INFO, or you could have
378
                the PRINT-INFO bug.  To work around the PRINT-INFO bug, add
379
                a PERLND PRINT-INFO block, setting the PYREND here will
380
                actually work in the BINARY-INFO block.
381
                """
382
            )
383
        )
384

385
    if year is not None:
4✔
386
        pdf = pd.DataFrame(pdf.loc[f"{year}", :]).T
×
387
    pdf = pdf[[i for i in pdf.columns if "PERLND" in i or "IMPLND" in i]]
4✔
388

389
    mindex = [i.split("_") for i in pdf.columns]
4✔
390
    mindex = [(i[0], int(i[1]), i[2], int(i[1]) % modulus) for i in mindex]
4✔
391
    mindex = pd.MultiIndex.from_tuples(mindex, names=["op", "number", "balterm", "lc"])
4✔
392
    pdf.columns = mindex
4✔
393
    pdf = pdf.sort_index(axis="columns")
4✔
394
    mindex = pdf.columns
4✔
395
    aindex = [(i[0], i[1]) for i in pdf.columns]
4✔
396
    mindex = [
4✔
397
        (
398
            i[0],
399
            int(i[1]),
400
            i[2],
401
            int(i[1]) % modulus,
402
            float(lnds.setdefault(j, 0.0)),
403
            str(inverse_lcnames.setdefault(int(i[1]), "")),
404
        )
405
        for i, j in zip(mindex, aindex)
406
    ]
407
    mindex = pd.MultiIndex.from_tuples(
4✔
408
        mindex, names=["op", "number", "balterm", "lc", "area", "lcname"]
409
    )
410
    pdf.columns = mindex
4✔
411

412
    nsum = {}
4✔
413
    areas = {}
4✔
414
    namelist = {}
4✔
415
    setl = [i[1] for i in elements]
4✔
416
    setl = [item for sublist in setl for item in sublist]
4✔
417
    for lue in ["PERLND", "IMPLND"]:
4✔
418
        for bterm in [i[0] for i in setl if i[0]]:
4✔
419
            for lc in list(range(1, modulus + 1)):
4✔
420
                try:
4✔
421
                    subset = pdf.loc[
4✔
422
                        :, (lue, slice(None), bterm, lc, slice(None), slice(None))
423
                    ]
424
                except KeyError:
4✔
425
                    continue
4✔
426

427
                _give_negative_warning(subset)
4✔
428

429
                if uci is None:
4✔
430
                    if subset.empty is True:
×
431
                        nsum[(lue, lc, bterm)] = 0.0
×
432
                        if (lue, lc) not in namelist:
×
433
                            namelist[(lue, lc)] = ""
×
434
                    else:
435
                        nsum[(lue, lc, bterm)] = subset.mean(axis="columns").mean()
×
436
                        namelist[(lue, lc)] = inverse_lc.setdefault(lc, lc)
×
437
                else:
438
                    sareas = subset.columns.get_level_values("area")
4✔
439
                    ssareas = sum(sareas)
4✔
440
                    if (lue, lc) not in areas:
4✔
441
                        areas[(lue, lc)] = ssareas
4✔
442

443
                    if subset.empty is True or ssareas == 0:
4✔
444
                        nsum[(lue, lc, bterm)] = 0.0
×
445
                        if (lue, lc) not in namelist:
×
446
                            namelist[(lue, lc)] = ""
×
447
                    else:
448
                        fa = sareas / areas[(lue, lc)]
4✔
449
                        nsum[(lue, lc, bterm)] = (
4✔
450
                            (subset * fa).sum(axis="columns").mean()
451
                        )
452
                        namelist[(lue, lc)] = inverse_lc.setdefault(lc, lc)
4✔
453

454
    newnamelist = []
4✔
455
    for key, value in sorted(namelist.items()):
4✔
456
        if key[0] != "PERLND":
4✔
457
            continue
4✔
458
        if key[1] == value:
4✔
459
            newnamelist.append(f"{key[1]}")
4✔
460
        else:
461
            newnamelist.append(f"{key[1]}-{value}")
×
462

463
    printlist = []
4✔
464
    printlist.append(["BALANCE TERM"] + newnamelist + ["ALL"])
4✔
465

466
    mapipratio = {}
4✔
467
    mapipratio["PERLND"] = 1.0
4✔
468
    mapipratio["IMPLND"] = 1.0
4✔
469

470
    if uci is not None:
4✔
471
        pareas = []
4✔
472
        pnl = []
4✔
473
        iareas = []
4✔
474
        for nloper, nllc in namelist:
4✔
475
            if nloper == "PERLND":
4✔
476
                pnl.append((nloper, nllc))
4✔
477
                pareas.append(areas[("PERLND", nllc)])
4✔
478
        # If there is a PERLND there must be a IMPLND.
479
        for _, pllc in pnl:
4✔
480
            try:
4✔
481
                iareas.append(areas[("IMPLND", pllc)])
4✔
482
            except KeyError:
4✔
483
                iareas.append(0.0)
4✔
484
        ipratio = np.array(iareas) / (np.array(pareas) + np.array(iareas))
4✔
485
        ipratio = np.nan_to_num(ipratio)
4✔
486
        ipratio = np.pad(ipratio, (0, len(pareas) - len(iareas)), "constant")
4✔
487
        sumareas = sum(pareas) + sum(iareas)
4✔
488

489
        percent_areas = {}
4✔
490
        percent_areas["PERLND"] = np.array(pareas) / sumareas * 100
4✔
491
        percent_areas["IMPLND"] = np.array(iareas) / sumareas * 100
4✔
492
        percent_areas["COMBINED"] = percent_areas["PERLND"] + percent_areas["IMPLND"]
4✔
493

494
        # printlist.append(["PERVIOUS"])
495
        printlist.append(
4✔
496
            ["PERVIOUS AREA(acres)"]
497
            + [i if i > 0 else None for i in pareas]
498
            + [sum(pareas)]
499
        )
500

501
        printlist.append(
4✔
502
            ["PERVIOUS AREA(%)"]
503
            + [i if i > 0 else None for i in percent_areas["PERLND"]]
504
            + [sum(percent_areas["PERLND"])]
505
        )
506

507
        printlist.append([])
4✔
508
        # printlist.append(["IMPERVIOUS"])
509
        printlist.append(
4✔
510
            ["IMPERVIOUS AREA(acres)"]
511
            + [i if i > 0 else None for i in iareas]
512
            + [sum(iareas)]
513
        )
514

515
        printlist.append(
4✔
516
            ["IMPERVIOUS AREA(%)"]
517
            + [i if i > 0 else None for i in percent_areas["IMPLND"]]
518
            + [sum(percent_areas["IMPLND"])]
519
        )
520
        printlist.append([])
4✔
521

522
        mapipratio["PERLND"] = 1.0 - ipratio
4✔
523
        mapipratio["IMPLND"] = ipratio
4✔
524

525
    mapr = {}
4✔
526
    mapr["PERLND"] = 1.0
4✔
527
    mapr["IMPLND"] = 1.0
4✔
528

529
    for term, op in elements:
4✔
530
        if not term:
4✔
531
            # term is None - insert a blank line
532
            printlist.append([])
4✔
533
            continue
4✔
534

535
        test = [i[1] for i in op]
4✔
536
        if "IMPLND" in test and "PERLND" in test:
4✔
537
            maprat = mapipratio
4✔
538
            sumop = "COMBINED"
4✔
539
        else:
540
            maprat = mapr
4✔
541
            sumop = test[0]
4✔
542

543
        te = [0.0]
4✔
544
        for sterm, operation in op:
4✔
545
            try:
4✔
546
                tmp = np.array(
4✔
547
                    [nsum[(*i, sterm)] for i in sorted(namelist) if i[0] == operation]
548
                )
549
                if uci is not None:
4✔
550
                    tmp = (
4✔
551
                        np.pad(tmp, (0, len(pareas) - len(tmp)), "constant")
552
                        * maprat[operation]
553
                    )
554
                te = te + tmp
4✔
555
            except (KeyError, ValueError):
4✔
556
                pass
4✔
557
        if uci is None:
4✔
558
            te = [term] + [i for i in te] + [sum(te) / len(te)]
×
559
            # + [i if i > 0 else None for i in te]
560
        else:
561
            # this line assumes iareas are all at the beginning - fix?
562
            nte = np.pad(te, (0, len(iareas) - len(te)), "constant")
4✔
563
            te = [term] + [i for i in nte] + [sum(nte * percent_areas[sumop]) / 100]
4✔
564
            # + [i if i > 0 else None for i in nte]
565
        printlist.append(te)
4✔
566
    df = pd.DataFrame(printlist)
4✔
567
    df.columns = df.iloc[0, :]
4✔
568
    df = df[1:]
4✔
569
    df = df.set_index("BALANCE TERM")
4✔
570
    return df
4✔
571

572

573
def process_qual_names(qualnames, tempelements):
4✔
574
    qnames = qualnames.split(",")
×
575
    elemlist = []
×
576
    for qname in qnames:
×
577
        for tmp in tempelements:
×
578
            tmplabel = tmp[0]
×
579
            labelpos = min(len(tmplabel.split(" ")[0]), len(tmplabel.split(":")[0]))
×
580
            label = tmplabel[:labelpos] + "-" + qname
×
581
            if len(tmplabel) > labelpos:
×
582
                label = label + tmplabel[labelpos:]
×
583
            tmpvar = tmp[1]
×
584
            tmpvarnametuple = tmpvar[0]
×
585
            tmpvarname = tmpvarnametuple[0]
×
586
            tmpopname = tmpvarnametuple[1]
×
587
            varpos = len(tmpvarname)
×
588
            varname = tmpvarname[:varpos] + "-" + qname
×
589
            if len(tmpvarname) > varpos:
×
590
                varname = varname + tmpvarname[varpos:]
×
591
            varnametuple = (varname, tmpopname)
×
592
            varnamelist = []
×
593
            varnamelist.append(varnametuple)
×
594
            var = [label, varnamelist]
×
595
            elemlist.append(var)
×
596
            elements = tuple(elemlist)
×
597
    return elements
×
598

599

600
@command("detailed")
4✔
601
@tsutils.doc(docstrings)
4✔
602
def _detailed_cli(
4✔
603
    hbn,
604
    uci=None,
605
    year=None,
606
    ofilename="",
607
    modulus=20,
608
    tablefmt="csv_nos",
609
    float_format=".2f",
610
    constituent="flow",
611
    qualnames="",
612
):
613
    """Develops a detailed water or mass balance.
614

615
    Parameters
616
    ----------
617
    ${hbn}
618
    ${uci}
619
    ${year}
620
    ${ofilename}
621
    ${modulus}
622
    ${tablefmt}
623
    ${float_format}
624
    ${constituent}
625
    ${qualnames}
626
    """
627
    tsutils.printiso(
×
628
        detailed(
629
            hbn,
630
            uci=uci,
631
            year=year,
632
            ofilename=ofilename,
633
            modulus=modulus,
634
            constituent=constituent,
635
            qualnames=qualnames,
636
        ),
637
        float_format=float_format,
638
        headers="keys",
639
        tablefmt=tablefmt,
640
    )
641

642

643
@tsutils.copy_doc(_detailed_cli)
4✔
644
def detailed(
4✔
645
    hbn,
646
    uci=None,
647
    year=None,
648
    ofilename="",
649
    modulus=20,
650
    constituent="flow",
651
    qualnames="",
652
):
653
    """Develops a detailed water balance."""
654

655
    elements = _mass_balance[(constituent, "detailed", bool(uci))]
×
656
    if constituent == "qual":
×
657
        elements = process_qual_names(qualnames, elements)
×
658
    return process(uci, hbn, elements, year, ofilename, modulus)
×
659

660

661
@command("summary")
4✔
662
@tsutils.doc(docstrings)
4✔
663
def _summary_cli(
4✔
664
    hbn,
665
    uci=None,
666
    year=None,
667
    ofilename="",
668
    modulus=20,
669
    tablefmt="csv_nos",
670
    float_format=".2f",
671
    constituent="flow",
672
    qualnames="",
673
):
674
    """Develops a detailed water balance.
675

676
    Parameters
677
    ----------
678
    ${hbn}
679
    ${uci}
680
    ${year}
681
    ${ofilename}
682
    ${modulus}
683
    ${tablefmt}
684
    ${float_format}
685
    ${constituent}
686
    ${qualnames}
687
    """
688
    tsutils.printiso(
4✔
689
        summary(
690
            hbn,
691
            uci=uci,
692
            year=year,
693
            ofilename=ofilename,
694
            modulus=modulus,
695
            constituent=constituent,
696
            qualnames=qualnames,
697
        ),
698
        float_format=float_format,
699
        headers="keys",
700
        tablefmt=tablefmt,
701
    )
702

703

704
@tsutils.copy_doc(_summary_cli)
4✔
705
def summary(
4✔
706
    hbn,
707
    uci=None,
708
    year=None,
709
    ofilename="",
710
    modulus=20,
711
    constituent="flow",
712
    qualnames="",
713
):
714
    """Develops a summary mass balance.
715

716
    Parameters
717
    ----------
718
    ${hbn}
719
    ${uci}
720
    ${year}
721
    ${ofilename}
722
    ${modulus}
723
    ${constituent}
724
    ${qualnames}
725
    """
726
    elements = _mass_balance[(constituent, "summary", bool(uci))]
4✔
727
    if constituent == "qual":
4✔
728
        elements = process_qual_names(qualnames, elements)
×
729
    return process(uci, hbn, elements, year, ofilename, modulus)
4✔
730

731

732
@command("mapping")
4✔
733
@tsutils.doc(docstrings)
4✔
734
def _mapping_cli(hbn, year=None, tablefmt="csv_nos", index_prefix="", float_format="g"):
4✔
735
    """Develops a csv file appropriate for joining to a GIS layer.
736

737
    Parameters
738
    ----------
739
    ${hbn}
740

741
    ${year}
742

743
    ${ofilename}
744

745
    ${tablefmt}
746

747
    ${index_prefix}
748
        [optional, defaults to '']
749

750
        A string prepended to the PERLND code, which would allow being
751
        run on different models and collected into one dataset by
752
        creating a unique ID.
753

754
    ${float_format}
755
    """
756
    tsutils.printiso(
×
757
        mapping(
758
            hbn,
759
            year=year,
760
            index_prefix=index_prefix,
761
        ),
762
        float_format=float_format,
763
        headers="keys",
764
        tablefmt=tablefmt,
765
    )
766

767

768
@tsutils.copy_doc(_mapping_cli)
4✔
769
def mapping(hbn, year=None, index_prefix=""):
4✔
770
    try:
×
771
        pdf = extract(hbn, "yearly", ",,,")
×
772
    except ValueError as exc:
×
773
        raise ValueError(
×
774
            tsutils.error_wrapper(
775
                """
776
                The binary file does not have consistent ending months between
777
                PERLND and IMPLND.  This could be caused by the BYREND (Binary
778
                YeaR END) being set differently in the PERLND:BINARY-INFO and
779
                IMPLND:BINARY-INFO, or you could have the PRINT-INFO bug.  To
780
                work around the PRINT-INFO bug, add a PERLND PRINT-INFO block,
781
                setting the PYREND there will actually work in the BINARY-INFO
782
                block.
783
                """
784
            )
785
        ) from exc
786

787
    if year is not None:
×
788
        pdf = pd.DataFrame(pdf.loc[f"{year}", :]).T
×
789
    pdf = pdf[[i for i in pdf.columns if "PERLND" in i or "IMPLND" in i]]
×
790

791
    mindex = [i.split("_") for i in pdf.columns]
×
792
    mindex = [(i[0][0], int(i[1]), i[2]) for i in mindex]
×
793
    mindex = pd.MultiIndex.from_tuples(mindex, names=["op", "number", "balterm"])
×
794
    pdf.columns = mindex
×
795

796
    _give_negative_warning(pdf)
×
797

798
    pdf = pdf.mean(axis="index").to_frame()
×
799

800
    mindex = [("_".join([i[0], i[2]]), i[1]) for i in pdf.index]
×
801
    mindex = pd.MultiIndex.from_tuples(mindex, names=["balterm", "number"])
×
802
    pdf.index = mindex
×
803
    pdf = pdf.unstack("balterm")
×
804

805
    mindex = [i[1] for i in pdf.columns]
×
806
    pdf.columns = mindex
×
807

808
    pdf.index.name = "lue"
×
809

810
    if index_prefix:
×
811
        pdf.index = [index_prefix + str(i) for i in pdf.index]
×
812

813
    return pdf
×
814

815

816
@command("parameter")
4✔
817
@tsutils.doc(docstrings)
4✔
818
def _parameters_cli(
4✔
819
    uci,
820
    index_prefix="",
821
    index_delimiter="",
822
    modulus=20,
823
    tablefmt="csv_nos",
824
    float_format="g",
825
):
826
    """Develops a table of parameter values.
827

828
    Parameters
829
    ----------
830
    ${uci}
831
    ${index_prefix}
832
    ${index_delimiter}
833
    ${modulus}
834
    ${tablefmt}
835
    ${float_format}
836
    """
837
    tsutils.printiso(
×
838
        parameters(
839
            uci,
840
            index_prefix=index_prefix,
841
            index_delimiter=index_delimiter,
842
            modulus=modulus,
843
        ),
844
        float_format=float_format,
845
        headers="keys",
846
        tablefmt=tablefmt,
847
    )
848

849

850
@tsutils.copy_doc(_parameters_cli)
4✔
851
def parameters(
4✔
852
    uci,
853
    index_prefix="",
854
    index_delimiter="",
855
    modulus=20,
856
):
857
    blocklist = ["PWAT-PARM2", "PWAT-PARM3", "PWAT-PARM4"]  # , 'PWAT-STATE1']
×
858

859
    params = {}
×
860
    params["PWAT-PARM2"] = [
×
861
        "FOREST",
862
        "LZSN",
863
        "INFILT",
864
        "LSUR",
865
        "SLSUR",
866
        "KVARY",
867
        "AGWRC",
868
    ]
869
    params["PWAT-PARM3"] = [
×
870
        "PETMAX",
871
        "PETMIN",
872
        "INFEXP",
873
        "INFILD",
874
        "DEEPFR",
875
        "BASETP",
876
        "AGWETP",
877
    ]
878
    params["PWAT-PARM4"] = ["CEPSC", "UZSN", "NSUR", "INTFW", "IRC", "LZETP"]
×
879
    #    params['PWAT-STATE1'] = ['CEPS',   'SURS',   'UZS',    'IFWS',   'LZS',    'AGWS',   'GWVS']
880

881
    defaults = {}
×
882
    defaults["FOREST"] = 0.0
×
883
    defaults["KVARY"] = 0.0
×
884

885
    defaults["PETMAX"] = 40.0
×
886
    defaults["PETMIN"] = 35.0
×
887
    defaults["INFEXP"] = 2.0
×
888
    defaults["INFILD"] = 2.0
×
889
    defaults["DEEPFR"] = 0.0
×
890
    defaults["BASETP"] = 0.0
×
891
    defaults["AGWETP"] = 0.0
×
892

893
    defaults["CEPSC"] = 0.0
×
894
    defaults["NSUR"] = 0.1
×
895
    defaults["LZETP"] = 0.0
×
896

897
    # defaults['CEPS'] = 0.0
898
    # defaults['SURS'] = 0.0
899
    # defaults['UZS'] = 0.001
900
    # defaults['IFWS'] = 0.0
901
    # defaults['LZS'] = 0.001
902
    # defaults['AGWS'] = 0.0
903
    # defaults['GWVS'] = 0.0
904

905
    with open(uci, encoding="ascii") as fp:
×
906
        content = fp.readlines()
×
907

908
    content = [i[:81].rstrip() for i in content if "***" not in i]
×
909
    content = [i.rstrip() for i in content if i]
×
910

911
    files_start = content.index("FILES")
×
912
    files_end = content.index("END FILES")
×
913
    files = content[files_start + 1 : files_end]
×
914

915
    supfname = ""
×
916
    for line in files:
×
917
        words = line.split()
×
918
        if words[0] == "PESTSU":
×
919
            supfname = words[2]
×
920
    if supfname:
×
921
        ucipath = os.path.dirname(uci)
×
922
        with open(os.path.join(ucipath, supfname), encoding="ascii") as sfp:
×
923
            supfname = sfp.readlines()
×
924
            supfname = [i.strip() for i in supfname if "***" not in i]
×
925
            supfname = [i.strip() for i in supfname if i]
×
926

927
        supfname = {
×
928
            key.split()[0]: [float(i) for i in value.split()]
929
            for key, value in zip(supfname[:-1:2], supfname[1::2])
930
        }
931

932
    rngdata = []
×
933
    order = []
×
934
    for blk in blocklist:
×
935
        start = content.index(f"  {blk}")
×
936
        end = content.index(f"  END {blk}")
×
937
        block_lines = content[start + 1 : end]
×
938

939
        order.extend(params[blk])
×
940

941
        for line in block_lines:
×
942
            rngstrt = int(line[:5])
×
943
            try:
×
944
                rngend = int(line[5:10]) + 1
×
945
            except ValueError:
×
946
                rngend = rngstrt + 1
×
947
            tilde = re.match("~([0-9][0-9]*)~", line[10:])
×
948
            if tilde:
×
949
                tilde = tilde[0][1:-1]
×
950
            for rng in list(range(rngstrt, rngend)):
×
951
                for index, par in enumerate(params[blk]):
×
952
                    if tilde:
×
953
                        rngdata.append(
×
954
                            [
955
                                index_prefix + index_delimiter + str(rng),
956
                                par,
957
                                supfname[tilde][index],
958
                            ]
959
                        )
960
                    else:
961
                        start = (index + 1) * 10
×
962
                        rngdata.append(
×
963
                            [
964
                                index_prefix + index_delimiter + str(rng),
965
                                par,
966
                                float(line[start : start + 10]),
967
                            ]
968
                        )
969

970
    df = pd.DataFrame(rngdata)
×
971
    df.columns = ["lue", "term", "val"]
×
972
    df = df.pivot(index="lue", columns="term")
×
973
    df.columns = [i[1] for i in df.columns]
×
974
    df = df.loc[:, order]
×
975
    if index_prefix:
×
976
        spliton = index_prefix[-1]
×
977
    if index_delimiter:
×
978
        spliton = index_delimiter
×
979
    if index_prefix or index_delimiter:
×
980
        df = df.reindex(
×
981
            index=df.index.to_series()
982
            .str.rsplit(spliton)
983
            .str[-1]
984
            .astype(int)
985
            .sort_values()
986
            .index
987
        )
988
    else:
989
        df.index = df.index.astype(int)
×
990
        df = df.sort_index()
×
991

992
    return df
×
993

994

995
if __name__ == "__main__":
4✔
996
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc