@@ -166,21 +166,20 @@ def get_icelltype_from_laycon(laycon):
166166
167167class 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