forked from MODFLOW-ORG/modflow6-examples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathex-gwf-curvilinear.py
2654 lines (2286 loc) · 86.9 KB
/
ex-gwf-curvilinear.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ## Curvilinear example
#
# This example, ex-gwf-curvilinear, shows how the MODFLOW 6 DISV Package
# can be used to simulate a multipart curvilinear models.
#
# The example reproduces the hypothetical model grid presented in Figure 6 of
# Romero, D. M., & Silver, S. E. (2006).
# Grid cell distortion and MODFLOW's integrated finite difference
# numerical solution. Groundwater, 44(6), 797-802.
#
# The hypothetical, curvilinear grid is built in three parts:
# 1) 180 to 270 degree curvilinear grid
# 2) 16 by 18 structured grid
# 3) 90 to 0 degree curvilinear grid
# that are merged, as 1-2-3, to make the final multipart curvilinear grid.
# ### Initial setup
#
# ### Initial setup
#
# Import dependencies, define the example name and workspace, and read settings from environment variables.
# +
import copy
import os
import pathlib as pl
from itertools import cycle
from typing import List
import flopy
import matplotlib.pyplot as plt
import numpy as np
from flopy.plot.styles import styles
from modflow_devtools.misc import get_env, timed
# Example name and base workspace
sim_name = "ex-gwf-curvilin"
workspace = pl.Path("../examples")
# Settings from environment variables
write = get_env("WRITE", True)
run = get_env("RUN", True)
plot = get_env("PLOT", True)
plot_show = get_env("PLOT_SHOW", True)
plot_save = get_env("PLOT_SAVE", True)
# -
# ### Curvilinear grid
#
# Define some utilities to construct a curvilinear grid.
# +
class DisvPropertyContainer:
"""
Dataclass that stores MODFLOW 6 DISV grid information.
This is a base class that stores DISV **kwargs information used
by flopy for building a ``flopy.mf6.ModflowGwfdisv`` object.
All indices are zero-based, but translated to one-base for the figures and
by flopy for use with MODFLOW 6.
If no arguments are provided then an empty object is returned.
Parameters
----------
nlay : int
Number of layers.
vertices : list[list[int, float, float]]
List of vertices structured as
``[[iv, xv, vy], ...]``
where
``iv`` is the vertex index,
``xv`` is the x-coordinate, and
``yv`` is the y-coordinate.
cell2d : list[list[int, float, float, int, int...]]
List of MODFLOW 6 cells structured as
```[[icell2d, xc, yc, ncvert, icvert], ...]```
where
``icell2d`` is the cell index,
``xc`` is the x-coordinate for the cell center,
``yc`` is the y-coordinate for the cell center,
``ncvert`` is the number of vertices required to define the cell,
``icvert`` is a list of vertex indices that define the cell, and
in clockwise order.
top : np.ndarray
Is the top elevation for each cell in the top model layer.
botm : list[np.ndarray]
List of bottom elevation by layer for all model cells.
origin_x : float, default=0.0
X-coordinate of the origin used as the reference point for other
vertices. This is used for shift and rotate operations.
origin_y : float, default=0.0
X-coordinate of the origin used as the reference point for other
vertices. This is used for shift and rotate operations.
rotation : float, default=0.0
Rotation angle in degrees for the model grid.
shift_origin : bool, default=True
If True and `origin_x` or `origin_y` is non-zero, then all vertices are
shifted from an assumed (0.0, 0.0) origin to the (origin_x, origin_y)
location.
rotate_grid, default=True
If True and `rotation` is non-zero, then all vertices are rotated by
rotation degrees around (origin_x, origin_y).
Attributes
----------
nlay : int
Number of layers.
ncpl : int
Number of cells per layer.
nvert : int
Number of vertices.
vertices : list[list]
List of vertices structured as ``[[iv, xv, vy], ...]``
cell2d : list[list]
List of 2D cells structured as ```[[icell2d, xc, yc, ncvert, icvert], ...]```
top : np.ndarray
Top elevation for each cell in the top model layer.
botm : list[np.ndarray]
List of bottom elevation by layer for all model cells.
origin_x : float
X-coordinate reference point used by grid.
origin_y : float
Y-coordinate reference point used by grid.
rotation : float
Rotation angle of grid about (origin_x, origin_y)
Methods
-------
get_disv_kwargs()
Get the keyword arguments for creating a MODFLOW-6 DISV package.
plot_grid(...)
Plot the model grid from `vertices` and `cell2d` attributes.
change_origin(new_x_origin, new_y_origin)
Change the origin of the grid.
rotate_grid(rotation)
Rotate the grid.
get_centroid(icvert, vertices=None)
Calculate the centroid of a cell given by list of vertices `icvert`.
copy()
Create and return a copy of the current object.
"""
nlay: int
ncpl: int
nvert: int
vertices: List[list] # [[iv, xv, yv], ...]
cell2d: List[list] # [[ic, xc, yc, ncvert, icvert], ...]
top: np.ndarray
botm: List[np.ndarray]
origin_x: float
origin_y: float
rotation: float
def __init__(
self,
nlay=-1,
vertices=None,
cell2d=None,
top=None,
botm=None,
origin_x=0.0,
origin_y=0.0,
rotation=0.0,
shift_origin=True,
rotate_grid=True,
):
if nlay is None or nlay < 1:
self._init_empty()
return
self.nlay = nlay
self.ncpl = len(cell2d)
self.nvert = len(vertices)
self.vertices = [] if vertices is None else copy.deepcopy(vertices)
self.cell2d = [] if cell2d is None else copy.deepcopy(cell2d)
self.top = np.array([]) if top is None else copy.deepcopy(top)
self.botm = [] if botm is None else copy.deepcopy(botm)
self.origin_x, self.origin_y, self.rotation = 0.0, 0.0, 0.0
if shift_origin:
if abs(origin_x) > 1.0e-30 or abs(origin_y) > 1.0e-30:
self.change_origin(origin_x, origin_y)
elif not shift_origin:
self.origin_x, self.origin_y = origin_x, origin_y
if rotate_grid:
self.rotate_grid(rotation)
elif not shift_origin:
self.rotation = rotation
def get_disv_kwargs(self):
"""
Get the dict of keyword arguments for creating a MODFLOW-6 DISV
package using ``flopy.mf6.ModflowGwfdisv``.
"""
return {
"nlay": self.nlay,
"ncpl": self.ncpl,
"top": self.top,
"botm": self.botm,
"nvert": self.nvert,
"vertices": self.vertices,
"cell2d": self.cell2d,
}
def __repr__(self, cls="DisvPropertyContainer"):
return (
f"{cls}(\n\n"
f"nlay={self.nlay}, ncpl={self.ncpl}, nvert={self.nvert}\n\n"
f"origin_x={self.origin_x}, origin_y={self.origin_y}, "
f"rotation={self.rotation}\n\n"
f"vertices =\n{self._string_repr(self.vertices)}\n\n"
f"cell2d =\n{self._string_repr(self.cell2d)}\n\n"
f"top =\n{self.top}\n\n"
f"botm =\n{self.botm}\n\n)"
)
def _init_empty(self):
self.nlay = 0
self.ncpl = 0
self.nvert = 0
self.vertices = []
self.cell2d = []
self.top = np.array([])
self.botm = []
self.origin_x = 0.0
self.origin_y = 0.0
self.rotation = 0.0
def change_origin(self, new_x_origin, new_y_origin):
shift_x_origin = new_x_origin - self.origin_x
shift_y_origin = new_y_origin - self.origin_y
self.shift_origin(shift_x_origin, shift_y_origin)
def shift_origin(self, shift_x_origin, shift_y_origin):
if abs(shift_x_origin) > 1.0e-30 or abs(shift_y_origin) > 1.0e-30:
self.origin_x += shift_x_origin
self.origin_y += shift_y_origin
for vert in self.vertices:
vert[1] += shift_x_origin
vert[2] += shift_y_origin
for cell in self.cell2d:
cell[1] += shift_x_origin
cell[2] += shift_y_origin
def rotate_grid(self, rotation):
"""Rotate grid around origin_x, origin_y for given angle in degrees.
References
----------
[1] https://en.wikipedia.org/wiki/Transformation_matrix#Rotation
"""
#
if abs(rotation) > 1.0e-30:
self.rotation += rotation
sin, cos = np.sin, np.cos
a = np.radians(rotation)
x0, y0 = self.origin_x, self.origin_y
# Steps to shift
# 0) Get x, y coordinate to be shifted
# 1) Shift coordinate's reference point to origin
# 2) Rotate around origin
# 3) Shift back to original reference point
for vert in self.vertices:
_, x, y = vert
x, y = x - x0, y - y0
x, y = x * cos(a) - y * sin(a), x * sin(a) + y * cos(a)
vert[1] = x + x0
vert[2] = y + y0
for cell in self.cell2d:
_, x, y, *_ = cell
x, y = x - x0, y - y0
x, y = x * cos(a) - y * sin(a), x * sin(a) + y * cos(a)
cell[1] = x + x0
cell[2] = y + y0
@staticmethod
def _string_repr(list_list, sep=",\n"):
dim = len(list_list)
s = []
if dim == 0:
return "[]"
if dim < 7:
for elm in list_list:
s.append(repr(elm))
else:
for it in range(3):
s.append(repr(list_list[it]))
s.append("...")
for it in range(-3, 0):
s.append(repr(list_list[it]))
return sep.join(s)
def property_copy_to(self, DisvPropertyContainerType):
if isinstance(DisvPropertyContainerType, DisvPropertyContainer):
DisvPropertyContainerType.nlay = self.nlay
DisvPropertyContainerType.ncpl = self.ncpl
DisvPropertyContainerType.nvert = self.nvert
DisvPropertyContainerType.vertices = self.vertices
DisvPropertyContainerType.cell2d = self.cell2d
DisvPropertyContainerType.top = self.top
DisvPropertyContainerType.botm = self.botm
DisvPropertyContainerType.origin_x = self.origin_x
DisvPropertyContainerType.origin_y = self.origin_y
DisvPropertyContainerType.rotation = self.rotation
else:
raise RuntimeError(
"DisvPropertyContainer.property_copy_to "
"can only copy to objects that inherit "
"properties from DisvPropertyContainer"
)
def copy(self):
cp = DisvPropertyContainer()
self.property_copy_to(cp)
return cp
def keys(self):
"""
Get the keys used by ``flopy.mf6.ModflowGwfdisv``.
This method is only used to provide unpacking support for
`DisvPropertyContainer` objects and subclasses.
That is:
``flopy.mf6.ModflowGwfdisv(gwf, **DisvPropertyContainer)``
Returns
-------
list
List of keys used by ``flopy.mf6.ModflowGwfdisv``
"""
return self.get_disv_kwargs().keys()
def __getitem__(self, k):
if hasattr(self, k):
return getattr(self, k)
raise KeyError(f"{k}")
@staticmethod
def _get_array(cls_name, var, rep, rep2=None):
if rep2 is None:
rep2 = rep
try:
dim = len(var)
except TypeError:
dim, var = 1, [var]
try: # Check of array needs to be flattened
_ = len(var[0])
tmp = []
for row in var:
tmp.extend(row)
var = tmp
dim = len(var)
except TypeError:
pass
if dim != 1 and dim != rep and dim != rep2:
msg = f"{cls_name}(var): var must be a scalar "
msg += f"or have len(var)=={rep}"
if rep2 != rep:
msg += f"or have len(var)=={rep2}"
raise IndexError(msg)
if dim == 1:
return np.full(rep, var[0], dtype=np.float64)
else:
return np.array(var, dtype=np.float64)
def get_centroid(self, icvert, vertices=None):
"""
Calculate the centroid of a cell for a given set of vertices.
Parameters
----------
icvert : list[int]
List of vertex indices for the cell.
vertices : list[list], optional
List of vertices that `icvert` references to define the cell.
If not present, then the `vertices` attribute is used.
Returns
-------
tuple
A tuple containing the X and Y coordinates of the centroid.
References
----------
[1] https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
"""
if vertices is None:
vertices = self.vertices
nv = len(icvert)
x = []
y = []
for iv in icvert:
x.append(vertices[iv][1])
y.append(vertices[iv][2])
if nv < 3:
raise RuntimeError("get_centroid: len(icvert) < 3")
if nv == 3: # Triangle
return sum(x) / 3, sum(y) / 3
xc, yc = 0.0, 0.0
signedArea = 0.0
for i in range(nv - 1):
x0, y0, x1, y1 = x[i], y[i], x[i + 1], y[i + 1]
a = x0 * y1 - x1 * y0
signedArea += a
xc += (x0 + x1) * a
yc += (y0 + y1) * a
x0, y0, x1, y1 = x1, y1, x[0], y[0]
a = x0 * y1 - x1 * y0
signedArea += a
xc += (x0 + x1) * a
yc += (y0 + y1) * a
signedArea *= 0.5
return xc / (6 * signedArea), yc / (6 * signedArea)
def plot_grid(
self,
title="",
plot_time=0.0,
show=True,
figsize=(10, 10),
dpi=None,
xlabel="",
ylabel="",
cell2d_override=None,
vertices_override=None,
ax_override=None,
cell_dot=True,
cell_num=True,
cell_dot_size=7.5,
cell_dot_color="coral",
vertex_dot=True,
vertex_num=True,
vertex_dot_size=6.5,
vertex_dot_color="skyblue",
grid_color="grey",
):
"""
Plot the model grid with optional features.
All inputs are optional.
Parameters
----------
title : str, default=""
Title for the plot.
plot_time : float, default=0.0
Time interval for animation (if greater than 0).
show : bool, default=True
Whether to display the plot. If false, then plot_time is set to -1
and function returns figure and axis objects.
figsize : tuple, default=(10, 10)
Figure size (width, height) in inches. Default is (10, 10).
dpi : float, default=None
Set dpi for Matplotlib figure.
If set to None, then uses Matplotlib default dpi.
xlabel : str, default=""
X-axis label.
ylabel : str, default=""
Y-axis label.
cell2d_override : list[list], optional
List of ``cell2d`` cells to override the object's cell2d.
Default is None.
vertices_override : list[list], optional
List of vertices to override the object's vertices.
Default is None.
ax_override : matplotlib.axes.Axes, optional
Matplotlib axis object to use for generating plot instead of
making a new figure and axis objects. If present, then show is
set to False and plot_time to -1.
cell_dot : bool, default=True
Whether to add a filled circle at the cell center locations.
cell_num : bool, default=True
Whether to label cells with cell2d index numbers.
cell_dot_size : bool, default=7.5
The size, in points, of the filled circles and index numbers
at the cell center.
cell_dot_color : str, default="coral"
The color of the filled circles at the cell center.
vertex_num : bool, default=True
Whether to label vertices with the vertex numbers.
vertex_dot : bool, default=True
Whether to add a filled circle at the vertex locations.
vertex_dot_size : bool, default=6.5
The size, in points, of the filled circles and index numbers
at the vertex locations.
vertex_dot_color : str, default="skyblue"
The color of the filled circles at the vertex locations.
grid_color : str or tuple[str], default="grey"
The color of the grid lines.
If plot_time > 0, then animation cycled through the colors
for each cell outline.
Returns
-------
(matplotlib.figure.Figure, matplotlib.axis.Axis) or None
If `show` is False, returns the Figure and Axis objects;
otherwise, returns None.
Raises
------
RuntimeError
If either `cell2d_override` or `vertices_override` is provided
without the other.
Notes
-----
This method plots the grid using Matplotlib. It can label cells and
vertices with numbers, show an animation of the plotting process, and
customize the plot appearance.
Note that figure size (`figsize`) is in inches and the total pixels is based on
the `dpi` (dots per inch). For example, figsize=(3, 5) and
dpi=110, results in a figure resolution of (330, 550).
Changing `figsize` does not effect the size
Elements (text, markers, lines) in Matplotlib use
72 points per inch (ppi) as a basis for translating to dpi.
Any changes to dpi result in a scaling effect. The default dpi of 100
results in a line width 100/72 pixels wide.
Similarly, a line width of 1 point with a dpi set to 72 results in
a line that is 1 pixel wide. The following then occurs:
2*72 dpi results in a line width 2 pixels;
3*72 dpi results in a line width 3 pixels; and
600 dpi results in a line width 600/72 pixels.
Conversely, changing `figsize` increases the total pixel count, but
elements maintain the same dpi. That is the figure wireframe will be
larger, but the elements will have the same pixel widths. For example,
a line width of 1 will have a width of 100/72 in for any figure size,
as long as the dpi is set to 100.
"""
if cell2d_override is not None and vertices_override is not None:
cell2d = cell2d_override
vertices = vertices_override
elif cell2d_override is not None or vertices_override is not None:
raise RuntimeError(
"plot_vertex_grid: if you specify "
"cell2d_override or vertices_override, "
"then you must specify both."
)
else:
cell2d = self.cell2d
vertices = self.vertices
if ax_override is None:
fig = plt.figure(figsize=figsize, dpi=dpi)
ax = fig.add_subplot()
else:
show = False
ax = ax_override
ax.set_aspect("equal", adjustable="box")
if not show:
plot_time = -1.0
if not isinstance(grid_color, tuple):
grid_color = (grid_color,)
ColorCycler = grid_color
if plot_time > 0.0 and grid_color == ("grey",):
ColorCycler = ("green", "red", "grey", "magenta", "cyan", "yellow")
ColorCycle = cycle(ColorCycler)
xvert = []
yvert = []
for r in vertices:
xvert.append(r[1])
yvert.append(r[2])
xcell = []
ycell = []
for r in cell2d:
xcell.append(r[1])
ycell.append(r[2])
if title != "":
ax.set_title(title)
if xlabel != "":
ax.set_xlabel(xlabel)
if ylabel != "":
ax.set_ylabel(ylabel)
vert_size = vertex_dot_size
cell_size = cell_dot_size
if vertex_dot:
ax.plot(
xvert,
yvert,
linestyle="None",
color=vertex_dot_color,
markersize=vert_size,
marker="o",
markeredgewidth=0.0,
zorder=2,
)
if cell_dot:
ax.plot(
xcell,
ycell,
linestyle="None",
color=cell_dot_color,
markersize=cell_size,
marker="o",
markeredgewidth=0.0,
zorder=2,
)
if cell_num:
for ic, xc, yc, *_ in cell2d:
ax.text(
xc,
yc,
f"{ic + 1}",
fontsize=cell_size,
color="black",
fontfamily="Arial Narrow",
fontweight="black",
rasterized=False,
horizontalalignment="center",
verticalalignment="center",
zorder=3,
)
if vertex_num:
for iv, xv, yv in vertices:
ax.text(
xv,
yv,
f"{iv + 1}",
fontsize=vert_size,
fontweight="black",
rasterized=False,
color="black",
fontfamily="Arial Narrow",
horizontalalignment="center",
verticalalignment="center",
zorder=3,
)
if plot_time > 0:
plt.show(block=False)
plt.pause(5 * plot_time)
for ic, xc, yc, ncon, *vert in cell2d:
color = next(ColorCycle)
conn = vert + [vert[0]] # Extra node to complete polygon
for i in range(ncon):
n1, n2 = conn[i], conn[i + 1]
px = [vertices[n1][1], vertices[n2][1]]
py = [vertices[n1][2], vertices[n2][2]]
ax.plot(px, py, color=color, zorder=1)
if plot_time > 0:
plt.draw()
plt.pause(plot_time)
if show:
plt.show()
elif ax_override is None:
return fig, ax
class DisvStructuredGridBuilder(DisvPropertyContainer):
"""
A class for generating a structured MODFLOW 6 DISV grid.
This class inherits from the `DisvPropertyContainer` class and provides
methods to generate a rectangular, structured grid give the number rows
(nrow), columns (ncol), row widths, and columns widths. Rows are
discretized along the y-axis and columns along the x-axis. The row, column
structure follows MODFLOW 6 structured grids. That is, the first row has
the largest y-axis vertices and last row the lowest; and the first column
has the lowest x-axis vertices and last column the highest.
All indices are zero-based, but translated to one-base for the figures and
by flopy for use with MODFLOW 6.
The following shows the placement for (row, column) pairs
in a nrow=3 and ncol=5 model:
``(0,0) (0,1) (0,2) (0,3) (0,4)``
``(1,0) (1,1) (1,2) (1,3) (1,4)``
``(2,0) (2,1) (2,2) (2,3) (2,4)``
Array-like structures that are multidimensional (has rows and columns)
are flatten by concatenating each row. Using the previous example,
the following is the flattened representation:
``(0,0) (0,1) (0,2) (0,3) (0,4) (1,0) (1,1) (1,2) (1,3) (1,4) (2,0) (2,1) (2,2) (2,3) (2,4)``
If no arguments are provided then an empty object is returned.
Parameters
----------
nlay : int
Number of layers
nrow : int
Number of rows (y-direction cells).
ncol : int
Number of columns (x-direction cells).
row_width : float or array_like
Width of y-direction cells (each row). If a single value is provided,
it will be used for all rows. Otherwise, it must be array_like
of length ncol.
col_width : float or array_like
Width of x-direction cells (each column). If a single value is
provided, it will be used for all columns. Otherwise, it must be
array_like of length ncol.
surface_elevation : float or array_like
Surface elevation for the top layer. Can either be a single float
for the entire `top`, or array_like of length `ncpl`.
If it is a multidimensional array_like, then it is flattened to a
single dimension along the rows (first dimension).
layer_thickness : float or array_like
Thickness of each layer. Can either be a single float
for model cells, or array_like of length `nlay`, or
array_like of length `nlay`*`ncpl`.
origin_x : float, default=0.0
X-coordinate reference point the lower-left corner of the model grid.
That is, the outermost corner of ``row=nrow-1` and `col=0`.
Rotations are performed around this point.
origin_y : float, default=0.0
Y-coordinate reference point the lower-left corner of the model grid.
That is, the outermost corner of ``row=nrow-1` and `col=0`.
Rotations are performed around this point.
rotation : float, default=0.0
Rotation angle in degrees for the model grid around (origin_x, origin_y).
Attributes
----------
nrow : int
Number of rows in the grid.
ncol : int
Number of columns in the grid.
row_width : np.ndarray
Width of y-direction cells (each row).
col_width : np.ndarray
Width of x-direction cells (each column).
Methods
-------
get_disv_kwargs()
Get the keyword arguments for creating a MODFLOW-6 DISV package.
plot_grid(...)
Plot the model grid from `vertices` and `cell2d` attributes.
get_cellid(row, col, col_check=True)
Get the cellid given the row and column indices.
get_row_col(cellid)
Get the row and column indices given the cellid.
get_vertices(row, col)
Get the vertex indices for a cell given the row and column indices.
iter_row_col()
Iterate over row and column indices for each cell.
iter_row_cellid(row)
Iterate over cellid's in a specific row.
iter_column_cellid(col)
Iterate over cellid's in a specific column.
"""
nrow: int
ncol: int
row_width: np.ndarray
col_width: np.ndarray
def __init__(
self,
nlay=-1,
nrow=-1, # number of Y direction cells
ncol=-1, # number of X direction cells
row_width=10.0, # width of Y direction cells (each row)
col_width=10.0, # width of X direction cells (each column)
surface_elevation=100.0,
layer_thickness=100.0,
origin_x=0.0,
origin_y=0.0,
rotation=0.0,
):
if nlay is None or nlay < 1:
self._init_empty()
return
ncpl = ncol * nrow # Nodes per layer
self.nrow = nrow
self.ncol = ncol
ncell = ncpl * nlay
# Check if layer_thickness needs to be flattened
cls_name = "DisvStructuredGridBuilder"
top = self._get_array(cls_name, surface_elevation, ncpl)
thick = self._get_array(cls_name, layer_thickness, nlay, ncell)
self.row_width = self._get_array(cls_name, row_width, ncol)
self.col_width = self._get_array(cls_name, col_width, nrow)
bot = []
if thick.size == nlay:
for lay in range(nlay):
bot.append(top - thick[: lay + 1].sum())
else:
st = 0
sp = ncpl
bt = top.copy()
for lay in range(nlay):
bt -= thick[st:sp]
st, sp = sp, sp + ncpl
bot.append(bt)
# Build the grid
# Setup vertices
vertices = []
# Get row 1 top:
yv_model_top = self.col_width.sum()
# Assemble vertices along x-axis and model top
iv = 0
xv, yv = 0.0, yv_model_top
vertices.append([iv, xv, yv])
for c in range(ncol):
iv += 1
xv += self.row_width[c]
vertices.append([iv, xv, yv])
# Finish the rest of the grid a row at a time
for r in range(nrow):
iv += 1
yv -= self.col_width[r]
xv = 0.0
vertices.append([iv, xv, yv])
for c in range(ncol):
iv += 1
xv += self.row_width[c]
vertices.append([iv, xv, yv])
# cell2d: [icell2d, xc, yc, ncvert, icvert]
cell2d = []
ic = -1
# Finish the rest of the grid a row at a time
for r in range(nrow):
for c in range(ncol):
ic += 1
icvert = self.get_vertices(r, c)
xc, yc = self.get_centroid(icvert, vertices)
cell2d.append([ic, xc, yc, 4, *icvert])
super().__init__(nlay, vertices, cell2d, top, bot, origin_x, origin_y, rotation)
def __repr__(self):
return super().__repr__("DisvStructuredGridBuilder")
def _init_empty(self):
super()._init_empty()
nul = np.array([])
self.nrow = 0
self.ncol = 0
self.row_width = nul
self.col_width = nul
def property_copy_to(self, DisvStructuredGridBuilderType):
if isinstance(DisvStructuredGridBuilderType, DisvStructuredGridBuilder):
super().property_copy_to(DisvStructuredGridBuilderType)
DisvStructuredGridBuilderType.nrow = self.nrow
DisvStructuredGridBuilderType.ncol = self.ncol
DisvStructuredGridBuilderType.row_width = self.row_width.copy()
DisvStructuredGridBuilderType.col_width = self.col_width.copy()
else:
raise RuntimeError(
"DisvStructuredGridBuilder.property_copy_to "
"can only copy to objects that inherit "
"properties from DisvStructuredGridBuilder"
)
def copy(self):
cp = DisvStructuredGridBuilder()
self.property_copy_to(cp)
return cp
def get_cellid(self, row, col):
"""
Get the cellid given the row and column indices.
Parameters
----------
row : int
Row index.
col : int
Column index.
Returns
-------
int
cellid index
"""
return row * self.ncol + col
def get_row_col(self, cellid):
"""
Get the row and column indices given the cellid.
Parameters
----------
cellid : int
cellid index
Returns
-------
(int, int)
Row index, Column index
"""
row = cellid // self.ncol
col = cellid - row * self.ncol
return row, col
def get_vertices(self, row, col):
"""
Get the vertex indices for a cell given the row and column indices.
Parameters
----------
row : int
Row index.
col : int
Column index.
Returns
-------
list[int]
List of vertex indices that define the cell at (row, col).
"""
nver = self.ncol + 1
return [
row * nver + col,
row * nver + col + 1,
(row + 1) * nver + col + 1,
(row + 1) * nver + col,
]
def iter_row_col(self):
"""Generator that iterates through each rows' columns.
Yields
-------
(int, int)
Row index, column index
"""
for cellid in range(self.ncpl):
yield self.get_row_col(cellid)
def iter_row_cellid(self, row):
"""Generator that iterates through the cellid within a row.
That is, the cellid for all columns within the specified row.
Parameters
----------
row : int
Row index.
Yields
-------
int
cellid index
"""
for col in range(self.ncol):
yield self.get_cellid(row, col)
def iter_column_cellid(self, col):
"""Generator that iterates through the cellid within a column.