33# This example demonstrates a tentative `attrs`-based object model.
44
55from pathlib import Path
6- from typing import List , Literal , Optional , Union
6+ from typing import Any , List , Literal , NamedTuple , Optional
77
8- # ## GWF IC
98import numpy as np
109from attr import asdict , define , field , fields_dict
1110from cattr import Converter
11+ from flopy .discretization import StructuredGrid
1212from numpy .typing import NDArray
13-
14- # We can define block classes where variable descriptions become
15- # the variable's docstring. Ideally we can come up with a Python
16- # input specification that is equivalent to (and convertible to)
17- # the original MF6 input specification, while knowing as little
18- # as possible about the MF6 input format; but anything we can't
19- # get rid of can go in field `metadata`.
13+ from xarray import Dataset , DataTree
2014
2115
2216@define
23- class Options :
17+ class GwfIc :
18+ strt : NDArray [np .float64 ] = field (
19+ metadata = {"block" : "packagedata" , "shape" : "(nodes)" }
20+ )
2421 export_array_ascii : bool = field (
25- default = False ,
26- metadata = {"longname" : "export array variables to netcdf output files" },
22+ default = False , metadata = {"block" : "options" }
2723 )
28- """
29- keyword that specifies input griddata arrays should be
30- written to layered ascii output files.
31- """
32-
3324 export_array_netcdf : bool = field (
34- default = False , metadata = {"longname" : "starting head" }
25+ default = False ,
26+ metadata = {"block" : "options" },
3527 )
36- """
37- keyword that specifies input griddata arrays should be
38- written to the model output netcdf file.
39- """
40-
4128
42- # Eventually we may be able to take advantage of NumPy
43- # support for shape parameters:
44- # https://github.com/numpy/numpy/issues/16544
45- #
46- # We can still take advantage of type parameters.
29+ def __attrs_post_init__ (self ):
30+ # TODO: setup attributes for blocks?
31+ self .data = DataTree (Dataset ({"strt" : self .strt }), name = "ic" )
4732
4833
4934@define
50- class PackageData :
51- strt : NDArray [np .float64 ] = field (
52- metadata = {"longname" : "starting head" , "shape" : ("nodes" )}
35+ class GwfOc :
36+ @define
37+ class Format :
38+ columns : int
39+ width : int
40+ digits : int
41+ format : Literal ["exponential" , "fixed" , "general" , "scientific" ]
42+
43+ periods : List [List [tuple ]] = field (
44+ metadata = {"block" : "perioddata" }
45+ )
46+ budget_file : Optional [Path ] = field (
47+ default = None , metadata = {"block" : "options" }
48+ )
49+ budget_csv_file : Optional [Path ] = field (
50+ default = None , metadata = {"block" : "options" }
51+ )
52+ head_file : Optional [Path ] = field (
53+ default = None , metadata = {"block" : "options" }
54+ )
55+ printhead : Optional [Format ] = field (
56+ default = None , metadata = {"block" : "options" }
5357 )
54- """
55- is the initial (starting) head---that is, head at the
56- beginning of the GWF Model simulation. STRT must be specified for
57- all simulations, including steady-state simulations. One value is
58- read for every model cell. For simulations in which the first stress
59- period is steady state, the values used for STRT generally do not
60- affect the simulation (exceptions may occur if cells go dry and (or)
61- rewet). The execution time, however, will be less if STRT includes
62- hydraulic heads that are close to the steady-state solution. A head
63- value lower than the cell bottom can be provided if a cell should
64- start as dry.
65- """
66-
67-
68- # Putting it all together:
69-
70-
71- @define
72- class GwfIc :
73- options : Options = field ()
74- packagedata : PackageData = field ()
75-
76-
77- # ## GWF OC
78- #
79- # The output control package has a more complicated variable structure.
80- # Below docstrings/descriptions are omitted for space-saving.
81-
82-
83- @define
84- class Format :
85- columns : int = field ()
86- width : int = field ()
87- digits : int = field ()
88- format : Literal ["exponential" , "fixed" , "general" , "scientific" ] = field ()
89-
90-
91- @define
92- class Options :
93- budget_file : Optional [Path ] = field (default = None )
94- budget_csv_file : Optional [Path ] = field (default = None )
95- head_file : Optional [Path ] = field (default = None )
96- printhead : Optional [Format ] = field (default = None )
97-
98-
99- # It's awkward to have single-parameter classes, but
100- # it's the only way I got `cattrs` to distinguish a
101- # number of choices with the same shape in a union
102- # like `OCSetting`. There may be a better way.
103-
104-
105- @define
106- class All :
107- all : bool = field ()
108-
109-
110- @define
111- class First :
112- first : bool = field ()
11358
11459
11560@define
116- class Last :
117- last : bool = field ()
118-
61+ class GwfDis :
62+ nlay : int = field (metadata = {"block" : "dimensions" })
63+ ncol : int = field (metadata = {"block" : "dimensions" })
64+ nrow : int = field (metadata = {"block" : "dimensions" })
65+ delr : NDArray [np .float64 ] = field (
66+ metadata = {"block" : "griddata" , "shape" : "(ncol,)" }
67+ )
68+ delc : NDArray [np .float64 ] = field (
69+ metadata = {"block" : "griddata" , "shape" : "(nrow,)" }
70+ )
71+ top : NDArray [np .float64 ] = field (
72+ metadata = {"block" : "griddata" , "shape" : "(ncol, nrow)" }
73+ )
74+ botm : NDArray [np .float64 ] = field (
75+ metadata = {"block" : "griddata" , "shape" : "(ncol, nrow, nlay)" }
76+ )
77+ idomain : NDArray [np .float64 ] = field (
78+ metadata = {"block" : "griddata" , "shape" : "(ncol, nrow, nlay)" }
79+ )
80+ length_units : str = field (default = None , metadata = {"block" : "options" })
81+ nogrb : bool = field (default = False , metadata = {"block" : "options" })
82+ xorigin : float = field (default = None , metadata = {"block" : "options" })
83+ yorigin : float = field (default = None , metadata = {"block" : "options" })
84+ angrot : float = field (default = None , metadata = {"block" : "options" })
85+ export_array_netcdf : bool = field (
86+ default = False , metadata = {"block" : "options" }
87+ )
11988
120- @define
121- class Steps :
122- steps : List [int ] = field ()
89+ def __attrs_post_init__ (self ):
90+ self .data = DataTree (
91+ Dataset (
92+ {
93+ "nlay" : self .nlay ,
94+ "ncol" : self .ncol ,
95+ "nrow" : self .nrow ,
96+ "delr" : self .delr ,
97+ "delc" : self .delc ,
98+ "top" : self .top ,
99+ "botm" : self .botm ,
100+ "idomain" : self .idomain ,
101+ }
102+ ),
103+ name = "dis" ,
104+ )
105+ # TODO: check for parent and update dimensions
106+ # then try to realign any existing packages?
123107
124108
125109@define
126- class Frequency :
127- frequency : int = field ()
110+ class Gwf :
111+ dis : GwfDis = field ()
112+ ic : GwfIc = field ()
128113
114+ def __attrs_post_init__ (self ):
115+ self .data = DataTree .from_dict (
116+ {"/dis" : self .dis , "/ic" : self .ic }, name = "gwf"
117+ )
118+ self .grid = StructuredGrid (** asdict (self .dis ))
129119
130- PrintSave = Literal ["print" , "save" ]
131- RType = Literal ["budget" , "head" ]
132- OCSetting = Union [All , First , Last , Steps , Frequency ]
120+ @ic .validator
121+ def _check_dims (self , attribute , value ):
122+ assert value .strt .shape == (
123+ self .dis .nlay * self .dis .nrow * self .dis .ncol
124+ )
133125
134126
135- @define
136- class OutputControlData :
137- printsave : PrintSave = field ()
138- rtype : RType = field ()
139- ocsetting : OCSetting = field ()
140-
141- @classmethod
142- def from_tuple (cls , t ):
143- t = list (t )
144- printsave = t .pop (0 )
145- rtype = t .pop (0 )
146- ocsetting = {
147- "all" : All ,
148- "first" : First ,
149- "last" : Last ,
150- "steps" : Steps ,
151- "frequency" : Frequency ,
152- }[t .pop (0 ).lower ()](t )
153- return cls (printsave , rtype , ocsetting )
154-
155-
156- Period = List [OutputControlData ]
157- Periods = List [Period ]
127+ # We can define a package with some data.
158128
159129
160- @define
161- class GwfOc :
162- options : Options = field ()
163- periods : Periods = field ()
130+ oc = GwfOc (
131+ budget_file = "some/file/path.cbc" ,
132+ periods = [[("print" , "budget" , "steps" , 1 , 3 , 5 )]]
133+ )
134+ assert isinstance (oc .budget_file , str ) # TODO path
164135
165136
166137# We now set up a `cattrs` converter to convert an unstructured
@@ -169,63 +140,19 @@ class GwfOc:
169140converter = Converter ()
170141
171142
172- # Register a hook for the `OutputControlData.from_tuple` method.
173- # MODFLOW 6 defines records as tuples, from which we'll need to
174- # instantiate objects.
175-
176-
177- def output_control_data_hook (value , _ ) -> OutputControlData :
178- return OutputControlData .from_tuple (value )
179-
180-
181- converter .register_structure_hook (OutputControlData , output_control_data_hook )
182-
183-
184- # We can inspect the input specification with `attrs` machinery.
185-
186-
187- spec = fields_dict (OutputControlData )
188- assert len (spec ) == 3
189-
190- ocsetting = spec ["ocsetting" ]
191- assert ocsetting .type is OCSetting
192-
193-
194- # We can define a block with some data.
195-
196-
197- options = Options (
198- budget_file = "some/file/path.cbc" ,
199- )
200- assert isinstance (options .budget_file , str ) # TODO path
201- assert len (asdict (options )) == 4
202-
203-
204- # We can load a record from a tuple.
205-
206-
207- ocdata = OutputControlData .from_tuple (("print" , "budget" , "steps" , 1 , 3 , 5 ))
208- assert ocdata .printsave == "print"
209- assert ocdata .rtype == "budget"
210- assert ocdata .ocsetting == Steps ([1 , 3 , 5 ])
211-
212-
213143# We can load the full package from an unstructured dictionary,
214144# as would be returned by a separate IO layer in the future.
215145# (Either hand-written or using e.g. lark.)
216146
217-
218147gwfoc = converter .structure (
219148 {
220- "options" : {
221- "budget_file" : "some/file/path.cbc" ,
222- "head_file" : "some/file/path.hds" ,
223- "printhead" : {
224- "columns" : 1 ,
225- "width" : 10 ,
226- "digits" : 8 ,
227- "format" : "scientific" ,
228- },
149+ "budget_file" : "some/file/path.cbc" ,
150+ "head_file" : "some/file/path.hds" ,
151+ "printhead" : {
152+ "columns" : 1 ,
153+ "width" : 10 ,
154+ "digits" : 8 ,
155+ "format" : "scientific" ,
229156 },
230157 "periods" : [
231158 [
@@ -236,11 +163,9 @@ def output_control_data_hook(value, _) -> OutputControlData:
236163 },
237164 GwfOc ,
238165)
239- assert gwfoc .options . budget_file == Path ("some/file/path.cbc" )
240- assert gwfoc .options . printhead .width == 10
241- assert gwfoc .options . printhead .format == "scientific"
166+ assert gwfoc .budget_file == Path ("some/file/path.cbc" )
167+ assert gwfoc .printhead .width == 10
168+ assert gwfoc .printhead .format == "scientific"
242169period = gwfoc .periods [0 ]
243170assert len (period ) == 2
244- assert period [0 ] == OutputControlData .from_tuple (
245- ("print" , "budget" , "steps" , 1 , 3 , 5 )
246- )
171+ assert period [0 ] == ("print" , "budget" , "steps" , 1 , 3 , 5 )
0 commit comments