Skip to content

Commit 8a74535

Browse files
committed
DSM2-1228 feat: add GTM restart file creation functionality and enhance related modules
1 parent 6e7c6c0 commit 8a74535

8 files changed

Lines changed: 551 additions & 88 deletions

File tree

pydsm/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44

55
__author__ = """Nicky Sandhu"""
66
__email__ = "psandhu@water.ca.gov"
7-
__all__ = []
7+
__all__ = [
8+
"__version__",
9+
# GTM helper exports
10+
"build_timewindow_for_time",
11+
"get_interpolated_cell_concentrations",
12+
"write_gtm_restart",
13+
]
814

915
from ._version import __version__
16+
from .gtmh5 import build_timewindow_for_time, get_interpolated_cell_concentrations
17+
from .create_gtm_restart import write_gtm_restart

pydsm/cli.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pydsm import repeating_timeseries
99
from pydsm import create_cd_inp
1010
from pydsm import extend_dss_ts
11+
from pydsm import write_gtm_restart
1112

1213

1314
import pandas as pd
@@ -317,6 +318,28 @@ def csv_to_dss(
317318
)
318319

319320

321+
@click.command(name="create-gtm-restart")
322+
@click.argument("tidefile", type=click.Path(exists=True))
323+
@click.argument("target_time", type=str)
324+
@click.argument("outfile", type=click.Path())
325+
@click.option(
326+
"-c",
327+
"--constituent",
328+
default="ec",
329+
show_default=True,
330+
help="Constituent to export (e.g. ec)",
331+
)
332+
def create_gtm_restart_cmd(tidefile, target_time, outfile, constituent):
333+
"""Create a GTM restart file from a GTM/Qual tide HDF5 file.
334+
335+
TIDEFILE: Path to HDF5 tide file.
336+
TARGET_TIME: Desired time (e.g. '05FEB2020 0300'). Nearest model output time will be used.
337+
OUTFILE: Destination restart file name.
338+
"""
339+
path = write_gtm_restart(tidefile, target_time, outfile, constituent=constituent)
340+
click.echo(f"Wrote restart file: {path}")
341+
342+
320343
# Add the commands to the group repeating
321344
repeating.add_command(create_repeating)
322345
repeating.add_command(extend_repeating)
@@ -330,6 +353,8 @@ def csv_to_dss(
330353
main.add_command(update_hydro_tidefile_with_inp)
331354
main.add_command(create_dsm2_input_for_cd)
332355
main.add_command(pretty_print_input)
356+
# GTM restart creation
357+
main.add_command(create_gtm_restart_cmd)
333358
#
334359
main.add_command(extend_dss_ts.extend_dss_ts)
335360
if __name__ == "__main__":

pydsm/create_gtm_restart.py

Lines changed: 169 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,173 @@
1-
#modify an existing gtm restart file to create a new restart file.
1+
"""Create a GTM restart file from a tide (Qual/GTM HDF5) file.
2+
3+
Generates a restart file containing:
4+
- Cell concentrations (interpolated per cell from channel end concentrations)
5+
- Reservoir concentrations
6+
for a requested model time.
7+
8+
Example
9+
-------
10+
>>> write_gtm_restart(
11+
... tidefile="tests/data/hist_v82_mss2_extran_gtm.h5",
12+
... target_time="05FEB2020 0300",
13+
... outfile="restart_created.qrf",
14+
... constituent="ec",
15+
... )
16+
17+
Output file format (illustrative):
18+
30DEC2024 2400/time
19+
65744640 /julmin
20+
1 /n_column
21+
4279 /n_cell
22+
cell_no ec
23+
1 685.4853218050435544
24+
... (cells) ...
25+
9 /n_resv
26+
reservoir_name ec
27+
clifton_court 416.6467054602688336
28+
... (reservoirs) ...
29+
"""
30+
31+
from __future__ import annotations
32+
33+
from datetime import datetime, timedelta
34+
from pathlib import Path
35+
from typing import Iterable
236

337
import numpy as np
438

5-
base_restart = "../restart_gtm1000_nx2.qrf"
6-
out_restart = "../restart_gtm1000_ones_nx2.qrt"
7-
8-
def read_restart(restart_fn,extract_details=False):
9-
with open(restart_fn,'r') as file:
10-
filedata = file.read()
11-
filetxt = filedata.split('\n')
12-
ncells = int(filetxt[3].split('/')[0])
13-
cell_header_str = '\n'.join(filetxt[0:5])
14-
cell_str = filetxt[5:5+ncells]
15-
cell_array = np.array([l.split() for l in cell_str])
16-
cell_name = cell_array[:,0]
17-
cell_value = cell_array[:,1]
18-
nreser = int(filetxt[5+ncells].split('/')[0])
19-
res_header_str = '\n'.join(filetxt[5+ncells:5+ncells+2])
20-
res_str = filetxt[5+ncells+2:5+ncells+2+nreser]
21-
res_array = np.array([l.split() for l in res_str])
22-
res_name = res_array[:,0]
23-
res_value = res_array[:,1]
24-
if extract_details:
25-
return cell_name, res_name, ncells, nreser,cell_header_str,res_header_str
26-
else:
27-
field = np.append(cell_value,res_value)
28-
return field
29-
30-
31-
def write_restart(field,restart_fn,cell_name, res_name,
32-
cell_header_str, res_header_str):
33-
"""
34-
read from a pdaf field_fn and write into a gtm restart_fn
35-
"""
36-
#field = read_pdaf_field(field_fn)
37-
ncells = len(cell_name)
38-
nres = len(res_name)
39-
cell_list = ['%32s%32s'%(cid,cv) for cid, cv in zip(cell_name, field[:ncells])]
40-
cell_str = '\n'.join(cell_list)
41-
res_list = [cid.ljust(32) + '%32s'%cv for cid, cv in zip(res_name,field[ncells:ncells+nres])]
42-
res_list = '\n'.join(res_list)
43-
field_str = '\n'.join([cell_header_str,cell_str,res_header_str,res_list])
44-
with open(restart_fn,'w') as file:
45-
file.write(field_str)
46-
47-
# Get basic structure from an example restart file.
48-
cell_name, res_name, ncells, nreser,cell_header_str,res_header_str = read_restart(base_restart,extract_details=True)
49-
field = read_restart(base_restart)
50-
field_new = np.ones_like(field) # all one initial condition
51-
#field_zero = (field.astype(float)*0.0).astype(str)
52-
write_restart(field_new,out_restart,cell_name,res_name,
53-
cell_header_str, res_header_str)
39+
from .gtmh5 import (
40+
build_timewindow_for_time,
41+
get_interpolated_cell_concentrations,
42+
_format_time,
43+
)
44+
from .qualh5 import QualH5
45+
46+
_EPOCH_JULMIN = datetime(1899, 12, 31, 0, 0) # Excel-like epoch (matches sample)
47+
48+
49+
def _time_label_with_2400(dt: datetime) -> str:
50+
"""Format time similar to DSM2/expected restart label.
51+
52+
If the time is exactly on midnight (00:00) we represent it as previous day 2400.
53+
"""
54+
if dt.hour == 0 and dt.minute == 0:
55+
prev = dt - timedelta(days=1)
56+
return prev.strftime("%d%b%Y").upper() + " 2400"
57+
return dt.strftime("%d%b%Y %H%M").upper()
58+
59+
60+
def _compute_julmin(dt: datetime) -> int:
61+
"""Compute julian minutes relative to the _EPOCH_JULMIN baseline.
62+
63+
This matches the magnitude of values seen in existing restart samples.
64+
"""
65+
return int((dt - _EPOCH_JULMIN).total_seconds() // 60)
66+
67+
68+
def _format_cell_header(constituent: str) -> str:
69+
return f"{'cell_no':>29}{constituent:>20} "
70+
71+
72+
def _format_res_header(constituent: str) -> str:
73+
return f"{'reservoir_name':>29}{constituent:>20} "
74+
75+
76+
def write_gtm_restart(
77+
tidefile: str | Path,
78+
target_time: str | datetime,
79+
outfile: str | Path,
80+
constituent: str = "ec",
81+
) -> Path:
82+
"""Write a GTM restart file at the requested time.
83+
84+
Parameters
85+
----------
86+
tidefile : str | Path
87+
Path to GTM/Qual HDF5 file (tide file).
88+
target_time : str | datetime
89+
Desired time (will snap to nearest output time).
90+
outfile : str | Path
91+
Destination restart file path.
92+
constituent : str, default 'ec'
93+
Constituent to export.
94+
95+
Returns
96+
-------
97+
Path to the written restart file.
98+
"""
99+
tidefile = Path(tidefile)
100+
outfile = Path(outfile)
101+
qualt = QualH5(str(tidefile))
102+
103+
# Determine timewindow and chosen model time
104+
timewindow, model_time = build_timewindow_for_time(str(tidefile), target_time)
105+
106+
# End of interval (label uses interval end so that 2400 formatting matches sample)
107+
output_freq = qualt.get_output_freq()
108+
interval_end = model_time + output_freq
109+
110+
# Gather cell concentrations
111+
cell_conc = get_interpolated_cell_concentrations(
112+
str(tidefile), timewindow, constituent=constituent
113+
).ravel()
114+
n_cells = cell_conc.size
115+
116+
# Gather reservoir concentrations
117+
res_df = qualt.get_reservoirs()
118+
res_names = list(res_df["name"].values)
119+
if res_names:
120+
res_conc_df = qualt.get_reservoir_concentration(
121+
constituent, res_names, timewindow=timewindow
122+
)
123+
# Single row expected
124+
res_conc = res_conc_df.iloc[0].values.astype(float)
125+
else: # pragma: no cover - unlikely empty dataset
126+
res_conc = np.array([])
127+
n_res = len(res_conc)
128+
129+
# Compose header
130+
time_label = _time_label_with_2400(interval_end)
131+
julmin = _compute_julmin(interval_end)
132+
133+
lines: list[str] = []
134+
lines.append(f"{time_label}/time")
135+
lines.append(f"{julmin:12d} /julmin")
136+
lines.append(f"{1:12d} /n_column")
137+
lines.append(f"{n_cells:12d} /n_cell")
138+
lines.append(_format_cell_header(constituent))
139+
140+
# Cell data lines: id (1-based) and value
141+
for i, val in enumerate(cell_conc, start=1):
142+
# Align cell number under 'cell_no' and value under constituent header
143+
lines.append(f"{i:32d}{val:32.16f}")
144+
145+
lines.append(f"{n_res:12d} /n_resv")
146+
lines.append(_format_res_header(constituent))
147+
148+
for name, val in zip(res_names, res_conc):
149+
lines.append(f"{name:<32}{val:>32.16f}")
150+
151+
outfile.write_text("\n".join(lines) + "\n")
152+
return outfile
153+
154+
155+
__all__ = ["write_gtm_restart"]
156+
157+
158+
if __name__ == "__main__": # rudimentary CLI usage
159+
import argparse
160+
161+
parser = argparse.ArgumentParser(description="Create GTM restart file")
162+
parser.add_argument("tidefile", help="Path to GTM/Qual tide HDF5 file")
163+
parser.add_argument("target_time", help="Target time (e.g. '05FEB2020 0300')")
164+
parser.add_argument("outfile", help="Output restart file path")
165+
parser.add_argument(
166+
"-c", "--constituent", default="ec", help="Constituent name (default ec)"
167+
)
168+
args = parser.parse_args()
54169

170+
path = write_gtm_restart(
171+
args.tidefile, args.target_time, args.outfile, constituent=args.constituent
172+
)
173+
print(f"Wrote restart file: {path}")

pydsm/dsm2h5.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def read_table_attrs_as_df(filename, table_path):
153153
"""
154154
with h5py.File(filename, "r") as f:
155155
bf = f[table_path]
156-
a = pd.DataFrame(bf.attrs.items(), columns=["Name", "Value"], dtype=np.str)
156+
a = pd.DataFrame(bf.attrs.items(), columns=["Name", "Value"], dtype=np.str_)
157157
a = a.append(
158158
pd.DataFrame([("shape", str(bf.shape))], columns=["Name", "Value"]),
159159
ignore_index=True,

0 commit comments

Comments
 (0)