Skip to content

Commit 08b95ea

Browse files
feat(data storage): Data automatically stored internally when simulation or model relative path changed (#1126) (#1157)
* refact(paths): New simulation and model method "set_all_data_internal" added that sets all data internal. This method is automatically called whenever the simulation or model path are changed. This greatly simplifies flopy's behavior since flopy no longer needs to keep track of former and new external file paths that can possibly change multiple times before write is called. * refact(path): Added combination of changing model relative path and setting all data external to test case. * fix(cleanup): code cleanup * fix(code cleanup): code cleanup * fix(set data internal/external): code made more general to handle transient data without stress period keys * fix(formatting) * feat(data columns + paths): Flopy now automatically defaults to ncol for max_columns_of_data. Added support for specifying where to store external files when calling set_all_files_external. Path is relative to model(s)/simulation. Added several more path tests.
1 parent 86b270d commit 08b95ea

File tree

10 files changed

+543
-166
lines changed

10 files changed

+543
-166
lines changed

autotest/t504_test.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,12 @@ def test001a_tharmonic():
7676
sim.simulation_data.mfpath.set_sim_path(run_folder)
7777

7878
# write simulation to new location
79-
sim.set_all_data_external()
79+
sim.set_all_data_external(external_data_folder="data")
8080
sim.write_simulation(silent=True)
81-
81+
# verify external data written to correct location
82+
data_path = os.path.join(run_folder, "data", "flow15.dis_botm.txt")
83+
assert os.path.exists(data_path)
84+
# model export test
8285
model = sim.get_model(model_name)
8386
model.export("{}/tharmonic.nc".format(model.model_ws))
8487
model.export("{}/tharmonic.shp".format(model.model_ws))

autotest/t505_test.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from flopy.mf6.modflow.mfutlts import ModflowUtlts
3535
from flopy.mf6.utils import testutils
3636
from flopy.mf6.mfbase import MFDataException
37+
from flopy.mf6.mfbase import ExtFileAction
3738

3839
try:
3940
import shapefile
@@ -392,6 +393,9 @@ def np001():
392393
# write simulation to new location
393394
sim.set_all_data_external()
394395
sim.write_simulation()
396+
assert (
397+
sim.simulation_data.max_columns_of_data == dis_package.ncol.get_data()
398+
)
395399

396400
# run simulation
397401
if run:
@@ -420,6 +424,44 @@ def np001():
420424
# clean up
421425
sim.delete_output_files()
422426

427+
# test path changes, model file path relative to the simulation folder
428+
md_folder = "model_folder"
429+
model.set_model_relative_path(md_folder)
430+
run_folder_new = os.path.join(run_folder, md_folder)
431+
# set all data external
432+
sim.set_all_data_external(external_data_folder="data")
433+
sim.write_simulation()
434+
435+
assert (
436+
sim.simulation_data.max_columns_of_data == dis_package.ncol.get_data()
437+
)
438+
# run simulation from new path with external files
439+
if run:
440+
sim.run_simulation()
441+
442+
# get expected results
443+
budget_file = os.path.join(os.getcwd(), expected_cbc_file)
444+
budget_obj = bf.CellBudgetFile(budget_file, precision="double")
445+
budget_frf_valid = np.array(
446+
budget_obj.get_data(text="FLOW-JA-FACE", full3D=True)
447+
)
448+
449+
# compare output to expected results
450+
head_file = os.path.join(os.getcwd(), expected_head_file)
451+
head_new = os.path.join(run_folder_new, "np001_mod.hds")
452+
outfile = os.path.join(run_folder_new, "head_compare.dat")
453+
assert pymake.compare_heads(
454+
None, None, files1=head_file, files2=head_new, outfile=outfile
455+
)
456+
457+
budget_frf = sim.simulation_data.mfdata[
458+
(model_name, "CBC", "FLOW-JA-FACE")
459+
]
460+
assert array_util.array_comp(budget_frf_valid, budget_frf)
461+
462+
# clean up
463+
sim.delete_output_files()
464+
423465
try:
424466
error_occurred = False
425467
well_spd = {
@@ -594,6 +636,8 @@ def np002():
594636
sim_ws=run_folder,
595637
nocheck=True,
596638
)
639+
sim.simulation_data.max_columns_of_data = 22
640+
597641
name = sim.name_file
598642
assert name.continue_.get_data() == None
599643
assert name.nocheck.get_data() == True
@@ -659,6 +703,9 @@ def np002():
659703
idomain=2,
660704
filename="{}.dis".format(model_name),
661705
)
706+
assert sim.simulation_data.max_columns_of_data == 22
707+
sim.simulation_data.max_columns_of_data = dis_package.ncol.get_data()
708+
662709
ic_vals = [
663710
100.0,
664711
100.0,
@@ -815,6 +862,20 @@ def np002():
815862
spd2 = ghb2.stress_period_data.get_data(1)
816863
assert spd2 is None
817864

865+
# test paths
866+
sim_path_test = os.path.join(run_folder, "sim_path")
867+
sim.set_sim_path(sim_path_test)
868+
model.set_model_relative_path("model")
869+
# make external data folder path relative to simulation folder
870+
ext_folder = os.path.join("..", "data")
871+
sim.set_all_data_external(external_data_folder=ext_folder)
872+
sim.write_simulation()
873+
# test
874+
ext_full_path = os.path.join(sim_path_test, "data")
875+
assert os.path.exists(
876+
os.path.join(ext_full_path, "np002_mod.dis_botm.txt")
877+
)
878+
818879
return
819880

820881

@@ -2525,7 +2586,6 @@ def test006_2models_gnc():
25252586
sim.simulation_data.mfpath.set_sim_path(run_folder)
25262587

25272588
# write simulation to new location
2528-
sim.set_all_data_external()
25292589
sim.write_simulation()
25302590

25312591
# run simulation
@@ -2548,6 +2608,24 @@ def test006_2models_gnc():
25482608
None, None, files1=head_file, files2=head_new, outfile=outfile
25492609
)
25502610

2611+
# test external file paths
2612+
sim_path = os.path.join(run_folder, "path_test")
2613+
sim.simulation_data.mfpath.set_sim_path(sim_path)
2614+
model_1.set_model_relative_path("model1")
2615+
model_2.set_model_relative_path("model2")
2616+
sim.set_all_data_external(external_data_folder="data")
2617+
sim.write_simulation()
2618+
ext_file_path_1 = os.path.join(
2619+
sim_path, "model1", "data", "model1.dis_botm.txt"
2620+
)
2621+
assert os.path.exists(ext_file_path_1)
2622+
ext_file_path_2 = os.path.join(
2623+
sim_path, "model2", "data", "model2.dis_botm.txt"
2624+
)
2625+
assert os.path.exists(ext_file_path_2)
2626+
if run:
2627+
sim.run_simulation()
2628+
25512629
# clean up
25522630
sim.delete_output_files()
25532631

@@ -3165,8 +3243,8 @@ def test_transport():
31653243

31663244

31673245
if __name__ == "__main__":
3168-
np002()
31693246
np001()
3247+
np002()
31703248
test004_bcfss()
31713249
test005_advgw_tidal()
31723250
test006_2models_gnc()

flopy/mf6/data/mfdataarray.py

Lines changed: 138 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
from collections import OrderedDict
44
from ..data.mfstructure import DatumType
55
from .mfdatastorage import DataStorage, DataStructureType, DataStorageType
6-
from ...utils.datautil import MultiList
6+
from ...utils.datautil import MultiList, DatumUtil
77
from ..mfbase import ExtFileAction, MFDataException, VerbosityLevel
88
from ..utils.mfenums import DiscretizationType
99
from ...datbase import DataType
1010
from .mffileaccess import MFFileAccessArray
1111
from .mfdata import MFMultiDimVar, MFTransient
12+
from ...mbase import ModelInterface
1213

1314

1415
class MFArray(MFMultiDimVar):
@@ -483,7 +484,6 @@ def store_as_external_file(
483484
check_data : bool
484485
Verify data prior to storing
485486
"""
486-
487487
storage = self._get_storage_obj()
488488
if storage is None:
489489
self._set_storage_obj(self._new_storage(False, True))
@@ -563,7 +563,6 @@ def store_as_external_file(
563563
self._set_data(
564564
external_data, layer=current_layer, check_data=False
565565
)
566-
567566
except Exception as ex:
568567
type_, value_, traceback_ = sys.exc_info()
569568
raise MFDataException(
@@ -582,6 +581,93 @@ def store_as_external_file(
582581
ex,
583582
)
584583

584+
def store_internal(
585+
self,
586+
layer=None,
587+
check_data=True,
588+
):
589+
"""Stores data from layer `layer` internally. For unlayered data do
590+
not pass in `layer`. If layer is not specified all layers will be
591+
stored internally
592+
593+
Parameters
594+
----------
595+
layer : int
596+
Which layer to store in external file, `None` value stores all
597+
layers.
598+
check_data : bool
599+
Verify data prior to storing
600+
"""
601+
storage = self._get_storage_obj()
602+
if storage is None:
603+
self._set_storage_obj(self._new_storage(False, True))
604+
storage = self._get_storage_obj()
605+
# build list of layers
606+
if layer is None:
607+
layer_list = []
608+
for index in range(0, storage.layer_storage.get_total_size()):
609+
if (
610+
storage.layer_storage[index].data_storage_type
611+
== DataStorageType.external_file
612+
):
613+
layer_list.append(index)
614+
else:
615+
if (
616+
storage.layer_storage[layer].data_storage_type
617+
== DataStorageType.external_file
618+
):
619+
layer_list = [layer]
620+
else:
621+
layer_list = []
622+
623+
# store data from each layer
624+
for current_layer in layer_list:
625+
if isinstance(current_layer, int):
626+
current_layer = (current_layer,)
627+
# get the layer's data
628+
data = self._get_data(current_layer, True)
629+
630+
if data is None:
631+
# do not write empty data to an internal file
632+
continue
633+
try:
634+
# store layer's data internally
635+
if (
636+
self._simulation_data.verbosity_level.value
637+
>= VerbosityLevel.verbose.value
638+
):
639+
print(
640+
"Storing {} layer {} internally.."
641+
".".format(
642+
self.structure.name,
643+
current_layer[0] + 1,
644+
)
645+
)
646+
factor = storage.layer_storage[current_layer].factor
647+
internal_data = {
648+
"data": self._get_data(current_layer, True),
649+
"factor": factor,
650+
}
651+
self._set_data(
652+
internal_data, layer=current_layer, check_data=False
653+
)
654+
except Exception as ex:
655+
type_, value_, traceback_ = sys.exc_info()
656+
raise MFDataException(
657+
self.structure.get_model(),
658+
self.structure.get_package(),
659+
self._path,
660+
"storing data {} internally".format(self.structure.name),
661+
self.structure.name,
662+
inspect.stack()[0][3],
663+
type_,
664+
value_,
665+
traceback_,
666+
None,
667+
self._simulation_data.debug,
668+
ex,
669+
)
670+
585671
def has_data(self, layer=None):
586672
"""Returns whether layer "layer_num" has any data associated with it.
587673
@@ -1481,32 +1567,57 @@ def store_as_external_file(
14811567
check_data : bool
14821568
Verify data prior to storing
14831569
"""
1484-
1485-
sim_time = self._data_dimensions.package_dim.model_dim[
1486-
0
1487-
].simulation_time
1488-
num_sp = sim_time.get_num_stress_periods()
14891570
# store each stress period in separate file(s)
1490-
for sp in range(0, num_sp):
1491-
if sp in self._data_storage:
1492-
self._current_key = sp
1493-
layer_storage = self._get_storage_obj().layer_storage
1494-
if (
1495-
layer_storage.get_total_size() > 0
1496-
and self._get_storage_obj()
1497-
.layer_storage[0]
1498-
.layer_storage_type
1499-
!= DataStorageType.external_file
1500-
):
1501-
fname, ext = os.path.splitext(external_file_path)
1571+
for sp in self._data_storage.keys():
1572+
self._current_key = sp
1573+
layer_storage = self._get_storage_obj().layer_storage
1574+
if (
1575+
layer_storage.get_total_size() > 0
1576+
and self._get_storage_obj().layer_storage[0].data_storage_type
1577+
!= DataStorageType.external_file
1578+
):
1579+
fname, ext = os.path.splitext(external_file_path)
1580+
if DatumUtil.is_int(sp):
15021581
full_name = "{}_{}{}".format(fname, sp + 1, ext)
1503-
super().store_as_external_file(
1504-
full_name,
1505-
layer,
1506-
binary,
1507-
replace_existing_external,
1508-
check_data,
1509-
)
1582+
else:
1583+
full_name = "{}_{}{}".format(fname, sp, ext)
1584+
super().store_as_external_file(
1585+
full_name,
1586+
layer,
1587+
binary,
1588+
replace_existing_external,
1589+
check_data,
1590+
)
1591+
1592+
def store_internal(
1593+
self,
1594+
layer=None,
1595+
check_data=True,
1596+
):
1597+
"""Stores data from layer `layer` internally. For unlayered data do
1598+
not pass in `layer`. If layer is not specified all layers will be
1599+
stored internally.
1600+
1601+
Parameters
1602+
----------
1603+
layer : int
1604+
Which layer to store internally file, `None` value stores all
1605+
layers.
1606+
check_data : bool
1607+
Verify data prior to storing
1608+
"""
1609+
for sp in self._data_storage.keys():
1610+
self._current_key = sp
1611+
layer_storage = self._get_storage_obj().layer_storage
1612+
if (
1613+
layer_storage.get_total_size() > 0
1614+
and self._get_storage_obj().layer_storage[0].data_storage_type
1615+
== DataStorageType.external_file
1616+
):
1617+
super().store_internal(
1618+
layer,
1619+
check_data,
1620+
)
15101621

15111622
def get_data(self, layer=None, apply_mult=True, **kwargs):
15121623
"""Returns the data associated with stress period key `layer`.

0 commit comments

Comments
 (0)