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

timcera / hspf_utils / 10867244990

15 Sep 2024 01:59AM CUT coverage: 60.983% (+0.8%) from 60.18%
10867244990

push

github

timcera
bump: version 6.0.9 → 6.0.10

211 of 346 relevant lines covered (60.98%)

3.05 hits per line

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

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

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

8
import numpy as np
5✔
9
import pandas as pd
5✔
10
from hspfbintoolbox.hspfbintoolbox import extract
5✔
11
from toolbox_utils import tsutils
5✔
12

13
__all__ = ["detailed", "summary", "mapping", "parameters"]
5✔
14

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

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

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

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

36
        If None the water balance would cover the period of simulation.
37
        Otherwise the year for the water balance.""",
38
    "modulus": r"""modulus : int
39
        [optional, defaults to 20]
40

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

45
        The remainder of the PERLND label divided by the modulus is the land
46
        cover number.""",
47
    "tablefmt": r"""tablefmt : str
48
        [optional, default is 'cvs_nos']
49

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

58
        The format for floating point numbers in the output table.""",
59
    "index_prefix": r"""index_prefix
60
        [optional, defaults to '']
61

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

68
        Useful to separate the `index_prefix` from the PERLND/IMPLND number.
69
        """,
70
    "constituent": r"""constituent: str
71
        [optional, defaults to 'flow']
72

73
        The constituent to summarize in the table.
74

75
        Currently available constituents are: 'flow' for PWATER/IWATER and
76
        'qual' for PQUAL/IQUAL.
77

78
        if 'qual' is chosen, then the option 'qualnames' specifies the names to
79
        be found in the HBN file.
80
        """,
81
    "qualnames": r"""qualnames : str
82
        [optional, defaults to '']
83

84
        If 'constituent' is 'qual, then this is a comma-separated
85
        list of constituent names to be found in the HBN file.
86

87
        Example:
88
            --qualnames 'TOTAL N','TOTAL P'
89

90
        This will find PQUAL/IQUAL variables named 'SOQUAL-TOTAL N', etc, which
91
        occurs if the QUALID in QUAL-PROPS is 'TOTAL N'.
92
        """,
93
}
94

95
_mass_balance = {
5✔
96
    ("flow", "detailed", True): (
97
        ["SUPY", [("SUPY", "PERLND"), ("SUPY", "IMPLND"), ("IRRAPP6", "PERLND")]],
98
        ["SURLI", [("SURLI", "PERLND")]],
99
        ["UZLI", [("UZLI", "PERLND")]],
100
        ["LZLI", [("LZLI", "PERLND")]],
101
        ["", [("", "")]],
102
        ["SURO: PERVIOUS", [("SURO", "PERLND")]],
103
        ["SURO: IMPERVIOUS", [("SURO", "IMPLND")]],
104
        ["SURO: COMBINED", [("SURO", "PERLND"), ("SURO", "IMPLND")]],
105
        ["IFWO", [("IFWO", "PERLND")]],
106
        ["AGWO", [("AGWO", "PERLND")]],
107
        ["", [("", "")]],
108
        ["AGWI", [("AGWI", "PERLND")]],
109
        ["IGWI", [("IGWI", "PERLND")]],
110
        ["", [("", "")]],
111
        ["CEPE", [("CEPE", "PERLND")]],
112
        ["UZET", [("UZET", "PERLND")]],
113
        ["LZET", [("LZET", "PERLND")]],
114
        ["AGWET", [("AGWET", "PERLND")]],
115
        ["BASET", [("BASET", "PERLND")]],
116
        ["SURET", [("SURET", "PERLND")]],
117
        ["", [("", "")]],
118
        ["PERO", [("PERO", "PERLND")]],
119
        ["IGWI", [("IGWI", "PERLND")]],
120
        ["TAET: PERVIOUS", [("TAET", "PERLND")]],
121
        ["IMPEV: IMPERVIOUS", [("IMPEV", "IMPLND")]],
122
        ["ET: COMBINED", [("TAET", "PERLND"), ("IMPEV", "IMPLND")]],
123
        ["", [("", "")]],
124
        ["PET", [("PET", "PERLND"), ("PET", "IMPLND")]],
125
        ["", [("", "")]],
126
        ["PERS", [("PERS", "PERLND")]],
127
    ),
128
    ("flow", "detailed", False): (
129
        ["SUPY", [("SUPY", "PERLND")]],
130
        ["SURLI", [("SURLI", "PERLND")]],
131
        ["UZLI", [("UZLI", "PERLND")]],
132
        ["LZLI", [("LZLI", "PERLND")]],
133
        ["", [("", "")]],
134
        ["SURO: PERVIOUS", [("SURO", "PERLND")]],
135
        ["SURO: IMPERVIOUS", [("SURO", "IMPLND")]],
136
        ["IFWO", [("IFWO", "PERLND")]],
137
        ["AGWO", [("AGWO", "PERLND")]],
138
        ["", [("", "")]],
139
        ["AGWI", [("AGWI", "PERLND")]],
140
        ["IGWI", [("IGWI", "PERLND")]],
141
        ["", [("", "")]],
142
        ["CEPE", [("CEPE", "PERLND")]],
143
        ["UZET", [("UZET", "PERLND")]],
144
        ["LZET", [("LZET", "PERLND")]],
145
        ["AGWET", [("AGWET", "PERLND")]],
146
        ["BASET", [("BASET", "PERLND")]],
147
        ["SURET", [("SURET", "PERLND")]],
148
        ["", [("", "")]],
149
        ["PERO", [("PERO", "PERLND")]],
150
        ["IGWI", [("IGWI", "PERLND")]],
151
        ["TAET: PERVIOUS", [("TAET", "PERLND")]],
152
        ["IMPEV: IMPERVIOUS", [("IMPEV", "IMPLND")]],
153
        ["", [("", "")]],
154
        ["PET", [("PET", "PERLND")]],
155
        ["", [("", "")]],
156
        ["PERS", [("PERS", "PERLND")]],
157
    ),
158
    ("flow", "summary", True): (
159
        [
160
            "Rainfall and irrigation",
161
            [
162
                ("SUPY", "PERLND"),
163
                ("SUPY", "IMPLND"),
164
                ("SURLI", "PERLND"),
165
                ("UZLI", "PERLND"),
166
                ("LZLI", "PERLND"),
167
                ("IRRAPP6", "PERLND"),
168
            ],
169
        ],
170
        ["", [("", "")]],
171
        [
172
            "Runoff:Pervious",
173
            [("PERO", "PERLND")],
174
        ],
175
        ["Runoff:Impervious", [("SURO", "IMPLND")]],
176
        [
177
            "Runoff:Combined",
178
            [
179
                ("PERO", "PERLND"),
180
                ("SURO", "IMPLND"),
181
            ],
182
        ],
183
        ["", [("", "")]],
184
        ["Deep recharge", [("IGWI", "PERLND")]],
185
        ["", [("", "")]],
186
        ["Evaporation:Pervious", [("TAET", "PERLND")]],
187
        ["Evaporation:Impervious", [("IMPEV", "IMPLND")]],
188
        ["Evaporation:Combined", [("TAET", "PERLND"), ("IMPEV", "IMPLND")]],
189
    ),
190
    ("flow", "summary", False): (
191
        [
192
            "Rainfall and irrigation",
193
            [
194
                ("SUPY", "PERLND"),
195
                ("SUPY", "IMPLND"),
196
                ("SURLI", "PERLND"),
197
                ("UZLI", "PERLND"),
198
                ("LZLI", "PERLND"),
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
def _give_negative_warning(df):
5✔
264
    testpdf = pd.DataFrame(df) < 0
5✔
265
    if testpdf.any().any():
5✔
266
        warnings.warn(
×
267
            tsutils.error_wrapper(
268
                f"""
269
            This may be OK, but FYI there are negative values at:
270

271
            {df[testpdf].dropna(how='all').dropna(axis=1, how='all')}
272
            """
273
            )
274
        )
275

276

277
def process(uci, hbn, elements, year, modulus):
5✔
278
    with contextlib.suppress(TypeError):
5✔
279
        year = int(year)
5✔
280
    lcnames = dict(zip(range(modulus + 1, 1), zip(range(modulus + 1, 1))))
5✔
281
    inverse_lcnames = dict(zip(range(modulus + 1, 1), zip(range(modulus + 1, 1))))
5✔
282
    inverse_lc = {}
5✔
283

284
    lnds = {}
5✔
285

286
    if uci is not None:
5✔
287
        with open(uci, encoding="ascii") as fp:
5✔
288
            content = fp.readlines()
5✔
289

290
        if not os.path.exists(hbn):
5✔
291
            raise ValueError(
×
292
                f"""
293
*
294
*   File {hbn} does not exist.
295
*
296
"""
297
            )
298

299
        content = [i[:80] for i in content]
5✔
300
        content = [i.rstrip() for i in content]
5✔
301

302
        schematic_start = content.index("SCHEMATIC")
5✔
303
        schematic_end = content.index("END SCHEMATIC")
5✔
304
        schematic = content[schematic_start : schematic_end + 1]
5✔
305

306
        perlnd_start = content.index("PERLND")
5✔
307
        perlnd_end = content.index("END PERLND")
5✔
308
        perlnd = content[perlnd_start : perlnd_end + 1]
5✔
309

310
        pgeninfo_start = perlnd.index("  GEN-INFO")
5✔
311
        pgeninfo_end = perlnd.index("  END GEN-INFO")
5✔
312
        pgeninfo = perlnd[pgeninfo_start : pgeninfo_end + 1]
5✔
313

314
        masslink_start = content.index("MASS-LINK")
5✔
315
        masslink_end = content.index("END MASS-LINK")
5✔
316
        masslink = content[masslink_start : masslink_end + 1]
5✔
317

318
        lcnames = {}
5✔
319
        inverse_lcnames = {}
5✔
320
        inverse_lc = {}
5✔
321
        for line in pgeninfo[1:-1]:
5✔
322
            if "***" in line:
5✔
323
                continue
5✔
324
            if line.strip() == "":
5✔
325
                continue
×
326
            with contextlib.suppress(ValueError):
5✔
327
                _ = int(line[5:10])
5✔
328
                continue
5✔
329
            lcnames.setdefault(line[10:30].strip(), []).append(int(line[:5]))
×
330
            inverse_lcnames[int(line[:5])] = line[10:30].strip()
×
331
            inverse_lc[int(line[:5]) % modulus] = line[10:30].strip()
×
332

333
        masslink = [i for i in masslink if "***" not in i]
5✔
334
        masslink = [i for i in masslink if len(i.strip()) > 0]
5✔
335
        masslink = " ".join(masslink)
5✔
336
        mlgroups = re.findall(
5✔
337
            r"  MASS-LINK +?([0-9]+).*?LND     [PI]WATER [PS][EU]RO.*?  END MASS-LINK +?\1 ",
338
            masslink,
339
        )
340

341
        for line in schematic[3:-1]:
5✔
342
            if "***" in line:
5✔
343
                continue
5✔
344
            if line == "":
5✔
345
                continue
5✔
346
            words = line.split()
5✔
347
            if words[0] in ["PERLND", "IMPLND"] and words[5] in mlgroups:
5✔
348
                lnds[(words[0], int(words[1]))] = lnds.setdefault(
5✔
349
                    (words[0], int(words[1])), 0.0
350
                ) + float(words[2])
351

352
    try:
5✔
353
        pdf = extract(hbn, "yearly", ",,,")
5✔
354
    except ValueError as e:
×
355
        raise ValueError(
×
356
            tsutils.error_wrapper(
357
                f"""
358
                The binary file "{hbn}" does not have consistent ending months
359
                between PERLND and IMPLND.  This could be caused by the BYREND
360
                (Binary YeaR END) being set differently in the
361
                PERLND:BINARY-INFO and IMPLND:BINARY-INFO, or you could have
362
                the PRINT-INFO bug.  To work around the PRINT-INFO bug, add
363
                a PERLND PRINT-INFO block, setting the PYREND here will
364
                actually work in the BINARY-INFO block.
365
                """
366
            )
367
        ) from e
368

369
    if year is not None:
5✔
370
        pdf = pd.DataFrame(pdf.loc[f"{year}", :]).T
×
371
    pdf = pdf[[i for i in pdf.columns if "PERLND" in i or "IMPLND" in i]]
5✔
372

373
    mindex = [i.split("_") for i in pdf.columns]
5✔
374
    mindex = [(i[0], int(i[1]), i[2], int(i[1]) % modulus) for i in mindex]
5✔
375
    mindex = pd.MultiIndex.from_tuples(mindex, names=["op", "number", "balterm", "lc"])
5✔
376
    pdf.columns = mindex
5✔
377
    pdf = pdf.sort_index(axis="columns")
5✔
378
    mindex = pdf.columns
5✔
379
    aindex = [(i[0], i[1]) for i in pdf.columns]
5✔
380
    mindex = [
5✔
381
        (
382
            i[0],
383
            int(i[1]),
384
            i[2],
385
            int(i[1]) % modulus,
386
            float(lnds.setdefault(j, 0.0)),
387
            str(inverse_lcnames.setdefault(int(i[1]), "")),
388
        )
389
        for i, j in zip(mindex, aindex)
390
    ]
391
    mindex = pd.MultiIndex.from_tuples(
5✔
392
        mindex, names=["op", "number", "balterm", "lc", "area", "lcname"]
393
    )
394
    pdf.columns = mindex
5✔
395

396
    nsum = {}
5✔
397
    areas = {}
5✔
398
    namelist = {}
5✔
399
    setl = [i[1] for i in elements]
5✔
400
    setl = [item for sublist in setl for item in sublist]
5✔
401
    for lue in ["PERLND", "IMPLND"]:
5✔
402
        for bterm in [i[0] for i in setl if i[0]]:
5✔
403
            for lc in list(range(1, modulus + 1)):
5✔
404
                try:
5✔
405
                    subset = pdf.loc[
5✔
406
                        :, (lue, slice(None), bterm, lc, slice(None), slice(None))
407
                    ]
408
                except KeyError:
5✔
409
                    continue
5✔
410

411
                _give_negative_warning(subset)
5✔
412

413
                if uci is None:
5✔
414
                    if subset.empty is True:
×
415
                        nsum[(lue, lc, bterm)] = 0.0
×
416
                        if (lue, lc) not in namelist:
×
417
                            namelist[(lue, lc)] = ""
×
418
                    else:
419
                        nsum[(lue, lc, bterm)] = subset.mean(axis="columns").mean()
×
420
                        namelist[(lue, lc)] = inverse_lc.setdefault(lc, lc)
×
421
                else:
422
                    sareas = subset.columns.get_level_values("area")
5✔
423
                    ssareas = sum(sareas)
5✔
424
                    if (lue, lc) not in areas:
5✔
425
                        areas[(lue, lc)] = ssareas
5✔
426

427
                    if subset.empty is True or ssareas == 0:
5✔
428
                        nsum[(lue, lc, bterm)] = 0.0
×
429
                        if (lue, lc) not in namelist:
×
430
                            namelist[(lue, lc)] = ""
×
431
                    else:
432
                        fa = sareas / areas[(lue, lc)]
5✔
433
                        nsum[(lue, lc, bterm)] = (
5✔
434
                            (subset * fa).sum(axis="columns").mean()
435
                        )
436
                        namelist[(lue, lc)] = inverse_lc.setdefault(lc, lc)
5✔
437

438
    newnamelist = []
5✔
439
    for key, value in sorted(namelist.items()):
5✔
440
        if key[0] != "PERLND":
5✔
441
            continue
5✔
442
        if key[1] == value:
5✔
443
            newnamelist.append(f"{key[1]}")
5✔
444
        else:
445
            newnamelist.append(f"{key[1]}-{value}")
×
446

447
    printlist = [["BALANCE TERM"] + newnamelist + ["ALL"]]
5✔
448
    mapipratio = {"PERLND": 1.0, "IMPLND": 1.0}
5✔
449
    if uci is not None:
5✔
450
        pareas = []
5✔
451
        pnl = []
5✔
452
        iareas = []
5✔
453
        for nloper, nllc in namelist.items():
5✔
454
            if nloper[0] == "PERLND":
5✔
455
                pnl.append(("PERLND", nllc))
5✔
456
                pareas.append(areas[("PERLND", nllc)])
5✔
457
        # If there is a PERLND there must be a IMPLND.
458
        for _, pllc in pnl:
5✔
459
            try:
5✔
460
                iareas.append(areas[("IMPLND", pllc)])
5✔
461
            except KeyError:
5✔
462
                iareas.append(0.0)
5✔
463
        ipratio = np.array(iareas) / (np.array(pareas) + np.array(iareas))
5✔
464
        ipratio = np.nan_to_num(ipratio)
5✔
465
        ipratio = np.pad(ipratio, (0, len(pareas) - len(iareas)), "constant")
5✔
466
        sumareas = sum(pareas) + sum(iareas)
5✔
467

468
        percent_areas = {"PERLND": np.array(pareas) / sumareas * 100}
5✔
469
        percent_areas["IMPLND"] = np.array(iareas) / sumareas * 100
5✔
470
        percent_areas["COMBINED"] = percent_areas["PERLND"] + percent_areas["IMPLND"]
5✔
471

472
        printlist.extend(
5✔
473
            (
474
                ["PERVIOUS AREA(acres)"]
475
                + [i if i > 0 else None for i in pareas]
476
                + [sum(pareas)],
477
                ["PERVIOUS AREA(%)"]
478
                + [i if i > 0 else None for i in percent_areas["PERLND"]]
479
                + [sum(percent_areas["PERLND"])],
480
                [],
481
                ["IMPERVIOUS AREA(acres)"]
482
                + [i if i > 0 else None for i in iareas]
483
                + [sum(iareas)],
484
                ["IMPERVIOUS AREA(%)"]
485
                + [i if i > 0 else None for i in percent_areas["IMPLND"]]
486
                + [sum(percent_areas["IMPLND"])],
487
                [],
488
            )
489
        )
490
        mapipratio["PERLND"] = 1.0 - ipratio
5✔
491
        mapipratio["IMPLND"] = ipratio
5✔
492

493
    mapr = {"PERLND": 1.0, "IMPLND": 1.0}
5✔
494
    for term, op in elements:
5✔
495
        if not term:
5✔
496
            # term is None - insert a blank line
497
            printlist.append([])
5✔
498
            continue
5✔
499

500
        test = [i[1] for i in op]
5✔
501
        if "IMPLND" in test and "PERLND" in test:
5✔
502
            maprat = mapipratio
5✔
503
            sumop = "COMBINED"
5✔
504
        else:
505
            maprat = mapr
5✔
506
            sumop = test[0]
5✔
507

508
        te = []
5✔
509
        for sterm, operation in op:
5✔
510
            tmp = []
5✔
511
            for i in sorted(namelist):
5✔
512
                if i[0] == operation:
5✔
513
                    try:
5✔
514
                        tmp.append(nsum[(*i, sterm)])
5✔
515
                    except (IndexError, KeyError):
5✔
516
                        tmp.append(np.nan)
5✔
517
            tmp = np.array(tmp)
5✔
518
            if uci is not None:
5✔
519
                tmp = (
5✔
520
                    np.pad(
521
                        tmp,
522
                        (0, len(pareas) - len(tmp)),
523
                        "constant",
524
                        constant_values=np.nan,
525
                    )
526
                    * maprat[operation]
527
                )
528
            else:
529
                tmp = np.pad(
×
530
                    tmp,
531
                    (0, (len(printlist[0]) - 2) - len(tmp)),
532
                    "constant",
533
                    constant_values=np.nan,
534
                )
535

536
            if np.isfinite(tmp).any():  # need all for summary
5✔
537
                if len(te) == 0:
5✔
538
                    te = tmp
5✔
539
                else:
540
                    te = np.where(
5✔
541
                        np.isnan(te + tmp), np.where(np.isnan(te), tmp, te), te + tmp
542
                    )
543

544
        if uci is None:
5✔
545
            nte = np.pad(
×
546
                te,
547
                (0, (len(printlist[0]) - 2) - len(te)),
548
                "constant",
549
                constant_values=np.nan,
550
            )
551
            nte_mean = np.nanmean(nte)
×
552
            te = [term] + list(nte) + [nte_mean]
×
553
            # + [i if i > 0 else None for i in te]
554
        else:
555
            # this line assumes iareas are all at the beginning - fix?
556
            nte = np.pad(
5✔
557
                te, (0, len(iareas) - len(te)), "constant", constant_values=np.nan
558
            )
559
            te = [term] + list(nte) + [np.nansum(nte * percent_areas[sumop]) / 100]
5✔
560
            # + [i if i > 0 else None for i in nte]
561
        printlist.append(te)
5✔
562
    df = pd.DataFrame(printlist)
5✔
563
    df.columns = df.iloc[0, :]
5✔
564
    df = df[1:]
5✔
565
    return df.set_index("BALANCE TERM")
5✔
566

567

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

593

594
@tsutils.doc(docstrings)
5✔
595
def detailed(
5✔
596
    hbn,
597
    uci=None,
598
    year=None,
599
    modulus=20,
600
    constituent="flow",
601
    qualnames="",
602
):
603
    """Develops a detailed water or mass balance.
604

605
    Parameters
606
    ----------
607
    ${hbn}
608
    ${uci}
609
    ${year}
610
    ${modulus}
611
    ${constituent}
612
    ${qualnames}
613
    ${tablefmt}
614
    ${float_format}
615
    """
616
    elements = _mass_balance[(constituent, "detailed", bool(uci))]
×
617
    if constituent == "qual":
×
618
        elements = process_qual_names(qualnames, elements)
×
619
    return process(uci, hbn, elements, year, modulus)
×
620

621

622
@tsutils.doc(docstrings)
5✔
623
def summary(
5✔
624
    hbn,
625
    uci=None,
626
    year=None,
627
    modulus=20,
628
    constituent="flow",
629
    qualnames="",
630
):
631
    """Develops a summary mass balance.
632

633
    Parameters
634
    ----------
635
    ${hbn}
636
    ${uci}
637
    ${year}
638
    ${modulus}
639
    ${constituent}
640
    ${qualnames}
641
    ${tablefmt}
642
    ${float_format}
643
    """
644
    elements = _mass_balance[(constituent, "summary", bool(uci))]
5✔
645
    if constituent == "qual":
5✔
646
        elements = process_qual_names(qualnames, elements)
×
647
    return process(uci, hbn, elements, year, modulus)
5✔
648

649

650
@tsutils.doc(docstrings)
5✔
651
def mapping(
5✔
652
    hbn,
653
    year=None,
654
    index_prefix="",
655
):
656
    """Develops a csv file appropriate for joining to a GIS layer.
657

658
    Parameters
659
    ----------
660
    ${hbn}
661

662
    ${year}
663

664
    ${index_prefix}
665
        [optional, defaults to '']
666

667
        A string prepended to the PERLND code, which would allow being
668
        run on different models and collected into one dataset by
669
        creating a unique ID.
670

671
    ${tablefmt}
672

673
    ${float_format}
674
    """
675
    try:
×
676
        pdf = extract(hbn, "yearly", ",,,")
×
677
    except ValueError as exc:
×
678
        raise ValueError(
×
679
            tsutils.error_wrapper(
680
                """
681
                The binary file does not have consistent ending months between
682
                PERLND and IMPLND.  This could be caused by the BYREND (Binary
683
                YeaR END) being set differently in the PERLND:BINARY-INFO and
684
                IMPLND:BINARY-INFO, or you could have the PRINT-INFO bug.  To
685
                work around the PRINT-INFO bug, add a PERLND PRINT-INFO block,
686
                setting the PYREND there will actually work in the BINARY-INFO
687
                block.
688
                """
689
            )
690
        ) from exc
691

692
    if year is not None:
×
693
        pdf = pd.DataFrame(pdf.loc[f"{year}", :]).T
×
694
    pdf = pdf[[i for i in pdf.columns if "PERLND" in i or "IMPLND" in i]]
×
695

696
    mindex = [i.split("_") for i in pdf.columns]
×
697
    mindex = [(i[0][0], int(i[1]), i[2]) for i in mindex]
×
698
    mindex = pd.MultiIndex.from_tuples(mindex, names=["op", "number", "balterm"])
×
699
    pdf.columns = mindex
×
700

701
    _give_negative_warning(pdf)
×
702

703
    pdf = pdf.mean(axis="index").to_frame()
×
704

705
    mindex = [("_".join([i[0], i[2]]), i[1]) for i in pdf.index]
×
706
    mindex = pd.MultiIndex.from_tuples(mindex, names=["balterm", "number"])
×
707
    pdf.index = mindex
×
708
    pdf = pdf.unstack("balterm")
×
709

710
    mindex = [i[1] for i in pdf.columns]
×
711
    pdf.columns = mindex
×
712

713
    pdf.index.name = "lue"
×
714

715
    if index_prefix:
×
716
        pdf.index = [index_prefix + str(i) for i in pdf.index]
×
717

718
    return pdf
×
719

720

721
@tsutils.doc(docstrings)
5✔
722
def parameters(
5✔
723
    uci,
724
    index_prefix="",
725
    index_delimiter="",
726
):
727
    """Develops a table of parameter values.
728

729
    Parameters
730
    ----------
731
    ${uci}
732
    ${index_prefix}
733
    ${index_delimiter}
734
    ${tablefmt}
735
    ${float_format}
736
    """
737
    blocklist = ["PWAT-PARM2", "PWAT-PARM3", "PWAT-PARM4"]  # , 'PWAT-STATE1']
×
738

739
    params = {
×
740
        "PWAT-PARM2": [
741
            "FOREST",
742
            "LZSN",
743
            "INFILT",
744
            "LSUR",
745
            "SLSUR",
746
            "KVARY",
747
            "AGWRC",
748
        ],
749
        "PWAT-PARM3": [
750
            "PETMAX",
751
            "PETMIN",
752
            "INFEXP",
753
            "INFILD",
754
            "DEEPFR",
755
            "BASETP",
756
            "AGWETP",
757
        ],
758
        "PWAT-PARM4": ["CEPSC", "UZSN", "NSUR", "INTFW", "IRC", "LZETP"],
759
    }
760
    #    params['PWAT-STATE1'] = ['CEPS',   'SURS',   'UZS',    'IFWS',   'LZS',    'AGWS',   'GWVS']
761

762
    # defaults = {
763
    #     "FOREST": 0.0,
764
    #     "KVARY": 0.0,
765
    #     "PETMAX": 40.0,
766
    #     "PETMIN": 35.0,
767
    #     "INFEXP": 2.0,
768
    #     "INFILD": 2.0,
769
    #     "DEEPFR": 0.0,
770
    #     "BASETP": 0.0,
771
    #     "AGWETP": 0.0,
772
    #     "CEPSC": 0.0,
773
    #     "NSUR": 0.1,
774
    #     "LZETP": 0.0,
775
    # }
776
    # defaults['CEPS'] = 0.0
777
    # defaults['SURS'] = 0.0
778
    # defaults['UZS'] = 0.001
779
    # defaults['IFWS'] = 0.0
780
    # defaults['LZS'] = 0.001
781
    # defaults['AGWS'] = 0.0
782
    # defaults['GWVS'] = 0.0
783

784
    with open(uci, encoding="ascii") as fp:
×
785
        content = fp.readlines()
×
786

787
    content = [i[:81].rstrip() for i in content if "***" not in i]
×
788
    content = [i.rstrip() for i in content if i]
×
789

790
    files_start = content.index("FILES")
×
791
    files_end = content.index("END FILES")
×
792
    files = content[files_start + 1 : files_end]
×
793

794
    supfname = ""
×
795
    for line in files:
×
796
        words = line.split()
×
797
        if words[0] == "PESTSU":
×
798
            supfname = words[2]
×
799
    if supfname:
×
800
        ucipath = os.path.dirname(uci)
×
801
        with open(os.path.join(ucipath, supfname), encoding="ascii") as sfp:
×
802
            supfname = sfp.readlines()
×
803
            supfname = [i.strip() for i in supfname if "***" not in i]
×
804
            supfname = [i.strip() for i in supfname if i]
×
805

806
        supfname = {
×
807
            key.split()[0]: [float(i) for i in value.split()]
808
            for key, value in zip(supfname[:-1:2], supfname[1::2])
809
        }
810

811
    rngdata = []
×
812
    order = []
×
813
    for blk in blocklist:
×
814
        start = content.index(f"  {blk}")
×
815
        end = content.index(f"  END {blk}")
×
816
        block_lines = content[start + 1 : end]
×
817

818
        order.extend(params[blk])
×
819

820
        for line in block_lines:
×
821
            rngstrt = int(line[:5])
×
822
            try:
×
823
                rngend = int(line[5:10]) + 1
×
824
            except ValueError:
×
825
                rngend = rngstrt + 1
×
826
            tilde = re.match("~([0-9][0-9]*)~", line[10:])
×
827
            if tilde:
×
828
                tilde = tilde[0][1:-1]
×
829
            for rng in list(range(rngstrt, rngend)):
×
830
                for index, par in enumerate(params[blk]):
×
831
                    if tilde:
×
832
                        rngdata.append(
×
833
                            [
834
                                index_prefix + index_delimiter + str(rng),
835
                                par,
836
                                supfname[tilde][index],
837
                            ]
838
                        )
839
                    else:
840
                        start = (index + 1) * 10
×
841
                        rngdata.append(
×
842
                            [
843
                                index_prefix + index_delimiter + str(rng),
844
                                par,
845
                                float(line[start : start + 10]),
846
                            ]
847
                        )
848

849
    df = pd.DataFrame(rngdata)
×
850
    df.columns = ["lue", "term", "val"]
×
851
    df = df.pivot(index="lue", columns="term")
×
852
    df.columns = [i[1] for i in df.columns]
×
853
    df = df.loc[:, order]
×
854
    if index_prefix:
×
855
        spliton = index_prefix[-1]
×
856
    if index_delimiter:
×
857
        spliton = index_delimiter
×
858
    if index_prefix or index_delimiter:
×
859
        return df.reindex(
×
860
            index=df.index.to_series()
861
            .str.rsplit(spliton)
862
            .str[-1]
863
            .astype(int)
864
            .sort_values()
865
            .index
866
        )
867
    df.index = df.index.astype(int)
×
868
    return df.sort_index()
×
869

870

871
def main():
5✔
872
    import cltoolbox
5✔
873

874
    @cltoolbox.command()
5✔
875
    def about():
5✔
876
        """Display version number and system information."""
877
        tsutils.about("hspf_utils")
×
878

879
    @cltoolbox.command("detailed")
5✔
880
    @tsutils.copy_doc(detailed)
5✔
881
    def _detailed_cli(
5✔
882
        hbn,
883
        uci=None,
884
        year=None,
885
        modulus=20,
886
        constituent="flow",
887
        qualnames="",
888
        tablefmt="csv_nos",
889
        float_format=".2f",
890
    ):
891
        tsutils.printiso(
×
892
            detailed(
893
                hbn,
894
                uci=uci,
895
                year=year,
896
                modulus=modulus,
897
                constituent=constituent,
898
                qualnames=qualnames,
899
            ),
900
            float_format=float_format,
901
            headers="keys",
902
            tablefmt=tablefmt,
903
        )
904

905
    @cltoolbox.command("summary")
5✔
906
    @tsutils.copy_doc(summary)
5✔
907
    def _summary_cli(
5✔
908
        hbn,
909
        uci=None,
910
        year=None,
911
        modulus=20,
912
        constituent="flow",
913
        qualnames="",
914
        tablefmt="csv_nos",
915
        float_format=".2f",
916
    ):
917
        tsutils.printiso(
5✔
918
            summary(
919
                hbn,
920
                uci=uci,
921
                year=year,
922
                modulus=modulus,
923
                constituent=constituent,
924
                qualnames=qualnames,
925
            ),
926
            float_format=float_format,
927
            headers="keys",
928
            tablefmt=tablefmt,
929
        )
930

931
    @cltoolbox.command("mapping")
5✔
932
    @tsutils.copy_doc(mapping)
5✔
933
    def _mapping_cli(
5✔
934
        hbn, year=None, index_prefix="", tablefmt="csv_nos", float_format="g"
935
    ):
936
        tsutils.printiso(
×
937
            mapping(
938
                hbn,
939
                year=year,
940
                index_prefix=index_prefix,
941
            ),
942
            float_format=float_format,
943
            headers="keys",
944
            tablefmt=tablefmt,
945
        )
946

947
    @cltoolbox.command("parameters")
5✔
948
    @tsutils.copy_doc(parameters)
5✔
949
    def _parameters_cli(
5✔
950
        uci,
951
        index_prefix="",
952
        index_delimiter="",
953
        tablefmt="csv_nos",
954
        float_format="g",
955
    ):
956
        tsutils.printiso(
×
957
            parameters(
958
                uci,
959
                index_prefix=index_prefix,
960
                index_delimiter=index_delimiter,
961
            ),
962
            float_format=float_format,
963
            headers="keys",
964
            tablefmt=tablefmt,
965
        )
966

967
    cltoolbox.main()
5✔
968

969

970
if __name__ == "__main__":
5✔
971
    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