11import numpy as np
22
33
4+ class _GridAdapter :
5+ """
6+ Temporary workaround to normalize grid-specific array access patterns.
7+
8+ Different grid types (DIS, DISV, DISU) store arrays in different shapes.
9+ Adapters provide a uniformly shaped interface, (nlay, ncells_per_layer).
10+
11+ Currently this is used only in get_transmissivities(). It should not be
12+ necessary anymore in 4.x.
13+ """
14+
15+ def __init__ (self , model , dis_package , nlay ):
16+ self .model = model
17+ self .dis = dis_package
18+ self .nlay = nlay
19+
20+ def reshape_if_needed (self , array , target_shape ):
21+ """Reshape 1D array to 2D if needed."""
22+ if array .ndim == 1 and len (target_shape ) == 2 :
23+ return array .reshape (target_shape )
24+ return array
25+
26+ def get_k_array (self , paklist ):
27+ """
28+ Return hydraulic conductivity array in shape (nlay, ncells_per_layer).
29+
30+ Parameters
31+ ----------
32+ paklist : list
33+ List of package names in the model
34+ """
35+ # Check for flow packages in order of preference
36+ if "LPF" in paklist :
37+ return self .model .lpf .hk .array
38+ elif "UPW" in paklist :
39+ return self .model .upw .hk .array
40+ elif "NPF" in paklist :
41+ return self ._get_npf_k_array ()
42+ else :
43+ raise ValueError ("No LPF, UPW, or NPF package." )
44+
45+ def _get_npf_k_array (self ):
46+ """Get NPF k array. Subclasses may override for grid-specific handling."""
47+ return self .model .npf .k .array
48+
49+ def get_bottom_array (self ):
50+ """Return bottom elevation array in shape (nlay, ncells_per_layer)."""
51+ raise NotImplementedError
52+
53+ def get_top_for_slice (self , indices , grid_type ):
54+ """Return model top elevation for the given cell indices."""
55+ raise NotImplementedError
56+
57+ def get_layer_tops (self , botm_sliced , indices , grid_type ):
58+ """Return top elevation for each layer at the given indices."""
59+ raise NotImplementedError
60+
61+ def normalize_heads_array (self , heads ):
62+ """
63+ Normalize heads array to (nlay, ncells_per_layer) shape if needed.
64+
65+ Parameters
66+ ----------
67+ heads : ndarray
68+ Heads array in any valid format for this grid type
69+
70+ Returns
71+ -------
72+ ndarray
73+ Heads array in (nlay, ncells_per_layer) shape
74+ """
75+ # Default: no normalization needed
76+ return heads
77+
78+
79+ class _DisAdapter (_GridAdapter ):
80+ """Adapter for structured (DIS) grids."""
81+
82+ def get_bottom_array (self ):
83+ return self .dis .botm .array
84+
85+ def get_top_for_slice (self , indices , grid_type ):
86+ return self .dis .top .array [indices ]
87+
88+ def get_layer_tops (self , botm_sliced , indices , grid_type ):
89+ tops = np .empty_like (botm_sliced , dtype = float )
90+ tops [0 , :] = self .dis .top .array [indices ]
91+ tops [1 :, :] = botm_sliced [:- 1 ]
92+ return tops
93+
94+
95+ class _DisvAdapter (_GridAdapter ):
96+ """Adapter for vertex (DISV) grids."""
97+
98+ def get_bottom_array (self ):
99+ return self .dis .botm .array
100+
101+ def get_top_for_slice (self , indices , grid_type ):
102+ # DISV top is (ncpl,), indices is a tuple with one element
103+ return self .dis .top .array [indices [0 ]]
104+
105+ def get_layer_tops (self , botm_sliced , indices , grid_type ):
106+ tops = np .empty_like (botm_sliced , dtype = float )
107+ tops [0 , :] = self .dis .top .array [indices [0 ]]
108+ tops [1 :, :] = botm_sliced [:- 1 ]
109+ return tops
110+
111+
112+ class _DisuAdapter (_GridAdapter ):
113+ """Adapter for unstructured (DISU) grids."""
114+
115+ def _get_npf_k_array (self ):
116+ """Get NPF k array, reshaping from (nodes,) to (nlay, ncpl) if needed."""
117+ k_array = self .model .npf .k .array
118+ if k_array .ndim == 1 :
119+ return k_array .reshape ((self .nlay , - 1 ))
120+ return k_array
121+
122+ def get_bottom_array (self ):
123+ bot_array = self .dis .bot .array
124+ if bot_array .ndim == 1 :
125+ return bot_array .reshape ((self .nlay , - 1 ))
126+ return bot_array
127+
128+ def get_top_for_slice (self , indices , grid_type ):
129+ # DISU top is per-node (nodes,), reshape to (nlay, ncpl) and get layer 0
130+ top_array = self .dis .top .array
131+ if top_array .ndim == 1 :
132+ top_array = top_array .reshape ((self .nlay , - 1 ))
133+ return top_array [0 , indices [0 ]]
134+
135+ def get_layer_tops (self , botm_sliced , indices , grid_type ):
136+ # DISU has per-node tops, so each layer has different top values
137+ tops = np .empty_like (botm_sliced , dtype = float )
138+ top_array = self .dis .top .array
139+ if top_array .ndim == 1 :
140+ top_array = top_array .reshape ((self .nlay , - 1 ))
141+ tops [0 , :] = top_array [0 , indices [0 ]]
142+ tops [1 :, :] = botm_sliced [:- 1 ]
143+ return tops
144+
145+ def normalize_heads_array (self , heads ):
146+ """Normalize heads from flat (nnodes,) to (nlay, ncpl) if needed."""
147+ if heads .ndim == 1 :
148+ return heads .reshape ((self .nlay , - 1 ))
149+ return heads
150+
151+
152+ def _get_grid_adapter (model , nlay ):
153+ """
154+ Get the appropriate grid adapter for the model.
155+
156+ Parameters
157+ ----------
158+ model : flopy.modflow.Modflow or flopy.mf6.ModflowGwf object
159+ Model object
160+ nlay : int
161+ Number of layers
162+
163+ Returns
164+ -------
165+ adapter : _GridAdapter subclass instance
166+ Grid-specific adapter
167+ dis_package : discretization package
168+ The DIS, DISV, or DISU package
169+ """
170+ paklist = model .get_package_list ()
171+
172+ if "DISU" in paklist :
173+ return _DisuAdapter (model , model .disu , nlay )
174+ elif "DISV" in paklist :
175+ return _DisvAdapter (model , model .disv , nlay )
176+ elif "DIS" in paklist :
177+ return _DisAdapter (model , model .dis , nlay )
178+ else :
179+ # For older MODFLOW versions, default to DIS adapter
180+ return _DisAdapter (model , model .dis , nlay )
181+
182+
4183def get_transmissivities (
5184 heads ,
6185 m ,
@@ -24,9 +203,9 @@ def get_transmissivities(
24203 heads : 2D array OR 3D array
25204 numpy array of shape nlay by n locations (2D) OR complete heads array
26205 with the correct shape for structured grids (nlay, nrow, ncol) or for
27- vertex grids (nlay, ncpl).
206+ vertex grids (nlay, ncpl) or unstructured grids (nnodes) .
28207 m : flopy.modflow.Modflow or flopy.mf6.ModflowGwf object
29- Must have dis and lpf, upw, or npf packages.
208+ Must have dis, disv, or disu and lpf, upw, or npf packages.
30209 r : 1D array-like of ints, of length n locations
31210 row indices (optional; alternately specify x, y).
32211 Only valid for structured grids.
@@ -55,7 +234,7 @@ def get_transmissivities(
55234 - r, c (row, column indices)
56235 - x, y (real world coordinates)
57236
58- For vertex grids only x, y coordinates are supported.
237+ For vertex and unstructured grids, only x, y coordinates are supported.
59238
60239 Examples
61240 --------
@@ -78,6 +257,9 @@ def get_transmissivities(
78257 nrow = m .nrow
79258 ncol = m .ncol
80259
260+ # get grid adapter
261+ adapter = _get_grid_adapter (m , nlay )
262+
81263 # get slicing indices
82264 if r is not None and c is not None :
83265 if grid_type != "structured" :
@@ -93,21 +275,18 @@ def get_transmissivities(
93275 else :
94276 raise ValueError ("Must specify r, c indices or x, y locations." )
95277
96- # slice k
278+ # get k array using adapter (handles all flow packages)
97279 paklist = m .get_package_list ()
98- if "LPF" in paklist :
99- hk = m .lpf .hk .array [(slice (None ),) + indices ]
100- elif "UPW" in paklist :
101- hk = m .upw .hk .array [(slice (None ),) + indices ]
102- elif "NPF" in paklist :
103- hk = m .npf .k .array [(slice (None ),) + indices ]
104- else :
105- raise ValueError ("No LPF, UPW, or NPF package." )
280+ k_array = adapter .get_k_array (paklist )
281+ hk = k_array [(slice (None ),) + indices ]
282+
283+ # get and slice bottom array
284+ botm_array = adapter .get_bottom_array ()
285+ botm = botm_array [(slice (None ),) + indices ]
106286
107- # slice botm
108- botm = m . dis . botm . array [( slice ( None ),) + indices ]
287+ # normalize and slice heads using adapter
288+ heads = adapter . normalize_heads_array ( heads )
109289
110- # slice heads
111290 if grid_type == "structured" and heads .shape == (nlay , nrow , ncol ):
112291 heads = heads [(slice (None ),) + indices ]
113292 elif grid_type != "structured" and heads .shape == (nlay , ncpl ):
@@ -117,14 +296,11 @@ def get_transmissivities(
117296
118297 # open interval tops/bottoms default to model top/bottom
119298 if sctop is None :
120- sctop = m . dis . top . array [ indices ]
299+ sctop = adapter . get_top_for_slice ( indices , grid_type )
121300 if scbot is None :
122- scbot = m . dis . botm . array [( - 1 ,) + indices ]
301+ scbot = botm [ - 1 , : ]
123302
124- # make an array of layer tops
125- tops = np .empty_like (botm , dtype = float )
126- tops [0 , :] = m .dis .top .array [indices ]
127- tops [1 :, :] = botm [:- 1 ]
303+ tops = adapter .get_layer_tops (botm , indices , grid_type )
128304
129305 # expand top and bottom arrays to be same shape as botm, thickness, etc.
130306 # (so we have an open interval value for each layer)
0 commit comments