Skip to content

Commit ac7c6ff

Browse files
committed
more cleanup
1 parent 0f318f5 commit ac7c6ff

1 file changed

Lines changed: 69 additions & 117 deletions

File tree

flopy/utils/classic_to_mf6.py

Lines changed: 69 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -166,21 +166,20 @@ def get_icelltype_from_laycon(laycon):
166166

167167
class ClassicMfToMf6Converter:
168168
"""
169-
Convert classic MODFLOW binary outputs to MODFLOW 6 binary format.
169+
Convert classic MODFLOW compact binary files to MODFLOW 6 binary format.
170170
171171
Reads head and cell-budget files produced by any structured-grid MODFLOW
172172
variant with compact budget output (FLOW RIGHT FACE / FLOW FRONT FACE /
173173
FLOW LOWER FACE terms), and writes the MF6-compatible head file, budget
174174
file (``FLOW-JA-FACE``, ``DATA-SAT``), and binary grid record (GRB)
175175
required by PRT and other MF6 post-processors.
176176
177-
Confirmed compatible variants: MODFLOW-NWT (UPW), MODFLOW-2005 (LPF),
178-
MODFLOW-2000 (LPF or BCF).
177+
Compatible with: MODFLOW-NWT (UPW), MODFLOW-2005 (LPF), MODFLOW-2000 (LPF or BCF).
179178
180179
The easiest way to construct a converter from a loaded model is
181180
:meth:`from_model`, which autodetects the flow package and extracts all
182-
required parameters automatically. Construct directly only when a model
183-
object is not available (e.g. you only have the binary files on disk).
181+
required parameters automatically. A converter can also be constructed
182+
directly if a model is not available (e.g. you have binary output files).
184183
185184
Parameters
186185
----------
@@ -469,7 +468,6 @@ def convert(
469468
- 'bud': Path to budget file
470469
"""
471470
from ..mf6.utils import MfGrdFile
472-
from .binaryfile import CellBudgetFile, HeadFile
473471

474472
# Create output directory
475473
output_dir = Path(output_dir)
@@ -523,22 +521,20 @@ def convert(
523521

524522
return {"grb": grb_path, "hds": hds_path, "bud": bud_path}
525523

526-
def _build_header_lookup(self):
524+
def _build_budget_header_lookup(self):
527525
"""
528-
Build a dict mapping (kstp, kper) -> header record from the head file.
529-
530-
Returns
531-
-------
532-
dict
533-
Keys are (kstp, kper) tuples, values are header records with
534-
fields including 'pertim' and 'totim'.
526+
Build a dict mapping 0-based (kstp, kper) -> first CBC header record
527+
for that time step. The compact budget header stores delt, pertim,
528+
and totim directly, so no arithmetic is needed to recover them.
535529
"""
536-
# get_kstpkper() returns 0-based indices; recordarray stores 1-based
537-
# file values. Subtract 1 so the lookup keys match self.kstpkper.
538-
return {
539-
(int(rec["kstp"]) - 1, int(rec["kper"]) - 1): rec
540-
for rec in self.hds_obj.recordarray
541-
}
530+
seen = set()
531+
result = {}
532+
for rec in self.cbc_obj.recordarray:
533+
key = (int(rec["kstp"]) - 1, int(rec["kper"]) - 1)
534+
if key not in seen:
535+
result[key] = rec
536+
seen.add(key)
537+
return result
542538

543539
def _write_budget(self, filename, precision, verbose):
544540
"""Write budget file with FLOW-JA-FACE, DATA-SPDIS, and DATA-SAT."""
@@ -549,114 +545,83 @@ def _write_budget(self, filename, precision, verbose):
549545
if verbose:
550546
print(f" Processing {len(self.kstpkper)} time steps...")
551547

552-
# We'll write three terms per time step:
553-
# 1. FLOW-JA-FACE (imeth=1, array)
554-
# 2. DATA-SPDIS (imeth=6, list with qx, qy, qz)
555-
# 3. DATA-SAT (imeth=6, list)
556-
557-
# Build list of records
558548
records = []
549+
header_lookup = self._build_budget_header_lookup()
559550

560-
header_lookup = self._build_header_lookup()
561-
562-
for idx, kstpkper in enumerate(self.kstpkper):
551+
for kstpkper in self.kstpkper:
563552
kstp, kper = kstpkper
564553
rec = header_lookup[kstpkper]
565554
totim = float(rec["totim"])
566555
pertim = float(rec["pertim"])
567-
568-
# delt: for the first time step within a period pertim equals delt;
569-
# for subsequent steps subtract the previous step's pertim.
570-
# kstp is 0-based here (from get_kstpkper()).
571-
if kstp == 0:
572-
delt = pertim
573-
else:
574-
prev_rec = header_lookup.get((kstp - 1, kper))
575-
delt = pertim - float(prev_rec["pertim"]) if prev_rec else pertim
556+
delt = float(rec["delt"])
576557

577558
if verbose:
578559
print(f" Processing time step {kstpkper}...")
579560

580-
# Get head
581561
head = self.hds_obj.get_data(kstpkper=kstpkper)
582562

583-
# Get face flows
584563
if verbose:
585564
print(
586565
f" Available budget terms: "
587566
f"{self.cbc_obj.get_unique_record_names()}"
588567
)
589568

590-
# Check which face flows are available
591-
# For 1D/2D models, not all face flows may exist
569+
# for 1D/2D models, some directions may be missing
592570
available_terms = [
593571
t.decode().strip() for t in self.cbc_obj.get_unique_record_names()
594572
]
595573

596-
try:
597-
# FLOW RIGHT FACE (required for X-direction flow)
598-
if "FLOW RIGHT FACE" in available_terms:
599-
frf_data = self.cbc_obj.get_data(
600-
text="FLOW RIGHT FACE", kstpkper=kstpkper
601-
)
602-
if verbose:
603-
print(
604-
f" FLOW RIGHT FACE: {type(frf_data)}, "
605-
f"len={len(frf_data) if frf_data else 0}"
606-
)
607-
frf = frf_data[0] if frf_data and len(frf_data) > 0 else None
608-
else:
609-
frf = None
610-
611-
# FLOW FRONT FACE (required for Y-direction flow)
612-
if "FLOW FRONT FACE" in available_terms:
613-
fff_data = self.cbc_obj.get_data(
614-
text="FLOW FRONT FACE", kstpkper=kstpkper
574+
if "FLOW RIGHT FACE" in available_terms:
575+
frf_data = self.cbc_obj.get_data(
576+
text="FLOW RIGHT FACE", kstpkper=kstpkper
577+
)
578+
if verbose:
579+
print(
580+
f" FLOW RIGHT FACE: {type(frf_data)}, "
581+
f"len={len(frf_data) if frf_data else 0}"
615582
)
616-
if verbose:
617-
print(
618-
f" FLOW FRONT FACE: {type(fff_data)}, "
619-
f"len={len(fff_data) if fff_data else 0}"
620-
)
621-
fff = fff_data[0] if fff_data and len(fff_data) > 0 else None
622-
else:
623-
fff = None
624-
625-
# FLOW LOWER FACE (required for Z-direction flow)
626-
if "FLOW LOWER FACE" in available_terms:
627-
flf_data = self.cbc_obj.get_data(
628-
text="FLOW LOWER FACE", kstpkper=kstpkper
583+
frf = frf_data[0] if frf_data and len(frf_data) > 0 else None
584+
else:
585+
frf = None
586+
587+
if "FLOW FRONT FACE" in available_terms:
588+
fff_data = self.cbc_obj.get_data(
589+
text="FLOW FRONT FACE", kstpkper=kstpkper
590+
)
591+
if verbose:
592+
print(
593+
f" FLOW FRONT FACE: {type(fff_data)}, "
594+
f"len={len(fff_data) if fff_data else 0}"
629595
)
630-
if verbose:
631-
print(
632-
f" FLOW LOWER FACE: {type(flf_data)}, "
633-
f"len={len(flf_data) if flf_data else 0}"
634-
)
635-
flf = flf_data[0] if flf_data and len(flf_data) > 0 else None
636-
else:
637-
flf = None
638-
639-
# Validate at least one face flow exists
640-
if frf is None and fff is None and flf is None:
641-
raise ValueError("No face flows found in budget file")
642-
643-
# Create zero arrays for missing face flows
644-
# For 1D/2D models, not all directions have flow
645-
shape_3d = (self.nlay, self.nrow, self.ncol)
646-
if frf is None:
647-
frf = np.zeros(shape_3d, dtype=np.float64)
648-
if fff is None:
649-
fff = np.zeros(shape_3d, dtype=np.float64)
650-
if flf is None:
651-
flf = np.zeros(shape_3d, dtype=np.float64)
652-
653-
except Exception as e:
596+
fff = fff_data[0] if fff_data and len(fff_data) > 0 else None
597+
else:
598+
fff = None
599+
600+
if "FLOW LOWER FACE" in available_terms:
601+
flf_data = self.cbc_obj.get_data(
602+
text="FLOW LOWER FACE", kstpkper=kstpkper
603+
)
654604
if verbose:
655-
print(f" Warning: Could not read face flows: {e}")
656-
print(f" Skipping time step {kstpkper}")
657-
continue
605+
print(
606+
f" FLOW LOWER FACE: {type(flf_data)}, "
607+
f"len={len(flf_data) if flf_data else 0}"
608+
)
609+
flf = flf_data[0] if flf_data and len(flf_data) > 0 else None
610+
else:
611+
flf = None
612+
613+
if frf is None and fff is None and flf is None:
614+
raise ValueError("No face flows found in budget file")
615+
616+
# create zero arrays for missing face flows
617+
shape_3d = (self.nlay, self.nrow, self.ncol)
618+
if frf is None:
619+
frf = np.zeros(shape_3d, dtype=np.float64)
620+
if fff is None:
621+
fff = np.zeros(shape_3d, dtype=np.float64)
622+
if flf is None:
623+
flf = np.zeros(shape_3d, dtype=np.float64)
658624

659-
# 1. Convert to FLOW-JA-FACE
660625
flowja = get_structured_flowja(
661626
(frf, fff, flf),
662627
ia=self.ia,
@@ -665,7 +630,6 @@ def _write_budget(self, filename, precision, verbose):
665630
nrow=self.nrow,
666631
ncol=self.ncol,
667632
)
668-
669633
records.append(
670634
{
671635
"data": flowja,
@@ -679,25 +643,19 @@ def _write_budget(self, filename, precision, verbose):
679643
}
680644
)
681645

682-
# 2. DATA-SPDIS (specific discharge) - SKIPPED for now
683646
# TODO: get_specific_discharge() requires a model object and cannot
684-
# easily be driven from binary files alone. PRT can reconstruct
685-
# specific discharge from FLOW-JA-FACE if needed.
647+
# easily be driven from binary files alone. PRT doesn't need SPDIS,
648+
# but other models might, so skip it for now and come back to this.
686649

687-
# 3. Calculate saturation
688650
sat = get_saturation(
689651
head, self.top, self.botm, self.icelltype_3d, self.hdry, self.hnoflo
690652
)
691653

692-
# Build list data for DATA-SAT
693654
sat_flat = sat.flatten(order="F")
694655
active_sat = ~np.isnan(sat_flat)
695656
nlist_sat = np.sum(active_sat)
696-
697657
if nlist_sat > 0:
698658
nodes_sat = np.arange(self.ncells)[active_sat] + 1 # 1-based
699-
700-
# Create structured array for imeth=6
701659
dtype = np.dtype(
702660
[
703661
("node", np.int32),
@@ -709,7 +667,6 @@ def _write_budget(self, filename, precision, verbose):
709667
sat_data["node"] = nodes_sat
710668
sat_data["node2"] = nodes_sat
711669
sat_data["sat"] = sat_flat[active_sat]
712-
713670
records.append(
714671
{
715672
"data": sat_data,
@@ -724,7 +681,6 @@ def _write_budget(self, filename, precision, verbose):
724681
}
725682
)
726683

727-
# Write all records
728684
if verbose:
729685
print(f" Writing {len(records)} budget records...")
730686

@@ -747,7 +703,3 @@ def __repr__(self):
747703
f" time_steps={len(self.times)}\n"
748704
f")"
749705
)
750-
751-
752-
#: Backward-compatible alias. New code should use ClassicMfToMf6Converter.
753-
NwtToMf6Converter = ClassicMfToMf6Converter

0 commit comments

Comments
 (0)