diff --git a/autotest/conftest.py b/autotest/conftest.py index 9f7f8df01f..3c84d1c5e8 100644 --- a/autotest/conftest.py +++ b/autotest/conftest.py @@ -49,9 +49,12 @@ def backtrack_or_raise(): if is_in_ci(): tries.append(2) for t in tries: - parts = cwd.parts[0: cwd.parts.index("flopy") + t] + parts = cwd.parts[0 : cwd.parts.index("flopy") + t] pth = Path(*parts) - if next(iter([p for p in pth.glob("setup.cfg")]), None) is not None: + if ( + next(iter([p for p in pth.glob("setup.cfg")]), None) + is not None + ): return pth raise Exception( f"Can't infer location of project root from {cwd} " @@ -61,13 +64,17 @@ def backtrack_or_raise(): if cwd.name == "autotest": # we're in top-level autotest folder return cwd.parent - elif "autotest" in cwd.parts and cwd.parts.index("autotest") > cwd.parts.index("flopy"): + elif "autotest" in cwd.parts and cwd.parts.index( + "autotest" + ) > cwd.parts.index("flopy"): # we're somewhere inside autotests - parts = cwd.parts[0: cwd.parts.index("autotest")] + parts = cwd.parts[0 : cwd.parts.index("autotest")] return Path(*parts) - elif "examples" in cwd.parts and cwd.parts.index("examples") > cwd.parts.index("flopy"): + elif "examples" in cwd.parts and cwd.parts.index( + "examples" + ) > cwd.parts.index("flopy"): # we're somewhere inside examples folder - parts = cwd.parts[0: cwd.parts.index("examples")] + parts = cwd.parts[0 : cwd.parts.index("examples")] return Path(*parts) elif "flopy" in cwd.parts: if cwd.parts.count("flopy") >= 2: @@ -154,7 +161,7 @@ def is_github_rate_limited() -> Optional[bool]: """ try: with request.urlopen( - "https://api.github.com/users/octocat" + "https://api.github.com/users/octocat" ) as response: remaining = int(response.headers["x-ratelimit-remaining"]) if remaining < 10: @@ -197,8 +204,8 @@ def requires_exe(*exes): missing = {exe for exe in exes if not has_exe(exe)} return pytest.mark.skipif( missing, - reason=f"missing executable{'s' if len(missing) != 1 else ''}: " + - ", ".join(missing), + reason=f"missing executable{'s' if len(missing) != 1 else ''}: " + + ", ".join(missing), ) @@ -206,21 +213,23 @@ def requires_pkg(*pkgs): missing = {pkg for pkg in pkgs if not has_pkg(pkg)} return pytest.mark.skipif( missing, - reason=f"missing package{'s' if len(missing) != 1 else ''}: " + - ", ".join(missing), + reason=f"missing package{'s' if len(missing) != 1 else ''}: " + + ", ".join(missing), ) def requires_platform(platform, ci_only=False): return pytest.mark.skipif( - system().lower() != platform.lower() and (is_in_ci() if ci_only else True), + system().lower() != platform.lower() + and (is_in_ci() if ci_only else True), reason=f"only compatible with platform: {platform.lower()}", ) def excludes_platform(platform, ci_only=False): return pytest.mark.skipif( - system().lower() == platform.lower() and (is_in_ci() if ci_only else True), + system().lower() == platform.lower() + and (is_in_ci() if ci_only else True), reason=f"not compatible with platform: {platform.lower()}", ) @@ -240,18 +249,19 @@ def excludes_branch(branch): requires_github = pytest.mark.skipif( - not is_connected("github.com"), - reason="github.com is required.") + not is_connected("github.com"), reason="github.com is required." +) requires_spatial_reference = pytest.mark.skipif( not is_connected("spatialreference.org"), - reason="spatialreference.org is required." + reason="spatialreference.org is required.", ) # example data fixtures + @pytest.fixture(scope="session") def project_root_path(request) -> Path: return get_project_root_path(request.session.path) @@ -274,12 +284,14 @@ def example_shapefiles(example_data_path) -> List[Path]: # keepable temporary directory fixtures for various scopes + @pytest.fixture(scope="function") def tmpdir(tmpdir_factory, request) -> Path: - node = request.node.name \ - .replace("/", "_") \ - .replace("\\", "_") \ + node = ( + request.node.name.replace("/", "_") + .replace("\\", "_") .replace(":", "_") + ) temp = Path(tmpdir_factory.mktemp(node)) yield Path(temp) @@ -295,7 +307,7 @@ def tmpdir(tmpdir_factory, request) -> Path: @pytest.fixture(scope="class") def class_tmpdir(tmpdir_factory, request) -> Path: assert ( - request.cls is not None + request.cls is not None ), "Class-scoped temp dir fixture must be used on class" temp = Path(tmpdir_factory.mktemp(request.cls.__name__)) yield temp @@ -327,6 +339,7 @@ def session_tmpdir(tmpdir_factory, request) -> Path: # pytest configuration hooks + @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # this is necessary so temp dir fixtures can @@ -348,9 +361,9 @@ def pytest_addoption(parser): action="store", default=None, help="Move the contents of temporary test directories to correspondingly named subdirectories at the given " - "location after tests complete. This option can be used to exclude test results from automatic cleanup, " - "e.g. for manual inspection. The provided path is created if it does not already exist. An error is " - "thrown if any matching files already exist.", + "location after tests complete. This option can be used to exclude test results from automatic cleanup, " + "e.g. for manual inspection. The provided path is created if it does not already exist. An error is " + "thrown if any matching files already exist.", ) parser.addoption( @@ -358,9 +371,9 @@ def pytest_addoption(parser): action="store", default=None, help="Move the contents of temporary test directories to correspondingly named subdirectories at the given " - "location if the test case fails. This option automatically saves the outputs of failed tests in the " - "given location. The path is created if it doesn't already exist. An error is thrown if files with the " - "same names already exist in the given location.", + "location if the test case fails. This option automatically saves the outputs of failed tests in the " + "given location. The path is created if it doesn't already exist. An error is thrown if files with the " + "same names already exist in the given location.", ) parser.addoption( @@ -376,7 +389,7 @@ def pytest_addoption(parser): "--smoke", action="store_true", default=False, - help="Run only smoke tests (should complete in <1 minute)." + help="Run only smoke tests (should complete in <1 minute).", ) @@ -444,6 +457,7 @@ def pytest_report_header(config): # functions to run commands and scripts + def run_cmd(*args, verbose=False, **kwargs): """Run any command, return tuple (stdout, stderr, returncode).""" args = [str(g) for g in args] @@ -464,7 +478,8 @@ def run_cmd(*args, verbose=False, **kwargs): def run_py_script(script, *args, verbose=False): """Run a Python script, return tuple (stdout, stderr, returncode).""" return run_cmd( - sys.executable, script, *args, verbose=verbose, cwd=Path(script).parent) + sys.executable, script, *args, verbose=verbose, cwd=Path(script).parent + ) # use noninteractive matplotlib backend if in Mac OS CI to avoid pytest-xdist node failure @@ -473,4 +488,5 @@ def run_py_script(script, *args, verbose=False): def patch_macos_ci_matplotlib(): if is_in_ci() and system().lower() == "darwin": import matplotlib + matplotlib.use("agg") diff --git a/autotest/pytest.ini b/autotest/pytest.ini index 6566da0901..1b66575e73 100644 --- a/autotest/pytest.ini +++ b/autotest/pytest.ini @@ -1,4 +1,5 @@ [pytest] +addopts = -ra python_files = test_*.py profile_*.py diff --git a/autotest/regression/conftest.py b/autotest/regression/conftest.py index 5971ede0c0..d7e149abe6 100644 --- a/autotest/regression/conftest.py +++ b/autotest/regression/conftest.py @@ -33,7 +33,7 @@ def get_mf6_examples_path() -> Path: def is_nested(namfile) -> bool: p = Path(namfile) - if not p.is_file() or not p.name.endswith('.nam'): + if not p.is_file() or not p.name.endswith(".nam"): raise ValueError(f"Expected a namfile path, got {p}") return p.parent.parent.name != __mf6_examples @@ -43,7 +43,11 @@ def pytest_generate_tests(metafunc): # examples to skip: # - ex-gwtgwt-mt3dms-p10: https://github.com/MODFLOW-USGS/modflow6/pull/1008 exclude = ["ex-gwt-gwtgwt-mt3dms-p10"] - namfiles = [str(p) for p in get_mf6_examples_path().rglob("mfsim.nam") if not any(e in str(p) for e in exclude)] + namfiles = [ + str(p) + for p in get_mf6_examples_path().rglob("mfsim.nam") + if not any(e in str(p) for e in exclude) + ] # parametrization by model # - single namfile per test case @@ -63,16 +67,27 @@ def simulation_name_from_model_path(p): p = Path(p) return p.parent.parent.name if is_nested(p) else p.parent.name - for model_name, model_namfiles in groupby(namfiles, key=simulation_name_from_model_path): - models = sorted(list(model_namfiles)) # sort in alphabetical order (gwf < gwt) + for model_name, model_namfiles in groupby( + namfiles, key=simulation_name_from_model_path + ): + models = sorted( + list(model_namfiles) + ) # sort in alphabetical order (gwf < gwt) simulations.append(models) - print(f"Simulation {model_name} has {len(models)} model(s):\n" - f"{linesep.join(model_namfiles)}") + print( + f"Simulation {model_name} has {len(models)} model(s):\n" + f"{linesep.join(model_namfiles)}" + ) def simulation_name_from_model_namfiles(mnams): namfile = next(iter(mnams), None) - if namfile is None: pytest.skip("No namfiles (expected ordered collection)") + if namfile is None: + pytest.skip("No namfiles (expected ordered collection)") namfile = Path(namfile) - return (namfile.parent.parent if is_nested(namfile) else namfile.parent).name + return ( + namfile.parent.parent if is_nested(namfile) else namfile.parent + ).name - metafunc.parametrize(key, simulations, ids=simulation_name_from_model_namfiles) + metafunc.parametrize( + key, simulations, ids=simulation_name_from_model_namfiles + ) diff --git a/autotest/regression/test_lgr.py b/autotest/regression/test_lgr.py index 346ee7cc0f..196efa6fb6 100644 --- a/autotest/regression/test_lgr.py +++ b/autotest/regression/test_lgr.py @@ -3,9 +3,9 @@ from pathlib import Path import pytest +from autotest.conftest import requires_exe, requires_pkg import flopy -from autotest.conftest import requires_exe, requires_pkg @requires_exe("mflgr") @@ -47,7 +47,7 @@ def test_simplelgr(tmpdir, example_data_path): # get the namefiles of the parent and child namefiles = lgr.get_namefiles() assert ( - len(namefiles) == 2 + len(namefiles) == 2 ), f"get_namefiles returned {len(namefiles)} items instead of 2" tpth = dirname(namefiles[0]) diff --git a/autotest/regression/test_mf6.py b/autotest/regression/test_mf6.py index 1c895b40e1..0bf95f5dff 100644 --- a/autotest/regression/test_mf6.py +++ b/autotest/regression/test_mf6.py @@ -1,18 +1,49 @@ import copy import os -import sys import shutil +import sys from pathlib import Path import numpy as np import pytest +from autotest.conftest import requires_exe, requires_pkg import flopy -from autotest.conftest import requires_exe, requires_pkg -from flopy.mf6 import MFSimulation, ModflowTdis, ModflowGwfgwf, ModflowGwfgwt, ModflowIms, ModflowGwf, ModflowGwfdis, ModflowGwfic, \ - ModflowGwfnpf, ModflowGwfoc, ModflowGwfsto, ModflowGwfsfr, ModflowGwfwel, ModflowGwfdrn, ModflowGwfriv, ModflowGwfhfb, ModflowGwfchd, \ - ModflowGwfghb, ModflowGwfrcha, ModflowUtltas, ModflowGwfevt, ModflowGwfrch, ModflowGwfdisv, ModflowGwfgnc, ModflowGwfevta, MFModel, ModflowGwtdis, \ - ModflowGwtic, ModflowGwtadv, ModflowGwtmst, ModflowGwtssm, ModflowGwtoc, ExtFileAction +from flopy.mf6 import ( + ExtFileAction, + MFModel, + MFSimulation, + ModflowGwf, + ModflowGwfchd, + ModflowGwfdis, + ModflowGwfdisv, + ModflowGwfdrn, + ModflowGwfevt, + ModflowGwfevta, + ModflowGwfghb, + ModflowGwfgnc, + ModflowGwfgwf, + ModflowGwfgwt, + ModflowGwfhfb, + ModflowGwfic, + ModflowGwfnpf, + ModflowGwfoc, + ModflowGwfrch, + ModflowGwfrcha, + ModflowGwfriv, + ModflowGwfsfr, + ModflowGwfsto, + ModflowGwfwel, + ModflowGwtadv, + ModflowGwtdis, + ModflowGwtic, + ModflowGwtmst, + ModflowGwtoc, + ModflowGwtssm, + ModflowIms, + ModflowTdis, + ModflowUtltas, +) from flopy.mf6.data.mfdatastorage import DataStorageType from flopy.mf6.mfbase import FlopyException, MFDataException from flopy.mf6.utils import testutils @@ -350,7 +381,7 @@ def test_np001(tmpdir, example_data_path): sim.set_all_data_external() sim.write_simulation() assert ( - sim.simulation_data.max_columns_of_data == dis_package.ncol.get_data() + sim.simulation_data.max_columns_of_data == dis_package.ncol.get_data() ) # test package file with relative path to simulation path wel_path = os.path.join(ws, "well_folder", f"{model_name}.wel") @@ -403,7 +434,9 @@ def test_np001(tmpdir, example_data_path): riv_path = str(tmpdir / "data" / "np001_mod.riv_stress_period_data_1.txt") assert os.path.exists(riv_path) - assert sim.simulation_data.max_columns_of_data == dis_package.ncol.get_data() + assert ( + sim.simulation_data.max_columns_of_data == dis_package.ncol.get_data() + ) # run simulation from new path with external files sim.run_simulation() @@ -462,12 +495,12 @@ def test_np001(tmpdir, example_data_path): summary = ".".join(chk[0].summary_array.desc) assert "drn_1 package: invalid BC index" in summary assert ( - "npf package: vertical hydraulic conductivity values below " - "checker threshold of 1e-11" in summary + "npf package: vertical hydraulic conductivity values below " + "checker threshold of 1e-11" in summary ) assert ( - "npf package: horizontal hydraulic conductivity values above " - "checker threshold of 100000.0" in summary + "npf package: horizontal hydraulic conductivity values above " + "checker threshold of 100000.0" in summary ) data_invalid = False try: @@ -502,10 +535,10 @@ def test_np001(tmpdir, example_data_path): for line in fd: line_lst = line.strip().split() if ( - len(line) > 2 - and line_lst[0] == "0" - and line_lst[1] == "0" - and line_lst[2] == "0" + len(line) > 2 + and line_lst[0] == "0" + and line_lst[1] == "0" + and line_lst[2] == "0" ): found_cellid = True assert found_cellid @@ -810,12 +843,12 @@ def test_np002(tmpdir, example_data_path): chk = sim.check() summary = ".".join(chk[0].summary_array.desc) assert ( - "sto package: specific storage values below " - "checker threshold of 1e-06" in summary + "sto package: specific storage values below " + "checker threshold of 1e-06" in summary ) assert ( - "sto package: specific yield values above " - "checker threshold of 0.5" in summary + "sto package: specific yield values above " + "checker threshold of 0.5" in summary ) assert "Not a number" in summary model.remove_package("chd_2") @@ -1471,10 +1504,10 @@ def test005_create_tests_advgw_tidal(tmpdir, example_data_path): col_max = 6 for col in range(0, col_max): if ( - (row == 3 and col == 5) - or (row == 2 and col == 4) - or (row == 1 and col == 3) - or (row == 0 and col == 2) + (row == 3 and col == 5) + or (row == 2 and col == 4) + or (row == 1 and col == 3) + or (row == 0 and col == 2) ): mult = 0.5 else: @@ -1578,10 +1611,10 @@ def test005_create_tests_advgw_tidal(tmpdir, example_data_path): col_min = 6 for col in range(col_min, 10): if ( - (row == 0 and col == 9) - or (row == 1 and col == 8) - or (row == 2 and col == 7) - or (row == 3 and col == 6) + (row == 0 and col == 9) + or (row == 1 and col == 8) + or (row == 2 and col == 7) + or (row == 3 and col == 6) ): mult = 0.5 else: @@ -1943,9 +1976,9 @@ def test035_create_tests_fhb(tmpdir, example_data_path): ) time = model.modeltime assert ( - time.steady_state[0] == False - and time.steady_state[1] == False - and time.steady_state[2] == False + time.steady_state[0] == False + and time.steady_state[1] == False + and time.steady_state[2] == False ) wel_period = {0: [((0, 1, 0), "flow")]} wel_package = ModflowGwfwel( @@ -2750,8 +2783,10 @@ def test050_create_tests_circle_island(tmpdir, example_data_path): @requires_exe("mf6") @requires_pkg("pymake") -@pytest.mark.xfail(reason="possible python3.7/windows incompatibilities in testutils.read_std_array " - "https://github.com/modflowpy/flopy/runs/7581629193?check_suite_focus=true#step:11:1753") +@pytest.mark.xfail( + reason="possible python3.7/windows incompatibilities in testutils.read_std_array " + "https://github.com/modflowpy/flopy/runs/7581629193?check_suite_focus=true#step:11:1753" +) @pytest.mark.regression def test028_create_tests_sfr(tmpdir, example_data_path): import pymake @@ -3514,10 +3549,10 @@ def test005_advgw_tidal(tmpdir, example_data_path): model = sim.get_model(model_name) time = model.modeltime assert ( - time.steady_state[0] == True - and time.steady_state[1] == False - and time.steady_state[2] == False - and time.steady_state[3] == False + time.steady_state[0] == True + and time.steady_state[1] == False + and time.steady_state[2] == False + and time.steady_state[3] == False ) ghb = model.get_package("ghb") obs = ghb.obs @@ -3976,14 +4011,14 @@ def test006_2models_mvr(tmpdir, example_data_path): model = sim.get_model(model_name) for package in model_package_check: assert ( - package in model.package_type_dict - or package in sim.package_type_dict - ) == (package in load_only or f"{package}6" in load_only) + package in model.package_type_dict + or package in sim.package_type_dict + ) == (package in load_only or f"{package}6" in load_only) assert (len(sim._exchange_files) > 0) == ( - "gwf6-gwf6" in load_only or "gwf-gwf" in load_only + "gwf6-gwf6" in load_only or "gwf-gwf" in load_only ) assert (len(sim._ims_files) > 0) == ( - "ims6" in load_only or "ims" in load_only + "ims6" in load_only or "ims" in load_only ) # load package by name @@ -4072,7 +4107,7 @@ def test001e_uzf_3lay(tmpdir, example_data_path): model = sim.get_model() for package in model_package_check: assert (package in model.package_type_dict) == ( - package in load_only or f"{package}6" in load_only + package in load_only or f"{package}6" in load_only ) # test running a runnable load_only case sim = MFSimulation.load( diff --git a/autotest/regression/test_mf6_examples.py b/autotest/regression/test_mf6_examples.py index 5ec55d4184..a37f8320e5 100644 --- a/autotest/regression/test_mf6_examples.py +++ b/autotest/regression/test_mf6_examples.py @@ -2,9 +2,9 @@ from shutil import copytree import pytest - from autotest.conftest import requires_exe, requires_pkg from autotest.regression.conftest import is_nested + from flopy.mf6 import MFSimulation @@ -26,19 +26,22 @@ def test_mf6_example_simulations(tmpdir, mf6_example_namfiles): import pymake # make sure we have at least 1 name file - if len(mf6_example_namfiles) == 0: pytest.skip("No namfiles (expected ordered collection)") + if len(mf6_example_namfiles) == 0: + pytest.skip("No namfiles (expected ordered collection)") namfile = Path(mf6_example_namfiles[0]) # pull the first model's namfile # coupled models have nested dirs (e.g., 'mf6gwf' and 'mf6gwt') under model directory # TODO: are there multiple types of couplings? e.g. besides GWF-GWT, mt3dms? nested = is_nested(namfile) - tmpdir = Path(tmpdir / "workspace") # working directory (must not exist for copytree) - cmpdir = tmpdir / "compare" # comparison directory + tmpdir = Path( + tmpdir / "workspace" + ) # working directory (must not exist for copytree) + cmpdir = tmpdir / "compare" # comparison directory # copy model files into working directory copytree( - src=namfile.parent.parent if nested else namfile.parent, - dst=tmpdir) + src=namfile.parent.parent if nested else namfile.parent, dst=tmpdir + ) def run_models(): # run models in order received (should be alphabetical, so gwf precedes gwt) @@ -53,10 +56,7 @@ def run_models(): # load simulation sim = MFSimulation.load( - namfile_name, - version="mf6", - exe_name="mf6", - sim_ws=str(wrkdir) + namfile_name, version="mf6", exe_name="mf6", sim_ws=str(wrkdir) ) assert isinstance(sim, MFSimulation) @@ -73,8 +73,8 @@ def run_models(): assert success # get head file outputs - headfiles1 = [p for p in wrkdir.glob('*.hds')] - headfiles2 = [p for p in cmpdir.glob('*.hds')] + headfiles1 = [p for p in wrkdir.glob("*.hds")] + headfiles2 = [p for p in cmpdir.glob("*.hds")] # compare heads assert pymake.compare_heads( @@ -84,6 +84,7 @@ def run_models(): text="head", files1=[str(p) for p in headfiles1], files2=[str(p) for p in headfiles2], - outfile=str(cmpdir / "head_compare.dat")) + outfile=str(cmpdir / "head_compare.dat"), + ) run_models() diff --git a/autotest/regression/test_mfnwt.py b/autotest/regression/test_mfnwt.py index fb16da80f4..7680770542 100644 --- a/autotest/regression/test_mfnwt.py +++ b/autotest/regression/test_mfnwt.py @@ -1,9 +1,9 @@ import os import pytest - from autotest.conftest import get_example_data_path, requires_exe, requires_pkg -from flopy.modflow import Modflow, ModflowUpw, ModflowNwt + +from flopy.modflow import Modflow, ModflowNwt, ModflowUpw from flopy.utils import parsenamefile @@ -12,7 +12,7 @@ def get_nfnwt_namfiles(): nwtpth = get_example_data_path(__file__) / "mf2005_test" namfiles = [] m = Modflow("test", version="mfnwt") - for namfile in nwtpth.rglob('*.nam'): + for namfile in nwtpth.rglob("*.nam"): nf = parsenamefile(namfile, m.mfnam_packages) lpf = False wel = False @@ -135,7 +135,9 @@ def test_run_mfnwt_model(tmpdir, namfile): fn1 = os.path.join(pthf, namfile) fsum = str(tmpdir / f"{base_name}.head.out") - assert pymake.compare_heads(fn0, fn1, outfile=fsum), "head comparison failure" + assert pymake.compare_heads( + fn0, fn1, outfile=fsum + ), "head comparison failure" fsum = str(tmpdir / f"{base_name}.budget.out") assert pymake.compare_budget( diff --git a/autotest/regression/test_modflow.py b/autotest/regression/test_modflow.py index d02e928f8f..1418e504c4 100644 --- a/autotest/regression/test_modflow.py +++ b/autotest/regression/test_modflow.py @@ -1,11 +1,11 @@ +import filecmp from os.path import join, splitext from pathlib import Path from shutil import copytree -import filecmp import pytest +from autotest.conftest import get_example_data_path, requires_exe, requires_pkg -from autotest.conftest import requires_exe, requires_pkg, get_example_data_path from flopy.modflow import Modflow, ModflowOc @@ -74,9 +74,7 @@ def test_uzf_unit_numbers(tmpdir, uzf_example_path): fn1 = join(model_ws2, mfnam) # compare budget terms - fsum = join( - str(tmpdir), f"{splitext(mfnam)[0]}.budget.out" - ) + fsum = join(str(tmpdir), f"{splitext(mfnam)[0]}.budget.out") success = pymake.compare_budget( fn0, fn1, max_incpd=0.1, max_cumpd=0.1, outfile=fsum ) @@ -176,10 +174,13 @@ def test_gage(tmpdir, example_data_path): @requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression -@pytest.mark.parametrize("namfile", [ - str(__example_data_path / "pcgn_test" / nf) - for nf in ["twri.nam", "MNW2.nam"] -]) +@pytest.mark.parametrize( + "namfile", + [ + str(__example_data_path / "pcgn_test" / nf) + for nf in ["twri.nam", "MNW2.nam"] + ], +) def test_mf2005pcgn(tmpdir, namfile): import pymake @@ -227,7 +228,9 @@ def test_mf2005pcgn(tmpdir, namfile): @requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression -@pytest.mark.parametrize("namfile", [str(__example_data_path / "secp" / nf) for nf in ["secp.nam"]]) +@pytest.mark.parametrize( + "namfile", [str(__example_data_path / "secp" / nf) for nf in ["secp.nam"]] +) def test_mf2005gmg(tmpdir, namfile): import pymake @@ -269,7 +272,10 @@ def test_mf2005gmg(tmpdir, namfile): @requires_exe("mf2005") @requires_pkg("pymake") @pytest.mark.regression -@pytest.mark.parametrize("namfile", [str(__example_data_path / "freyberg" / nf) for nf in ["freyberg.nam"]]) +@pytest.mark.parametrize( + "namfile", + [str(__example_data_path / "freyberg" / nf) for nf in ["freyberg.nam"]], +) def test_mf2005(tmpdir, namfile): """ test045 load and write of MODFLOW-2005 GMG example problem @@ -334,13 +340,16 @@ def test_mf2005(tmpdir, namfile): assert success, "budget comparison failure" -mf2005_namfiles = [str(__example_data_path / "mf2005_test" / nf) for nf in [ - "fhb.nam", - "l1a2k.nam", - "l1b2k.nam", - "l1b2k_bath.nam", - "lakeex3.nam", -]] +mf2005_namfiles = [ + str(__example_data_path / "mf2005_test" / nf) + for nf in [ + "fhb.nam", + "l1a2k.nam", + "l1b2k.nam", + "l1b2k_bath.nam", + "lakeex3.nam", + ] +] @requires_exe("mf2005") @@ -354,7 +363,9 @@ def test_mf2005fhb(tmpdir, namfile): ws = str(tmpdir / "ws") copytree(Path(namfile).parent, ws) - m = Modflow.load(Path(namfile).name, model_ws=ws, verbose=True, exe_name="mf2005") + m = Modflow.load( + Path(namfile).name, model_ws=ws, verbose=True, exe_name="mf2005" + ) assert m.load_fail is False success, buff = m.run_model(silent=False) diff --git a/autotest/regression/test_str.py b/autotest/regression/test_str.py index a98816d2e3..6cebfbb0a5 100644 --- a/autotest/regression/test_str.py +++ b/autotest/regression/test_str.py @@ -1,7 +1,7 @@ import pytest - from autotest.conftest import requires_exe, requires_pkg -from flopy.modflow import Modflow, ModflowStr, ModflowOc + +from flopy.modflow import Modflow, ModflowOc, ModflowStr str_items = { 0: { @@ -91,7 +91,7 @@ def test_str_fixed_free(tmpdir, example_data_path): m2 = None assert ( - m2 is not None + m2 is not None ), "could not load the fixed format model with aux variables" for p in tmpdir.glob("*"): @@ -117,10 +117,12 @@ def test_str_fixed_free(tmpdir, example_data_path): m2 = None assert ( - m2 is not None + m2 is not None ), "could not load the free format model with aux variables" # compare the fixed and free format head files fn1 = str(tmpdir / "str.nam") fn2 = str(tmpdir / "str.nam") - assert pymake.compare_heads(fn1, fn2, verbose=True), "fixed and free format input output head files are different" + assert pymake.compare_heads( + fn1, fn2, verbose=True + ), "fixed and free format input output head files are different" diff --git a/autotest/regression/test_swi2.py b/autotest/regression/test_swi2.py index a8e78c5f1a..4f738bd5e5 100644 --- a/autotest/regression/test_swi2.py +++ b/autotest/regression/test_swi2.py @@ -2,8 +2,8 @@ import shutil import pytest - from autotest.conftest import requires_exe, requires_pkg + from flopy.modflow import Modflow @@ -16,7 +16,9 @@ def swi_path(example_data_path): @requires_pkg("pymake") @pytest.mark.slow @pytest.mark.regression -@pytest.mark.parametrize("namfile", ["swiex1.nam", "swiex2_strat.nam", "swiex3.nam"]) +@pytest.mark.parametrize( + "namfile", ["swiex1.nam", "swiex2_strat.nam", "swiex3.nam"] +) def test_mf2005swi2(tmpdir, swi_path, namfile): import pymake diff --git a/autotest/regression/test_wel.py b/autotest/regression/test_wel.py index 7f30f02ef4..ea3c917233 100644 --- a/autotest/regression/test_wel.py +++ b/autotest/regression/test_wel.py @@ -2,8 +2,8 @@ import numpy as np import pytest - from autotest.conftest import requires_exe, requires_pkg + from flopy.modflow import ( Modflow, ModflowBas, @@ -100,7 +100,9 @@ def test_binary_well(tmpdir): # compare the files fsum = os.path.join(str(tmpdir), f"{os.path.splitext(mfnam)[0]}.head.out") - assert pymake.compare_heads(fn0, fn1, outfile=fsum), "head comparison failure" + assert pymake.compare_heads( + fn0, fn1, outfile=fsum + ), "head comparison failure" fsum = os.path.join( str(tmpdir), f"{os.path.splitext(mfnam)[0]}.budget.out" diff --git a/autotest/test_conftest.py b/autotest/test_conftest.py index b13b6ac2b9..03aa98ccd5 100644 --- a/autotest/test_conftest.py +++ b/autotest/test_conftest.py @@ -1,17 +1,23 @@ -import os import inspect +import os import platform from pathlib import Path from shutil import which import pytest from _pytest.config import ExitCode - -from autotest.conftest import get_project_root_path, get_example_data_path, requires_exe, requires_pkg, requires_platform, excludes_platform - +from autotest.conftest import ( + excludes_platform, + get_example_data_path, + get_project_root_path, + requires_exe, + requires_pkg, + requires_platform, +) # temporary directory fixtures + def test_tmpdirs(tmpdir, module_tmpdir): # function-scoped temporary directory assert isinstance(tmpdir, Path) @@ -36,10 +42,13 @@ def test_function_scoped_tmpdir_slash_in_name(tmpdir, name): # node name might have slashes if test function is parametrized # (e.g., test_function_scoped_tmpdir_slash_in_name[a/slash]) - replaced1 = name.replace('/', '_').replace("\\", '_').replace(':', '_') - replaced2 = name.replace('/', '_').replace("\\", '__').replace(':', '_') - assert f"{inspect.currentframe().f_code.co_name}[{replaced1}]" in tmpdir.stem or \ - f"{inspect.currentframe().f_code.co_name}[{replaced2}]" in tmpdir.stem + replaced1 = name.replace("/", "_").replace("\\", "_").replace(":", "_") + replaced2 = name.replace("/", "_").replace("\\", "__").replace(":", "_") + assert ( + f"{inspect.currentframe().f_code.co_name}[{replaced1}]" in tmpdir.stem + or f"{inspect.currentframe().f_code.co_name}[{replaced2}]" + in tmpdir.stem + ) class TestClassScopedTmpdir: @@ -70,6 +79,7 @@ def test_session_scoped_tmpdir(session_tmpdir): # misc utilities + def test_get_project_root_path_from_autotest(): cwd = Path(__file__).parent root = get_project_root_path(cwd) @@ -77,8 +87,12 @@ def test_get_project_root_path_from_autotest(): assert root.is_dir() assert root.name == "flopy" - contents = [p.name for p in root.glob('*')] - assert "autotest" in contents and "examples" in contents and "README.md" in contents + contents = [p.name for p in root.glob("*")] + assert ( + "autotest" in contents + and "examples" in contents + and "README.md" in contents + ) def test_get_project_root_path_from_project_root(): @@ -88,8 +102,12 @@ def test_get_project_root_path_from_project_root(): assert root.is_dir() assert root.name == "flopy" - contents = [p.name for p in root.glob('*')] - assert "autotest" in contents and "examples" in contents and "README.md" in contents + contents = [p.name for p in root.glob("*")] + assert ( + "autotest" in contents + and "examples" in contents + and "README.md" in contents + ) @pytest.mark.parametrize("relative_path", ["", "utils", "mf6/utils"]) @@ -100,8 +118,12 @@ def test_get_project_root_path_from_within_flopy_module(relative_path): assert root.is_dir() assert root.name == "flopy" - contents = [p.name for p in root.glob('*')] - assert "autotest" in contents and "examples" in contents and "README.md" in contents + contents = [p.name for p in root.glob("*")] + assert ( + "autotest" in contents + and "examples" in contents + and "README.md" in contents + ) def test_get_paths(): @@ -114,13 +136,16 @@ def test_get_paths(): @pytest.mark.parametrize("current_path", [__file__, None]) def test_get_example_data_path(current_path): parts = get_example_data_path(current_path).parts - assert (parts[-3] == "flopy" and - parts[-2] == "examples" and - parts[-1] == "data") + assert ( + parts[-3] == "flopy" + and parts[-2] == "examples" + and parts[-1] == "data" + ) # requiring/excluding executables & platforms + @requires_exe("mf6") def test_mf6(): assert which("mf6") @@ -128,6 +153,7 @@ def test_mf6(): exes = ["mfusg", "mfnwt"] + @requires_exe(*exes) def test_mfusg_and_mfnwt(): assert all(which(exe) for exe in exes) @@ -136,13 +162,15 @@ def test_mfusg_and_mfnwt(): @requires_pkg("numpy") def test_numpy(): import numpy + assert numpy is not None @requires_pkg("numpy", "matplotlib") def test_numpy_and_matplotlib(): - import numpy import matplotlib + import numpy + assert numpy is not None and matplotlib is not None @@ -159,6 +187,7 @@ def test_breaks_osx_ci(): # meta-test marker and CLI argument --meta (-M) + @pytest.mark.meta("test_meta") def test_meta_inner(): pass @@ -178,15 +207,21 @@ def pytest_terminal_summary(self, terminalreporter): def test_meta(): - args = [f"{__file__}", "-v", "-s", - "-k", test_meta_inner.__name__, - "-M", "test_meta"] + args = [ + f"{__file__}", + "-v", + "-s", + "-k", + test_meta_inner.__name__, + "-M", + "test_meta", + ] assert pytest.main(args, plugins=[TestMeta()]) == ExitCode.OK # CLI arguments --keep (-K) and --keep-failed -FILE_NAME = 'hello.txt' +FILE_NAME = "hello.txt" @pytest.mark.meta("test_keep") @@ -217,42 +252,75 @@ def test_keep_session_scoped_tmpdir_inner(session_tmpdir): @pytest.mark.parametrize("arg", ["--keep", "-K"]) def test_keep_function_scoped_tmpdir(tmpdir, arg): inner_fn = test_keep_function_scoped_tmpdir_inner.__name__ - args = [__file__, "-v", "-s", - "-k", inner_fn, - "-M", "test_keep", - "-K", tmpdir] + args = [ + __file__, + "-v", + "-s", + "-k", + inner_fn, + "-M", + "test_keep", + "-K", + tmpdir, + ] assert pytest.main(args) == ExitCode.OK assert Path(tmpdir / f"{inner_fn}0" / FILE_NAME).is_file() @pytest.mark.parametrize("arg", ["--keep", "-K"]) def test_keep_class_scoped_tmpdir(tmpdir, arg): - args = [__file__, "-v", "-s", - "-k", TestKeepClassScopedTmpdirInner.test_keep_class_scoped_tmpdir_inner.__name__, - "-M", "test_keep", - "-K", tmpdir] + args = [ + __file__, + "-v", + "-s", + "-k", + TestKeepClassScopedTmpdirInner.test_keep_class_scoped_tmpdir_inner.__name__, + "-M", + "test_keep", + "-K", + tmpdir, + ] assert pytest.main(args) == ExitCode.OK - assert Path(tmpdir / f"{TestKeepClassScopedTmpdirInner.__name__}0" / FILE_NAME).is_file() + assert Path( + tmpdir / f"{TestKeepClassScopedTmpdirInner.__name__}0" / FILE_NAME + ).is_file() @pytest.mark.parametrize("arg", ["--keep", "-K"]) def test_keep_module_scoped_tmpdir(tmpdir, arg): - args = [__file__, "-v", "-s", - "-k", test_keep_module_scoped_tmpdir_inner.__name__, - "-M", "test_keep", - "-K", tmpdir] + args = [ + __file__, + "-v", + "-s", + "-k", + test_keep_module_scoped_tmpdir_inner.__name__, + "-M", + "test_keep", + "-K", + tmpdir, + ] assert pytest.main(args) == ExitCode.OK this_file_path = Path(__file__) - this_test_dir = (tmpdir / f"{str(this_file_path.parent.name)}.{str(this_file_path.stem)}0") - assert FILE_NAME in [f.name for f in this_test_dir.glob('*')] + this_test_dir = ( + tmpdir + / f"{str(this_file_path.parent.name)}.{str(this_file_path.stem)}0" + ) + assert FILE_NAME in [f.name for f in this_test_dir.glob("*")] @pytest.mark.parametrize("arg", ["--keep", "-K"]) def test_keep_session_scoped_tmpdir(tmpdir, arg, request): - args = [__file__, "-v", "-s", - "-k", test_keep_session_scoped_tmpdir_inner.__name__, - "-M", "test_keep", - "-K", tmpdir] + args = [ + __file__, + "-v", + "-s", + "-k", + test_keep_session_scoped_tmpdir_inner.__name__, + "-M", + "test_keep", + "-K", + tmpdir, + ] assert pytest.main(args) == ExitCode.OK assert Path(tmpdir / f"{request.session.name}0" / FILE_NAME).is_file() @@ -268,10 +336,9 @@ def test_keep_failed_function_scoped_tmpdir_inner(tmpdir): @pytest.mark.parametrize("keep", [True, False]) def test_keep_failed_function_scoped_tmpdir(tmpdir, keep): inner_fn = test_keep_failed_function_scoped_tmpdir_inner.__name__ - args = [__file__, "-v", "-s", - "-k", inner_fn, - "-M", "test_keep_failed"] - if keep: args += ["--keep-failed", tmpdir] + args = [__file__, "-v", "-s", "-k", inner_fn, "-M", "test_keep_failed"] + if keep: + args += ["--keep-failed", tmpdir] assert pytest.main(args) == ExitCode.TESTS_FAILED kept_file = Path(tmpdir / f"{inner_fn}0" / FILE_NAME).is_file() diff --git a/autotest/test_datautil.py b/autotest/test_datautil.py index 53844f67a8..2f789b86bc 100644 --- a/autotest/test_datautil.py +++ b/autotest/test_datautil.py @@ -1,6 +1,3 @@ - - - def test_split_data_line(): # TODO: try to reproduce this: https://github.com/modflowpy/flopy/runs/7581629193?check_suite_focus=true#step:11:1753 - pass \ No newline at end of file + pass diff --git a/autotest/test_dis_cases.py b/autotest/test_dis_cases.py new file mode 100644 index 0000000000..ae6ba62e96 --- /dev/null +++ b/autotest/test_dis_cases.py @@ -0,0 +1,89 @@ +import numpy as np + +from flopy.mf6 import MFSimulation, ModflowGwf, ModflowGwfdis, ModflowGwfdisv + + +def case_dis(): + sim = MFSimulation() + gwf = ModflowGwf(sim) + dis = ModflowGwfdis( + gwf, + nlay=3, + nrow=21, + ncol=20, + delr=500.0, + delc=500.0, + top=400.0, + botm=[220.0, 200.0, 0.0], + xorigin=3000, + yorigin=1000, + angrot=10, + ) + + return gwf + + +def case_disv(): + sim = MFSimulation() + gwf = ModflowGwf(sim) + + nrow, ncol = 21, 20 + delr, delc = 500.0, 500.0 + ncpl = nrow * ncol + xv = np.linspace(0, delr * ncol, ncol + 1) + yv = np.linspace(delc * nrow, 0, nrow + 1) + xv, yv = np.meshgrid(xv, yv) + xv = xv.ravel() + yv = yv.ravel() + + def get_vlist(i, j, nrow, ncol): + v1 = i * (ncol + 1) + j + v2 = v1 + 1 + v3 = v2 + ncol + 1 + v4 = v3 - 1 + return [v1, v2, v3, v4] + + iverts = [] + for i in range(nrow): + for j in range(ncol): + iverts.append(get_vlist(i, j, nrow, ncol)) + + nvert = xv.shape[0] + verts = np.hstack((xv.reshape(nvert, 1), yv.reshape(nvert, 1))) + + cellxy = np.empty((nvert, 2)) + for icpl in range(ncpl): + iv = iverts[icpl] + cellxy[icpl, 0] = (xv[iv[0]] + xv[iv[1]]) / 2.0 + cellxy[icpl, 1] = (yv[iv[1]] + yv[iv[2]]) / 2.0 + + # need to create cell2d, which is [[icpl, xc, yc, nv, iv1, iv2, iv3, iv4]] + cell2d = [ + [icpl, cellxy[icpl, 0], cellxy[icpl, 1], 4] + iverts[icpl] + for icpl in range(ncpl) + ] + vertices = [ + [ivert, verts[ivert, 0], verts[ivert, 1]] for ivert in range(nvert) + ] + xorigin = 3000 + yorigin = 1000 + angrot = 10 + ModflowGwfdisv( + gwf, + nlay=3, + ncpl=ncpl, + top=400.0, + botm=[220.0, 200.0, 0.0], + nvert=nvert, + vertices=vertices, + cell2d=cell2d, + xorigin=xorigin, + yorigin=yorigin, + angrot=angrot, + ) + gwf.modelgrid.set_coord_info(xoff=xorigin, yoff=yorigin, angrot=angrot) + return gwf + + +def case_disu(): + pass diff --git a/autotest/test_example_notebooks.py b/autotest/test_example_notebooks.py index 56aa2633d7..188f0efbb6 100644 --- a/autotest/test_example_notebooks.py +++ b/autotest/test_example_notebooks.py @@ -1,21 +1,31 @@ import re import pytest - from autotest.conftest import get_project_root_path, run_cmd def get_example_notebooks(exclude=None): prjroot = get_project_root_path(__file__) nbpaths = [str(p) for p in (prjroot / "examples" / "FAQ").glob("*.ipynb")] - nbpaths += [str(p) for p in (prjroot / "examples" / "Notebooks").glob("*.ipynb")] - nbpaths += [str(p) for p in (prjroot / "examples" / "groundwater_paper" / "Notebooks").glob("*.ipynb")] - return sorted([p for p in nbpaths if not exclude or not any(e in p for e in exclude)]) + nbpaths += [ + str(p) for p in (prjroot / "examples" / "Notebooks").glob("*.ipynb") + ] + nbpaths += [ + str(p) + for p in ( + prjroot / "examples" / "groundwater_paper" / "Notebooks" + ).glob("*.ipynb") + ] + return sorted( + [p for p in nbpaths if not exclude or not any(e in p for e in exclude)] + ) @pytest.mark.slow @pytest.mark.example -@pytest.mark.parametrize("notebook", get_example_notebooks(exclude=["mf6_lgr"])) # TODO: figure out why this one fails +@pytest.mark.parametrize( + "notebook", get_example_notebooks(exclude=["mf6_lgr"]) +) # TODO: figure out why this one fails def test_notebooks(notebook): args = ["jupytext", "--from", "ipynb", "--execute", notebook] stdout, stderr, returncode = run_cmd(*args, verbose=True) diff --git a/autotest/test_example_scripts.py b/autotest/test_example_scripts.py index dc665e831a..b00771b14f 100644 --- a/autotest/test_example_scripts.py +++ b/autotest/test_example_scripts.py @@ -3,7 +3,6 @@ from os import linesep import pytest - from autotest.conftest import get_project_root_path, run_py_script @@ -11,11 +10,23 @@ def get_example_scripts(exclude=None): prjroot = get_project_root_path(__file__) # sort to appease pytest-xdist: all workers must collect identically ordered sets of tests - return sorted(reduce(lambda a, b: a + b, - [[str(p) for p in d.rglob('*.py') if (p.name not in exclude if exclude else True)] for d in [ - prjroot / "examples" / "scripts", - prjroot / "examples" / "Tutorials"]], - [])) + return sorted( + reduce( + lambda a, b: a + b, + [ + [ + str(p) + for p in d.rglob("*.py") + if (p.name not in exclude if exclude else True) + ] + for d in [ + prjroot / "examples" / "scripts", + prjroot / "examples" / "Tutorials", + ] + ], + [], + ) + ) @pytest.mark.slow @@ -31,12 +42,14 @@ def test_scripts(script): assert returncode == 0 - allowed_patterns = [ - "findfont", - "warning", - "loose" - ] - - assert (not stderr or - # trap warnings & non-fatal errors - all((not line or any(p in line.lower() for p in allowed_patterns)) for line in stderr.split(linesep))) + allowed_patterns = ["findfont", "warning", "loose"] + + assert ( + not stderr + or + # trap warnings & non-fatal errors + all( + (not line or any(p in line.lower() for p in allowed_patterns)) + for line in stderr.split(linesep) + ) + ) diff --git a/autotest/test_export.py b/autotest/test_export.py index 026412f6a9..415ab0c969 100644 --- a/autotest/test_export.py +++ b/autotest/test_export.py @@ -1,21 +1,22 @@ +import math import os import shutil from pathlib import Path from typing import List -import math import matplotlib.pyplot as plt import numpy as np import pytest - -import flopy from autotest.conftest import ( SHAPEFILE_EXTENSIONS, get_example_data_path, has_pkg, requires_exe, - requires_pkg, requires_spatial_reference, + requires_pkg, + requires_spatial_reference, ) + +import flopy from flopy.discretization import StructuredGrid, UnstructuredGrid from flopy.export import NetCdf from flopy.export.shapefile_utils import ( @@ -274,8 +275,8 @@ def test_export_shapefile_polygon_closed(tmpdir): @requires_pkg("rasterio", "shapefile", "scipy") def test_export_array(tmpdir, example_data_path): - from scipy.ndimage import rotate import rasterio + from scipy.ndimage import rotate namfile = "freyberg.nam" model_ws = example_data_path / "freyberg" @@ -896,7 +897,7 @@ def test_polygon_from_ij_with_epsg(tmpdir): # 502s are also possible and possibly unavoidable) ep = EpsgReference() prj = ep.to_dict() - + assert 26715 in prj fpth = os.path.join(ws, "test.prj") diff --git a/autotest/test_generate_classes.py b/autotest/test_generate_classes.py index a0af071a69..b2fefb292f 100644 --- a/autotest/test_generate_classes.py +++ b/autotest/test_generate_classes.py @@ -1,10 +1,12 @@ import pytest - from autotest.conftest import excludes_branch + from flopy.mf6.utils import generate_classes -@pytest.mark.skip(reason="TODO: use external copy of the repo, otherwise files are rewritten") +@pytest.mark.skip( + reason="TODO: use external copy of the repo, otherwise files are rewritten" +) @excludes_branch("master") def test_generate_classes_from_dfn(): # maybe compute hashes of files before/after diff --git a/autotest/test_grid.py b/autotest/test_grid.py index bdd2000281..5687ff3519 100644 --- a/autotest/test_grid.py +++ b/autotest/test_grid.py @@ -1,15 +1,18 @@ import os +from warnings import warn import matplotlib import numpy as np import pytest +from autotest.conftest import requires_pkg +from autotest.test_dis_cases import case_dis, case_disv +from autotest.test_grid_cases import GridCases from flaky import flaky from matplotlib import pyplot as plt - -from autotest.conftest import requires_pkg +from pytest_cases import parametrize_with_cases from flopy.discretization import StructuredGrid, UnstructuredGrid, VertexGrid -from flopy.mf6 import MFSimulation, ModflowGwf, ModflowGwfdis, ModflowGwfdisv +from flopy.mf6 import MFSimulation from flopy.modflow import Modflow, ModflowDis from flopy.utils.cvfdutil import gridlist_to_disv_gridprops, to_cvfd from flopy.utils.triangle import Triangle @@ -149,90 +152,6 @@ def test_get_rc_from_node_coordinates(): assert c == j, f"col {c} not equal {j} for xy ({x}, {y})" -@pytest.fixture -def dis_model(): - sim = MFSimulation() - gwf = ModflowGwf(sim) - dis = ModflowGwfdis( - gwf, - nlay=3, - nrow=21, - ncol=20, - delr=500.0, - delc=500.0, - top=400.0, - botm=[220.0, 200.0, 0.0], - xorigin=3000, - yorigin=1000, - angrot=10, - ) - - return gwf - - -@pytest.fixture -def disv_model(): - sim = MFSimulation() - gwf = ModflowGwf(sim) - - nrow, ncol = 21, 20 - delr, delc = 500.0, 500.0 - ncpl = nrow * ncol - xv = np.linspace(0, delr * ncol, ncol + 1) - yv = np.linspace(delc * nrow, 0, nrow + 1) - xv, yv = np.meshgrid(xv, yv) - xv = xv.ravel() - yv = yv.ravel() - - def get_vlist(i, j, nrow, ncol): - v1 = i * (ncol + 1) + j - v2 = v1 + 1 - v3 = v2 + ncol + 1 - v4 = v3 - 1 - return [v1, v2, v3, v4] - - iverts = [] - for i in range(nrow): - for j in range(ncol): - iverts.append(get_vlist(i, j, nrow, ncol)) - - nvert = xv.shape[0] - verts = np.hstack((xv.reshape(nvert, 1), yv.reshape(nvert, 1))) - - cellxy = np.empty((nvert, 2)) - for icpl in range(ncpl): - iv = iverts[icpl] - cellxy[icpl, 0] = (xv[iv[0]] + xv[iv[1]]) / 2.0 - cellxy[icpl, 1] = (yv[iv[1]] + yv[iv[2]]) / 2.0 - - # need to create cell2d, which is [[icpl, xc, yc, nv, iv1, iv2, iv3, iv4]] - cell2d = [ - [icpl, cellxy[icpl, 0], cellxy[icpl, 1], 4] + iverts[icpl] - for icpl in range(ncpl) - ] - vertices = [ - [ivert, verts[ivert, 0], verts[ivert, 1]] for ivert in range(nvert) - ] - xorigin = 3000 - yorigin = 1000 - angrot = 10 - ModflowGwfdisv( - gwf, - nlay=3, - ncpl=ncpl, - top=400.0, - botm=[220.0, 200.0, 0.0], - nvert=nvert, - vertices=vertices, - cell2d=cell2d, - xorigin=xorigin, - yorigin=yorigin, - angrot=angrot, - ) - gwf.modelgrid.set_coord_info(xoff=xorigin, yoff=yorigin, angrot=angrot) - return gwf - - def load_verts(fname): verts = np.genfromtxt( fname, dtype=[int, float, float], names=["iv", "x", "y"] @@ -254,8 +173,17 @@ def load_iverts(fname): return iverts, np.array(xc), np.array(yc) -def test_intersection(dis_model, disv_model): +@pytest.fixture +def dis_model(): + return case_dis() + + +@pytest.fixture +def disv_model(): + return case_disv() + +def test_intersection(dis_model, disv_model): for i in range(5): if i == 0: # inside a cell, in real-world coordinates @@ -803,520 +731,182 @@ def test_voronoi_vertex_grid(tmpdir): assert len(gridprops["cell2d"]) == 43 -def __voronoi_grid_0(): - name = "vor0" - ncpl = 3803 - domain = [ - [1831.381546, 6335.543757], - [4337.733475, 6851.136153], - [6428.747084, 6707.916043], - [8662.980804, 6493.085878], - [9350.437333, 5891.561415], - [9235.861245, 4717.156511], - [8963.743036, 3685.971717], - [8691.624826, 2783.685023], - [8047.13433, 2038.94045], - [7416.965845, 578.0953252], - [6414.425073, 105.4689614], - [5354.596258, 205.7230386], - [4624.173696, 363.2651598], - [3363.836725, 563.7733141], - [1330.11116, 1809.788273], - [399.1804436, 2998.515188], - [914.7728404, 5132.494831], - # [1831.381546, 6335.543757], - ] - area_max = 100.0**2 - poly = np.array(domain) - angle = 30 - return name, poly, area_max, ncpl, angle - - -@pytest.fixture -def voronoi_grid_0(): - return __voronoi_grid_0() - - -def __voronoi_grid_1(): - name = "vor1" - ncpl = 1679 - xmin = 0.0 - xmax = 2.0 - ymin = 0.0 - ymax = 1.0 - area_max = 0.001 - poly = np.array(((xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax))) - angle = 30 - return name, poly, area_max, ncpl, angle - - -@pytest.fixture -def voronoi_grid_1(): - return __voronoi_grid_1() - - -def __voronoi_grid_2(): - name = "vor2" - ncpl = 538 - theta = np.arange(0.0, 2 * np.pi, 0.2) - radius = 100.0 - x = radius * np.cos(theta) - y = radius * np.sin(theta) - circle_poly = [(x, y) for x, y in zip(x, y)] - max_area = 50 - angle = 30 - return name, circle_poly, max_area, ncpl, angle - - -@pytest.fixture -def voronoi_grid_2(): - return __voronoi_grid_2() - - -@requires_pkg("shapely", "scipy") @flaky -@pytest.mark.parametrize( - "grid_info", [__voronoi_grid_0(), __voronoi_grid_1(), __voronoi_grid_2()] -) -def test_voronoi_grid(tmpdir, grid_info): - name, poly, area_max, ncpl, angle = grid_info - tri = Triangle(maximum_area=area_max, angle=angle, model_ws=str(tmpdir)) - tri.add_polygon(poly) - tri.build(verbose=False) - vor = VoronoiGrid(tri) - - gridprops = vor.get_gridprops_vertexgrid() - voronoi_grid = VertexGrid(**gridprops, nlay=1) - - fig = plt.figure(figsize=(10, 10)) - ax = fig.add_subplot() - ax.set_aspect("equal") - voronoi_grid.plot(ax=ax) - plt.savefig(os.path.join(str(tmpdir), f"{name}.png")) +@requires_pkg("shapely", "scipy") +@parametrize_with_cases("grid_info", cases=GridCases, prefix="voronoi") +def test_voronoi_grid(request, tmpdir, grid_info): + name = ( + request.node.name.replace("/", "_") + .replace("\\", "_") + .replace(":", "_") + ) + ncpl, vor, gridprops, grid = grid_info - # TODO: why does this sometimes happen on CI + # TODO: debug off-by-3 issue # could be a rounding error as described here: # https://github.com/modflowpy/flopy/issues/1492#issuecomment-1210596349 # ensure proper number of cells - almost_right = (ncpl == 538 and gridprops["ncpl"] == 535) - assert ncpl == gridprops["ncpl"] or almost_right - - # ensure that all cells have 3 or more points - ninvalid_cells = [] - for icell, ivts in enumerate(vor.iverts): - if len(ivts) < 3: - ninvalid_cells.append(icell) - errmsg = f"The following cells do not have 3 or more vertices.\n{ninvalid_cells}" - assert len(ninvalid_cells) == 0, errmsg - - -@requires_pkg("shapely", "scipy") -@flaky -def test_voronoi_grid3(tmpdir): - name = "vor3" - answer_ncpl = 300 - - theta = np.arange(0.0, 2 * np.pi, 0.2) - radius = 100.0 - x = radius * np.cos(theta) - y = radius * np.sin(theta) - circle_poly = [(x, y) for x, y in zip(x, y)] - - theta = np.arange(0.0, 2 * np.pi, 0.2) - radius = 30.0 - x = radius * np.cos(theta) + 25.0 - y = radius * np.sin(theta) + 25.0 - inner_circle_poly = [(x, y) for x, y in zip(x, y)] - - tri = Triangle(maximum_area=100, angle=30, model_ws=str(tmpdir)) - tri.add_polygon(circle_poly) - tri.add_polygon(inner_circle_poly) - tri.add_hole((25, 25)) - tri.build(verbose=False) - - vor = VoronoiGrid(tri) - gridprops = vor.get_gridprops_vertexgrid() - voronoi_grid = VertexGrid(**gridprops, nlay=1) - - import matplotlib.pyplot as plt - - fig = plt.figure(figsize=(10, 10)) - ax = fig.add_subplot() - ax.set_aspect("equal") - voronoi_grid.plot(ax=ax) - plt.savefig(os.path.join(str(tmpdir), f"{name}.png")) - - # ensure proper number of cells - ncpl = gridprops["ncpl"] - errmsg = f"Number of cells should be {answer_ncpl}. Found {ncpl}" - assert ncpl == answer_ncpl, errmsg + almost_right = ncpl == 538 and gridprops["ncpl"] == 535 + if almost_right: + warn(f"off-by-3") # ensure that all cells have 3 or more points - ninvalid_cells = [] - for icell, ivts in enumerate(vor.iverts): - if len(ivts) < 3: - ninvalid_cells.append(icell) - errmsg = f"The following cells do not have 3 or more vertices.\n{ninvalid_cells}" - assert len(ninvalid_cells) == 0, errmsg - - -@requires_pkg("shapely", "scipy") -@flaky -def test_voronoi_grid4(tmpdir): - name = "vor4" - answer_ncpl = 410 - active_domain = [(0, 0), (100, 0), (100, 100), (0, 100)] - area1 = [(10, 10), (40, 10), (40, 40), (10, 40)] - area2 = [(60, 60), (80, 60), (80, 80), (60, 80)] - tri = Triangle(angle=30, model_ws=str(tmpdir)) - tri.add_polygon(active_domain) - tri.add_polygon(area1) - tri.add_polygon(area2) - tri.add_region((1, 1), 0, maximum_area=100) # point inside active domain - tri.add_region((11, 11), 1, maximum_area=10) # point inside area1 - tri.add_region((61, 61), 2, maximum_area=3) # point inside area2 - tri.build(verbose=False) - - vor = VoronoiGrid(tri) - gridprops = vor.get_gridprops_vertexgrid() - voronoi_grid = VertexGrid(**gridprops, nlay=1) + invalid_cells = [i for i, ivts in enumerate(vor.iverts) if len(ivts) < 3] + # make a plot including invalid cells fig = plt.figure(figsize=(10, 10)) ax = fig.add_subplot() ax.set_aspect("equal") - voronoi_grid.plot(ax=ax) + grid.plot(ax=ax) + ax.plot( + grid.xcellcenters[invalid_cells], + grid.ycellcenters[invalid_cells], + "ro", + ) plt.savefig(os.path.join(str(tmpdir), f"{name}.png")) - # ensure proper number of cells - ncpl = gridprops["ncpl"] - errmsg = f"Number of cells should be {answer_ncpl}. Found {ncpl}" - assert ncpl == answer_ncpl, errmsg - - # ensure that all cells have 3 or more points - ninvalid_cells = [] - for icell, ivts in enumerate(vor.iverts): - if len(ivts) < 3: - ninvalid_cells.append(icell) - errmsg = f"The following cells do not have 3 or more vertices.\n{ninvalid_cells}" - assert len(ninvalid_cells) == 0, errmsg - - -@requires_pkg("shapely", "scipy") -@flaky -def test_voronoi_grid5(tmpdir): - name = "vor5" - answer_ncpl = 1305 - active_domain = [(0, 0), (100, 0), (100, 100), (0, 100)] - area1 = [(10, 10), (40, 10), (40, 40), (10, 40)] - area2 = [(70, 70), (90, 70), (90, 90), (70, 90)] - - tri = Triangle(angle=30, model_ws=str(tmpdir)) - - # requirement that active_domain is first polygon to be added - tri.add_polygon(active_domain) - - # requirement that any holes be added next - theta = np.arange(0.0, 2 * np.pi, 0.2) - radius = 10.0 - x = radius * np.cos(theta) + 50.0 - y = radius * np.sin(theta) + 70.0 - circle_poly0 = [(x, y) for x, y in zip(x, y)] - tri.add_polygon(circle_poly0) - tri.add_hole((50, 70)) - - # Add a polygon to force cells to conform to it - theta = np.arange(0.0, 2 * np.pi, 0.2) - radius = 10.0 - x = radius * np.cos(theta) + 70.0 - y = radius * np.sin(theta) + 20.0 - circle_poly1 = [(x, y) for x, y in zip(x, y)] - tri.add_polygon(circle_poly1) - # tri.add_hole((70, 20)) - - # add line through domain to force conforming cells - line = [(x, x) for x in np.linspace(11, 89, 100)] - tri.add_polygon(line) - - # then regions and other polygons should follow - tri.add_polygon(area1) - tri.add_polygon(area2) - tri.add_region((1, 1), 0, maximum_area=100) # point inside active domain - tri.add_region((11, 11), 1, maximum_area=10) # point inside area1 - tri.add_region((70, 70), 2, maximum_area=1) # point inside area2 + assert ncpl == gridprops["ncpl"] or almost_right + assert ( + len(invalid_cells) == 0 + ), f"The following cells do not have 3 or more vertices.\n{invalid_cells}" - tri.build(verbose=False) - vor = VoronoiGrid(tri) - gridprops = vor.get_gridprops_vertexgrid() - voronoi_grid = VertexGrid(**gridprops, nlay=1) +@pytest.fixture +def structured_grid(): + return GridCases().structured_small() - ninvalid_cells = [] - for icell, ivts in enumerate(vor.iverts): - if len(ivts) < 3: - ninvalid_cells.append(icell) - fig = plt.figure(figsize=(10, 10)) - ax = fig.add_subplot() - ax.set_aspect("equal") - voronoi_grid.plot(ax=ax) +@pytest.fixture +def vertex_grid(): + return GridCases().vertex_small() - # plot invalid cells - ax.plot( - voronoi_grid.xcellcenters[ninvalid_cells], - voronoi_grid.ycellcenters[ninvalid_cells], - "ro", - ) - plt.savefig(os.path.join(str(tmpdir), f"{name}.png")) +@pytest.fixture +def unstructured_grid(): + return GridCases().unstructured_small() - # ensure proper number of cells - ncpl = gridprops["ncpl"] - errmsg = f"Number of cells should be {answer_ncpl}. Found {ncpl}" - assert ncpl == answer_ncpl, errmsg - # ensure that all cells have 3 or more points - errmsg = f"The following cells do not have 3 or more vertices.\n{ninvalid_cells}" - assert len(ninvalid_cells) == 0, errmsg - - -def test_structured_thick(): - nlay, nrow, ncol = 3, 2, 3 - delc = 1.0 * np.ones(nrow, dtype=float) - delr = 1.0 * np.ones(ncol, dtype=float) - top = 10.0 * np.ones((nrow, ncol), dtype=float) - botm = np.zeros((nlay, nrow, ncol), dtype=float) - botm[0, :, :] = 5.0 - botm[1, :, :] = 0.0 - botm[2, :, :] = -5.0 - grid = StructuredGrid( - nlay=nlay, - nrow=nrow, - ncol=ncol, - delc=delc, - delr=delr, - top=top, - botm=botm, - ) - thick = grid.thick +def test_structured_thick(structured_grid): + thick = structured_grid.thick assert np.allclose(thick, 5.0), "thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 10.0) + sat_thick = structured_grid.saturated_thick(structured_grid.botm + 10.0) assert np.allclose(sat_thick, thick), "saturated thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 5.0) + sat_thick = structured_grid.saturated_thick(structured_grid.botm + 5.0) assert np.allclose(sat_thick, thick), "saturated thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 2.5) + sat_thick = structured_grid.saturated_thick(structured_grid.botm + 2.5) assert np.allclose(sat_thick, 2.5), "saturated thicknesses != 2.5" - sat_thick = grid.saturated_thick(grid.botm) + sat_thick = structured_grid.saturated_thick(structured_grid.botm) assert np.allclose(sat_thick, 0.0), "saturated thicknesses != 0." - sat_thick = grid.saturated_thick(grid.botm - 100.0) + sat_thick = structured_grid.saturated_thick(structured_grid.botm - 100.0) assert np.allclose(sat_thick, 0.0), "saturated thicknesses != 0." -def test_vertices_thick(): - nlay, ncpl = 3, 5 - vertices = [ - [0, 0.0, 3.0], - [1, 1.0, 3.0], - [2, 2.0, 3.0], - [3, 0.0, 2.0], - [4, 1.0, 2.0], - [5, 2.0, 2.0], - [6, 0.0, 1.0], - [7, 1.0, 1.0], - [8, 2.0, 1.0], - [9, 0.0, 0.0], - [10, 1.0, 0.0], - ] - iverts = [ - [0, 0, 1, 4, 3], - [1, 1, 2, 5, 4], - [2, 3, 4, 7, 6], - [3, 4, 5, 8, 7], - [4, 6, 7, 10, 9], - [5, 0, 1, 4, 3], - [6, 1, 2, 5, 4], - [7, 3, 4, 7, 6], - [8, 4, 5, 8, 7], - [9, 6, 7, 10, 9], - [10, 0, 1, 4, 3], - [11, 1, 2, 5, 4], - [12, 3, 4, 7, 6], - [13, 4, 5, 8, 7], - [14, 6, 7, 10, 9], - ] - top = np.ones(ncpl, dtype=float) * 10.0 - botm = np.zeros((nlay, ncpl), dtype=float) - botm[0, :] = 5.0 - botm[1, :] = 0.0 - botm[2, :] = -5.0 - grid = VertexGrid( - nlay=nlay, - ncpl=ncpl, - vertices=vertices, - cell2d=iverts, - top=top, - botm=botm, - ) - thick = grid.thick +def test_vertices_thick(vertex_grid): + thick = vertex_grid.thick assert np.allclose(thick, 5.0), "thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 10.0) + sat_thick = vertex_grid.saturated_thick(vertex_grid.botm + 10.0) assert np.allclose(sat_thick, thick), "saturated thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 5.0) + sat_thick = vertex_grid.saturated_thick(vertex_grid.botm + 5.0) assert np.allclose(sat_thick, thick), "saturated thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 2.5) + sat_thick = vertex_grid.saturated_thick(vertex_grid.botm + 2.5) assert np.allclose(sat_thick, 2.5), "saturated thicknesses != 2.5" - sat_thick = grid.saturated_thick(grid.botm) + sat_thick = vertex_grid.saturated_thick(vertex_grid.botm) assert np.allclose(sat_thick, 0.0), "saturated thicknesses != 0." - sat_thick = grid.saturated_thick(grid.botm - 100.0) + sat_thick = vertex_grid.saturated_thick(vertex_grid.botm - 100.0) assert np.allclose(sat_thick, 0.0), "saturated thicknesses != 0." -def test_unstructured_thick(): - nlay = 3 - ncpl = [5, 5, 5] - vertices = [ - [0, 0.0, 3.0], - [1, 1.0, 3.0], - [2, 2.0, 3.0], - [3, 0.0, 2.0], - [4, 1.0, 2.0], - [5, 2.0, 2.0], - [6, 0.0, 1.0], - [7, 1.0, 1.0], - [8, 2.0, 1.0], - [9, 0.0, 0.0], - [10, 1.0, 0.0], - ] - iverts = [ - [0, 0, 1, 4, 3], - [1, 1, 2, 5, 4], - [2, 3, 4, 7, 6], - [3, 4, 5, 8, 7], - [4, 6, 7, 10, 9], - [5, 0, 1, 4, 3], - [6, 1, 2, 5, 4], - [7, 3, 4, 7, 6], - [8, 4, 5, 8, 7], - [9, 6, 7, 10, 9], - [10, 0, 1, 4, 3], - [11, 1, 2, 5, 4], - [12, 3, 4, 7, 6], - [13, 4, 5, 8, 7], - [14, 6, 7, 10, 9], - ] - xcenters = [ - 0.5, - 1.5, - 0.5, - 1.5, - 0.5, - ] - ycenters = [ - 2.5, - 2.5, - 1.5, - 1.5, - 0.5, - ] - top = np.ones((nlay, 5), dtype=float) - top[0, :] = 10.0 - top[1, :] = 5.0 - top[2, :] = 0.0 - botm = np.zeros((nlay, 5), dtype=float) - botm[0, :] = 5.0 - botm[1, :] = 0.0 - botm[2, :] = -5.0 - - grid = UnstructuredGrid( - vertices=vertices, - iverts=iverts, - xcenters=xcenters, - ycenters=ycenters, - ncpl=ncpl, - top=top.flatten(), - botm=botm.flatten(), - ) - - thick = grid.thick +def test_unstructured_thick(unstructured_grid): + thick = unstructured_grid.thick assert np.allclose(thick, 5.0), "thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 10.0) + sat_thick = unstructured_grid.saturated_thick( + unstructured_grid.botm + 10.0 + ) assert np.allclose(sat_thick, thick), "saturated thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 5.0) + sat_thick = unstructured_grid.saturated_thick(unstructured_grid.botm + 5.0) assert np.allclose(sat_thick, thick), "saturated thicknesses != 5." - sat_thick = grid.saturated_thick(grid.botm + 2.5) + sat_thick = unstructured_grid.saturated_thick(unstructured_grid.botm + 2.5) assert np.allclose(sat_thick, 2.5), "saturated thicknesses != 2.5" - sat_thick = grid.saturated_thick(grid.botm) + sat_thick = unstructured_grid.saturated_thick(unstructured_grid.botm) assert np.allclose(sat_thick, 0.0), "saturated thicknesses != 0." - sat_thick = grid.saturated_thick(grid.botm - 100.0) + sat_thick = unstructured_grid.saturated_thick( + unstructured_grid.botm - 100.0 + ) assert np.allclose(sat_thick, 0.0), "saturated thicknesses != 0." -def test_ncb_thick(): - nlay = 3 - nrow = ncol = 15 - laycbd = np.array([1, 2, 0], dtype=int) - ncb = np.count_nonzero(laycbd) - dx = dy = 150 - delc = np.array( - [ - dy, - ] - * nrow - ) - delr = np.array( - [ - dx, - ] - * ncol - ) - top = np.ones((15, 15)) - botm = np.ones((nlay + ncb, nrow, ncol)) - elevations = np.array([-10, -20, -40, -50, -70])[:, np.newaxis] - botm *= elevations[:, None] - - modelgrid = StructuredGrid( - delc=delc, - delr=delr, - top=top, - botm=botm, - nlay=nlay, - nrow=nrow, - ncol=ncol, - laycbd=laycbd, - ) - - thick = modelgrid.thick +@parametrize_with_cases("grid", cases=GridCases, prefix="structured_cbd") +def test_structured_ncb_thick(grid): + thick = grid.thick - assert ( - thick.shape[0] == nlay + ncb + assert thick.shape[0] == grid.nlay + np.count_nonzero( + grid.laycbd ), "grid thick attribute returns incorrect shape" - thick = modelgrid.remove_confining_beds(modelgrid.thick) + thick = grid.remove_confining_beds(grid.thick) assert ( - thick.shape == modelgrid.shape + thick.shape == grid.shape ), "quasi3d confining beds not properly removed" - sat_thick = modelgrid.saturated_thick(modelgrid.thick) + sat_thick = grid.saturated_thick(grid.thick) assert ( - sat_thick.shape == modelgrid.shape + sat_thick.shape == grid.shape ), "saturated_thickness confining beds not removed" - if sat_thick[1, 0, 0] != 20: - raise AssertionError( - "saturated_thickness is not properly indexing confining beds" + assert ( + sat_thick[1, 0, 0] == 20 + ), "saturated_thickness is not properly indexing confining beds" + + +@parametrize_with_cases("grid", cases=GridCases, prefix="unstructured") +def test_unstructured_iverts(grid): + iverts = grid.iverts + assert not any( + None in l for l in iverts + ), "None type should not be returned in iverts list" + + +@parametrize_with_cases("grid", cases=GridCases, prefix="structured") +def test_get_lni_structured(grid): + for nn in range(0, grid.nnodes): + layer, i = grid.get_lni(nn) + assert layer * grid.ncpl + i == nn + + +@parametrize_with_cases("grid", cases=GridCases, prefix="vertex") +def test_get_lni_vertex(grid): + for nn in range(0, grid.nnodes): + layer, i = grid.get_lni(nn) + assert layer * grid.ncpl + i == nn + + +@parametrize_with_cases("grid", cases=GridCases, prefix="unstructured") +def test_get_lni_unstructured(grid): + for nn in range(0, grid.nnodes): + layer, i = grid.get_lni(nn) + csum = [0] + list( + np.cumsum( + ( + list(grid.ncpl) + if not isinstance(grid.ncpl, int) + else [grid.ncpl for _ in range(grid.nlay)] + ) + ) ) + assert csum[layer] + i == nn diff --git a/autotest/test_grid_cases.py b/autotest/test_grid_cases.py new file mode 100644 index 0000000000..8dcea84d23 --- /dev/null +++ b/autotest/test_grid_cases.py @@ -0,0 +1,400 @@ +import numpy as np + +from flopy.discretization import StructuredGrid, UnstructuredGrid, VertexGrid +from flopy.utils.triangle import Triangle +from flopy.utils.voronoi import VoronoiGrid + + +class GridCases: + def structured_small(self): + nlay, nrow, ncol = 3, 2, 3 + delc = 1.0 * np.ones(nrow, dtype=float) + delr = 1.0 * np.ones(ncol, dtype=float) + top = 10.0 * np.ones((nrow, ncol), dtype=float) + botm = np.zeros((nlay, nrow, ncol), dtype=float) + botm[0, :, :] = 5.0 + botm[1, :, :] = 0.0 + botm[2, :, :] = -5.0 + return StructuredGrid( + nlay=nlay, + nrow=nrow, + ncol=ncol, + delc=delc, + delr=delr, + top=top, + botm=botm, + ) + + def structured_cbd_small(self): + nlay = 3 + nrow = ncol = 15 + laycbd = np.array([1, 2, 0], dtype=int) + ncb = np.count_nonzero(laycbd) + dx = dy = 150 + delc = np.array( + [ + dy, + ] + * nrow + ) + delr = np.array( + [ + dx, + ] + * ncol + ) + top = np.ones((15, 15)) + botm = np.ones((nlay + ncb, nrow, ncol)) + elevations = np.array([-10, -20, -40, -50, -70])[:, np.newaxis] + botm *= elevations[:, None] + + return StructuredGrid( + delc=delc, + delr=delr, + top=top, + botm=botm, + nlay=nlay, + nrow=nrow, + ncol=ncol, + laycbd=laycbd, + ) + + def vertex_small(self): + nlay, ncpl = 3, 5 + vertices = [ + [0, 0.0, 3.0], + [1, 1.0, 3.0], + [2, 2.0, 3.0], + [3, 0.0, 2.0], + [4, 1.0, 2.0], + [5, 2.0, 2.0], + [6, 0.0, 1.0], + [7, 1.0, 1.0], + [8, 2.0, 1.0], + [9, 0.0, 0.0], + [10, 1.0, 0.0], + ] + iverts = [ + [0, 0, 1, 4, 3], + [1, 1, 2, 5, 4], + [2, 3, 4, 7, 6], + [3, 4, 5, 8, 7], + [4, 6, 7, 10, 9], + [5, 0, 1, 4, 3], + [6, 1, 2, 5, 4], + [7, 3, 4, 7, 6], + [8, 4, 5, 8, 7], + [9, 6, 7, 10, 9], + [10, 0, 1, 4, 3], + [11, 1, 2, 5, 4], + [12, 3, 4, 7, 6], + [13, 4, 5, 8, 7], + [14, 6, 7, 10, 9], + ] + top = np.ones(ncpl, dtype=float) * 10.0 + botm = np.zeros((nlay, ncpl), dtype=float) + botm[0, :] = 5.0 + botm[1, :] = 0.0 + botm[2, :] = -5.0 + return VertexGrid( + nlay=nlay, + ncpl=ncpl, + vertices=vertices, + cell2d=iverts, + top=top, + botm=botm, + ) + + def unstructured_small(self): + nlay = 3 + ncpl = [5, 5, 5] + vertices = [ + [0, 0.0, 3.0], + [1, 1.0, 3.0], + [2, 2.0, 3.0], + [3, 0.0, 2.0], + [4, 1.0, 2.0], + [5, 2.0, 2.0], + [6, 0.0, 1.0], + [7, 1.0, 1.0], + [8, 2.0, 1.0], + [9, 0.0, 0.0], + [10, 1.0, 0.0], + ] + iverts = [ + [0, 0, 1, 4, 3], + [1, 1, 2, 5, 4], + [2, 3, 4, 7, 6], + [3, 4, 5, 8, 7], + [4, 6, 7, 10, 9], + [5, 0, 1, 4, 3], + [6, 1, 2, 5, 4], + [7, 3, 4, 7, 6], + [8, 4, 5, 8, 7], + [9, 6, 7, 10, 9], + [10, 0, 1, 4, 3], + [11, 1, 2, 5, 4], + [12, 3, 4, 7, 6], + [13, 4, 5, 8, 7], + [14, 6, 7, 10, 9], + ] + xcenters = [ + 0.5, + 1.5, + 0.5, + 1.5, + 0.5, + ] + ycenters = [ + 2.5, + 2.5, + 1.5, + 1.5, + 0.5, + ] + top = np.ones((nlay, 5), dtype=float) + top[0, :] = 10.0 + top[1, :] = 5.0 + top[2, :] = 0.0 + botm = np.zeros((nlay, 5), dtype=float) + botm[0, :] = 5.0 + botm[1, :] = 0.0 + botm[2, :] = -5.0 + + return UnstructuredGrid( + vertices=vertices, + iverts=iverts, + xcenters=xcenters, + ycenters=ycenters, + ncpl=ncpl, + top=top.flatten(), + botm=botm.flatten(), + ) + + def unstructured_medium(self): + iverts = [ + [4, 3, 2, 1, 0, None], + [7, 0, 1, 6, 5, None], + [11, 10, 9, 8, 2, 3], + [1, 6, 13, 12, 8, 2], + [15, 14, 13, 6, 5, None], + [10, 9, 18, 17, 16, None], + [8, 12, 20, 19, 18, 9], + [22, 14, 13, 12, 20, 21], + [24, 17, 18, 19, 23, None], + [21, 20, 19, 23, 25, None], + ] + verts = [ + [0.0, 22.5], + [5.1072, 22.5], + [7.5, 24.0324], + [7.5, 30.0], + [0.0, 30.0], + [0.0, 7.5], + [4.684, 7.5], + [0.0, 15.0], + [14.6582, 21.588], + [22.5, 24.3766], + [22.5, 30.0], + [15.0, 30.0], + [15.3597, 8.4135], + [7.5, 5.6289], + [7.5, 0.0], + [0.0, 0.0], + [30.0, 30.0], + [30.0, 22.5], + [25.3285, 22.5], + [24.8977, 7.5], + [22.5, 5.9676], + [22.5, 0.0], + [15.0, 0.0], + [30.0, 7.5], + [30.0, 15.0], + [30.0, 0.0], + ] + + return UnstructuredGrid(verts, iverts, ncpl=[len(iverts)]) + + def voronoi_polygon(self, tmpdir): + ncpl = 3803 + domain = [ + [1831.381546, 6335.543757], + [4337.733475, 6851.136153], + [6428.747084, 6707.916043], + [8662.980804, 6493.085878], + [9350.437333, 5891.561415], + [9235.861245, 4717.156511], + [8963.743036, 3685.971717], + [8691.624826, 2783.685023], + [8047.13433, 2038.94045], + [7416.965845, 578.0953252], + [6414.425073, 105.4689614], + [5354.596258, 205.7230386], + [4624.173696, 363.2651598], + [3363.836725, 563.7733141], + [1330.11116, 1809.788273], + [399.1804436, 2998.515188], + [914.7728404, 5132.494831], + # [1831.381546, 6335.543757], + ] + poly = np.array(domain) + max_area = 100.0**2 + angle = 30 + + tri = Triangle( + maximum_area=max_area, angle=angle, model_ws=str(tmpdir) + ) + tri.add_polygon(poly) + tri.build(verbose=False) + vor = VoronoiGrid(tri) + gridprops = vor.get_gridprops_vertexgrid() + grid = VertexGrid(**gridprops, nlay=1) + + return ncpl, vor, gridprops, grid + + def voronoi_rectangle(self, tmpdir): + ncpl = 1679 + xmin = 0.0 + xmax = 2.0 + ymin = 0.0 + ymax = 1.0 + poly = np.array( + ((xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)) + ) + max_area = 0.001 + angle = 30 + + tri = Triangle( + maximum_area=max_area, angle=angle, model_ws=str(tmpdir) + ) + tri.add_polygon(poly) + tri.build(verbose=False) + vor = VoronoiGrid(tri) + gridprops = vor.get_gridprops_vertexgrid() + grid = VertexGrid(**gridprops, nlay=1) + + return ncpl, vor, gridprops, grid + + def voronoi_circle(self, tmpdir): + ncpl = 538 + theta = np.arange(0.0, 2 * np.pi, 0.2) + radius = 100.0 + x = radius * np.cos(theta) + y = radius * np.sin(theta) + poly = [(x, y) for x, y in zip(x, y)] + max_area = 50 + angle = 30 + + tri = Triangle( + maximum_area=max_area, angle=angle, model_ws=str(tmpdir) + ) + tri.add_polygon(poly) + tri.build(verbose=False) + vor = VoronoiGrid(tri) + gridprops = vor.get_gridprops_vertexgrid() + grid = VertexGrid(**gridprops, nlay=1) + + return ncpl, vor, gridprops, grid + + def voronoi_nested_circles(self, tmpdir): + ncpl = 300 + + theta = np.arange(0.0, 2 * np.pi, 0.2) + radius = 100.0 + x = radius * np.cos(theta) + y = radius * np.sin(theta) + circle_poly = [(x, y) for x, y in zip(x, y)] + + theta = np.arange(0.0, 2 * np.pi, 0.2) + radius = 30.0 + x = radius * np.cos(theta) + 25.0 + y = radius * np.sin(theta) + 25.0 + inner_circle_poly = [(x, y) for x, y in zip(x, y)] + + polys = [circle_poly, inner_circle_poly] + max_area = 100 + angle = 30 + + tri = Triangle( + maximum_area=max_area, angle=angle, model_ws=str(tmpdir) + ) + for poly in polys: + tri.add_polygon(poly) + tri.add_hole((25, 25)) + tri.build(verbose=False) + vor = VoronoiGrid(tri) + gridprops = vor.get_gridprops_vertexgrid() + grid = VertexGrid(**gridprops, nlay=1) + + return ncpl, vor, gridprops, grid + + def voronoi_polygons(self, tmpdir): + ncpl = 410 + active_domain = [(0, 0), (100, 0), (100, 100), (0, 100)] + area1 = [(10, 10), (40, 10), (40, 40), (10, 40)] + area2 = [(60, 60), (80, 60), (80, 80), (60, 80)] + tri = Triangle(angle=30, model_ws=str(tmpdir)) + tri.add_polygon(active_domain) + tri.add_polygon(area1) + tri.add_polygon(area2) + tri.add_region( + (1, 1), 0, maximum_area=100 + ) # point inside active domain + tri.add_region((11, 11), 1, maximum_area=10) # point inside area1 + tri.add_region((61, 61), 2, maximum_area=3) # point inside area2 + tri.build(verbose=False) + vor = VoronoiGrid(tri) + gridprops = vor.get_gridprops_vertexgrid() + grid = VertexGrid(**gridprops, nlay=1) + + return ncpl, vor, gridprops, grid + + def voronoi_many_polygons(self, tmpdir): + ncpl = 1305 + active_domain = [(0, 0), (100, 0), (100, 100), (0, 100)] + area1 = [(10, 10), (40, 10), (40, 40), (10, 40)] + area2 = [(70, 70), (90, 70), (90, 90), (70, 90)] + + tri = Triangle(angle=30, model_ws=str(tmpdir)) + + # requirement that active_domain is first polygon to be added + tri.add_polygon(active_domain) + + # requirement that any holes be added next + theta = np.arange(0.0, 2 * np.pi, 0.2) + radius = 10.0 + x = radius * np.cos(theta) + 50.0 + y = radius * np.sin(theta) + 70.0 + circle_poly0 = [(x, y) for x, y in zip(x, y)] + tri.add_polygon(circle_poly0) + tri.add_hole((50, 70)) + + # Add a polygon to force cells to conform to it + theta = np.arange(0.0, 2 * np.pi, 0.2) + radius = 10.0 + x = radius * np.cos(theta) + 70.0 + y = radius * np.sin(theta) + 20.0 + circle_poly1 = [(x, y) for x, y in zip(x, y)] + tri.add_polygon(circle_poly1) + # tri.add_hole((70, 20)) + + # add line through domain to force conforming cells + line = [(x, x) for x in np.linspace(11, 89, 100)] + tri.add_polygon(line) + + # then regions and other polygons should follow + tri.add_polygon(area1) + tri.add_polygon(area2) + tri.add_region( + (1, 1), 0, maximum_area=100 + ) # point inside active domain + tri.add_region((11, 11), 1, maximum_area=10) # point inside area1 + tri.add_region((70, 70), 2, maximum_area=1) # point inside area2 + + tri.build(verbose=False) + + vor = VoronoiGrid(tri) + gridprops = vor.get_gridprops_vertexgrid() + grid = VertexGrid(**gridprops, nlay=1) + + return ncpl, vor, gridprops, grid diff --git a/autotest/test_gridgen.py b/autotest/test_gridgen.py index 9dd1a9273a..549edb8c81 100644 --- a/autotest/test_gridgen.py +++ b/autotest/test_gridgen.py @@ -5,10 +5,10 @@ import matplotlib.pyplot as plt import numpy as np import pytest +from autotest.conftest import has_pkg, requires_exe, requires_pkg from matplotlib.collections import LineCollection, PathCollection, QuadMesh import flopy -from autotest.conftest import has_pkg, requires_exe, requires_pkg from flopy.utils.gridgen import Gridgen diff --git a/autotest/test_gridintersect.py b/autotest/test_gridintersect.py index 2b5e8c45c7..de371426fb 100644 --- a/autotest/test_gridintersect.py +++ b/autotest/test_gridintersect.py @@ -3,7 +3,6 @@ import matplotlib.pyplot as plt import numpy as np import pytest - from autotest.conftest import has_pkg, requires_pkg import flopy.discretization as fgrid @@ -15,7 +14,12 @@ if has_pkg("shapely"): from shapely.geometry import ( - Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, ) rtree_toggle = pytest.mark.parametrize("rtree", [True, False]) @@ -1301,4 +1305,4 @@ def test_raster_sampling_methods(example_data_path): if __name__ == "__main__": - test_all_intersections_shapely_no_strtree() \ No newline at end of file + test_all_intersections_shapely_no_strtree() diff --git a/autotest/test_gridutil.py b/autotest/test_gridutil.py new file mode 100644 index 0000000000..46ac4e286d --- /dev/null +++ b/autotest/test_gridutil.py @@ -0,0 +1,46 @@ +import pytest + +from flopy.utils.gridutil import get_lni + +cases = [ + (10, 0, 0, 0), + ([10, 10], 0, 0, 0), + ([10, 10], 10, 1, 0), + ([10, 10], 9, 0, 9), + ([10, 10], 15, 1, 5), + ([10, 20], 29, 1, 19), +] + + +@pytest.mark.parametrize("ncpl, nn, expected_layer, expected_ni", cases) +def test_get_lni_one_node(ncpl, nn, expected_layer, expected_ni): + actual_layer, actual_i = get_lni(ncpl, nn) + assert actual_layer == expected_layer + assert actual_i == expected_ni + + +@pytest.mark.parametrize("ncpl, nn, expected_layer, expected_ni", cases) +def test_get_lni_multiple_nodes(ncpl, nn, expected_layer, expected_ni): + # use previous neighbor if last index + # in a layer, otherwise next neighbor + t = 1 + if nn == 9 or nn == 29: + t = -1 + + nodes = [nn, nn + t] + lni = get_lni(ncpl, *nodes) + assert isinstance(lni, list) + i = 0 + for (actual_layer, actual_ni) in lni: + assert actual_layer == expected_layer + assert actual_ni == expected_ni + (i * t) + i += 1 + + +@pytest.mark.parametrize("ncpl", [c[0] for c in cases[1:]]) +def test_get_lni_no_nodes(ncpl): + lni = get_lni(ncpl) + nnodes = sum(ncpl) + assert len(lni) == nnodes + for nn in range(nnodes): + assert lni[nn] == get_lni(ncpl, nn) diff --git a/autotest/test_headufile.py b/autotest/test_headufile.py index a4ccbbfed1..9fb4b99cdf 100644 --- a/autotest/test_headufile.py +++ b/autotest/test_headufile.py @@ -1,8 +1,4 @@ -""" -HeadUFile get_ts tests using t505_test.py -""" - -import os +from pathlib import Path import pytest from autotest.conftest import requires_exe, requires_pkg @@ -18,11 +14,11 @@ ) from flopy.utils import HeadUFile from flopy.utils.gridgen import Gridgen +from flopy.utils.gridutil import get_lni -@requires_exe("mfusg", "gridgen") -@requires_pkg("shapely", "shapefile") -def test_mfusg(tmpdir): +@pytest.fixture(scope="module") +def mfusg_model(module_tmpdir): from shapely.geometry import Polygon name = "dummy" @@ -36,7 +32,7 @@ def test_mfusg(tmpdir): botm = [top - k * dz for k in range(1, nlay + 1)] # create dummy model and dis package for gridgen - m = Modflow(modelname=name, model_ws=str(tmpdir)) + m = Modflow(modelname=name, model_ws=str(module_tmpdir)) dis = ModflowDis( m, nlay=nlay, @@ -49,7 +45,7 @@ def test_mfusg(tmpdir): ) # Create and build the gridgen model with a refined area in the middle - g = Gridgen(dis, model_ws=str(tmpdir)) + g = Gridgen(dis, model_ws=str(module_tmpdir)) polys = [Polygon([(4, 4), (6, 4), (6, 6), (4, 6)])] g.add_refinement_features(polys, "polygon", 3, layers=[0]) @@ -68,7 +64,7 @@ def test_mfusg(tmpdir): name = "mymodel" m = MfUsg( modelname=name, - model_ws=str(tmpdir), + model_ws=str(module_tmpdir), exe_name="mfusg", structured=False, ) @@ -89,72 +85,77 @@ def test_mfusg(tmpdir): m.run_model() - # head is returned as a list of head arrays for each layer - head_file = os.path.join(str(tmpdir), f"{name}.hds") - head = HeadUFile(head_file).get_data() + # head contains a list of head arrays for each layer + head_file_path = Path(module_tmpdir / f"{name}.hds") + return m, HeadUFile(str(head_file_path)) + + +@requires_exe("mfusg", "gridgen") +@requires_pkg("shapely", "shapefile") +def test_get_ts_single_node(mfusg_model): + model, head_file = mfusg_model + head = head_file.get_data() # test if single node idx works - one_hds = HeadUFile(head_file).get_ts(idx=300) - if one_hds[0, 1] != head[0][300]: - raise AssertionError( - "Error head from 'get_ts' != head from 'get_data'" - ) + one_hds = head_file.get_ts(idx=300) + assert ( + one_hds[0, 1] == head[0][300] + ), "head from 'get_ts' != head from 'get_data'" - # test if list of nodes for idx works - nodes = [300, 182, 65] - multi_hds = HeadUFile(head_file).get_ts(idx=nodes) +@requires_exe("mfusg", "gridgen") +@requires_pkg("shapely", "shapefile") +def test_get_ts_multiple_nodes(mfusg_model): + model, head_file = mfusg_model + grid = model.modelgrid + head = head_file.get_data() + + # test if list of nodes for idx works + nodes = [500, 300, 182, 65] + multi_hds = head_file.get_ts(idx=nodes) for i, node in enumerate(nodes): - if multi_hds[0, i + 1] != head[0][node]: - raise AssertionError( - "Error head from 'get_ts' != head from 'get_data'" - ) - - -def test_usg_iverts(): - iverts = [ - [4, 3, 2, 1, 0, None], - [7, 0, 1, 6, 5, None], - [11, 10, 9, 8, 2, 3], - [1, 6, 13, 12, 8, 2], - [15, 14, 13, 6, 5, None], - [10, 9, 18, 17, 16, None], - [8, 12, 20, 19, 18, 9], - [22, 14, 13, 12, 20, 21], - [24, 17, 18, 19, 23, None], - [21, 20, 19, 23, 25, None], - ] - verts = [ - [0.0, 22.5], - [5.1072, 22.5], - [7.5, 24.0324], - [7.5, 30.0], - [0.0, 30.0], - [0.0, 7.5], - [4.684, 7.5], - [0.0, 15.0], - [14.6582, 21.588], - [22.5, 24.3766], - [22.5, 30.0], - [15.0, 30.0], - [15.3597, 8.4135], - [7.5, 5.6289], - [7.5, 0.0], - [0.0, 0.0], - [30.0, 30.0], - [30.0, 22.5], - [25.3285, 22.5], - [24.8977, 7.5], - [22.5, 5.9676], - [22.5, 0.0], - [15.0, 0.0], - [30.0, 7.5], - [30.0, 15.0], - [30.0, 0.0], - ] - - grid = UnstructuredGrid(verts, iverts, ncpl=[len(iverts)]) - - iverts = grid.iverts - if any(None in l for l in iverts): - raise ValueError("None type should not be returned in iverts list") + li, ni = get_lni(grid.ncpl, node) + assert ( + multi_hds[0, i + 1] == head[li][ni] + ), "head from 'get_ts' != head from 'get_data'" + + +@requires_exe("mfusg", "gridgen") +@requires_pkg("shapely", "shapefile") +def test_get_ts_all_nodes(mfusg_model): + model, head_file = mfusg_model + grid = model.modelgrid + head = head_file.get_data() + + # test if list of nodes for idx works + nodes = list(range(0, grid.nnodes)) + multi_hds = head_file.get_ts(idx=nodes) + for node in nodes: + li, ni = get_lni(grid.ncpl, node) + assert ( + multi_hds[0, node + 1] == head[li][ni] + ), "head from 'get_ts' != head from 'get_data'" + + +@requires_exe("mfusg", "gridgen") +@requires_pkg("shapely", "shapefile") +def test_get_lni(mfusg_model): + # added to help reproduce https://github.com/modflowpy/flopy/issues/1503 + + model, head_file = mfusg_model + grid = model.modelgrid + head = head_file.get_data() + + def get_expected(): + exp = dict() + for l, ncpl in enumerate(list(grid.ncpl)): + exp[l] = dict() + for nn in range(ncpl): + exp[l][nn] = head[l][nn] + return exp + + nodes = list(range(0, grid.nnodes)) + expected = get_expected() + for node in nodes: + layer, nn = get_lni(grid.ncpl, node) + assert expected[layer][nn] == head[layer][nn] diff --git a/autotest/test_hydmodfile.py b/autotest/test_hydmodfile.py index a0f88d856d..6f8ddef5aa 100644 --- a/autotest/test_hydmodfile.py +++ b/autotest/test_hydmodfile.py @@ -2,7 +2,6 @@ import numpy as np import pytest - from autotest.conftest import has_pkg, requires_pkg from flopy.modflow import Modflow, ModflowHyd diff --git a/autotest/test_lake_connections.py b/autotest/test_lake_connections.py index ced0431086..3a9fc6e2c9 100644 --- a/autotest/test_lake_connections.py +++ b/autotest/test_lake_connections.py @@ -2,7 +2,6 @@ import numpy as np import pytest - from autotest.conftest import requires_pkg from flopy.discretization import StructuredGrid diff --git a/autotest/test_lgr.py b/autotest/test_lgr.py index 22b985d259..c9646c9fc0 100644 --- a/autotest/test_lgr.py +++ b/autotest/test_lgr.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from flaky import flaky from autotest.conftest import requires_exe +from flaky import flaky import flopy diff --git a/autotest/test_listbudget.py b/autotest/test_listbudget.py index 801429dca1..815699f220 100644 --- a/autotest/test_listbudget.py +++ b/autotest/test_listbudget.py @@ -3,7 +3,6 @@ import numpy as np import pytest - from autotest.conftest import has_pkg, requires_pkg from flopy.utils import ( diff --git a/autotest/test_mf6.py b/autotest/test_mf6.py index b23f0c7714..6d51e85881 100644 --- a/autotest/test_mf6.py +++ b/autotest/test_mf6.py @@ -2,10 +2,9 @@ import numpy as np import pytest - -import flopy from autotest.conftest import requires_exe +import flopy from flopy.mf6 import ( MFModel, MFSimulation, @@ -20,6 +19,7 @@ ModflowGwfghb, ModflowGwfgnc, ModflowGwfgwf, + ModflowGwfgwt, ModflowGwfhfb, ModflowGwfic, ModflowGwflak, @@ -42,9 +42,13 @@ ModflowGwtssm, ModflowIms, ModflowTdis, - ModflowUtllaktab, ModflowGwfgwt, + ModflowUtllaktab, +) +from flopy.mf6.coordinates.modeldimensions import ( + DataDimensions, + ModelDimensions, + PackageDimensions, ) -from flopy.mf6.coordinates.modeldimensions import ModelDimensions, PackageDimensions, DataDimensions from flopy.mf6.data.mffileaccess import MFFileAccessArray from flopy.mf6.data.mfstructure import MFDataItemStructure, MFDataStructure from flopy.mf6.mfbase import MFFileMgmt @@ -73,14 +77,14 @@ def write_head( - fbin, - data, - kstp=1, - kper=1, - pertim=1.0, - totim=1.0, - text=" HEAD", - ilay=1, + fbin, + data, + kstp=1, + kper=1, + pertim=1.0, + totim=1.0, + text=" HEAD", + ilay=1, ): dt = np.dtype( [ @@ -397,7 +401,7 @@ def test_mf6_subdir(tmpdir): ) gwf_r = sim_r.get_model() assert ( - gwf.dis.delc.get_file_entry() == gwf_r.dis.delc.get_file_entry() + gwf.dis.delc.get_file_entry() == gwf_r.dis.delc.get_file_entry() ), "Something wrong with model external paths" sim_r.set_all_data_internal() @@ -412,7 +416,7 @@ def test_mf6_subdir(tmpdir): ) gwf_r2 = sim_r.get_model() assert ( - gwf_r.dis.delc.get_file_entry() == gwf_r2.dis.delc.get_file_entry() + gwf_r.dis.delc.get_file_entry() == gwf_r2.dis.delc.get_file_entry() ), "Something wrong with model external paths" diff --git a/autotest/test_mfnwt.py b/autotest/test_mfnwt.py index 245be3061a..dc6e238478 100644 --- a/autotest/test_mfnwt.py +++ b/autotest/test_mfnwt.py @@ -24,10 +24,10 @@ def analytical_water_table_solution(h1, h2, z, R, K, L, x): b1 = h1 - z b2 = h2 - z h = ( - np.sqrt( - b1 ** 2 - (x / L) * (b1 ** 2 - b2 ** 2) + (R * x / K) * (L - x) - ) - + z + np.sqrt( + b1**2 - (x / L) * (b1**2 - b2**2) + (R * x / K) * (L - x) + ) + + z ) return h diff --git a/autotest/test_mnw.py b/autotest/test_mnw.py index 33a5f2b129..fb5420c912 100644 --- a/autotest/test_mnw.py +++ b/autotest/test_mnw.py @@ -3,7 +3,6 @@ import numpy as np import pytest - from autotest.conftest import requires_pkg from flopy.modflow import Mnw, Modflow, ModflowDis, ModflowMnw2 diff --git a/autotest/test_modflow.py b/autotest/test_modflow.py index 861b82bb49..bad6cfa17d 100644 --- a/autotest/test_modflow.py +++ b/autotest/test_modflow.py @@ -22,7 +22,8 @@ ModflowPcg, ModflowRch, ModflowRiv, - ModflowWel, ModflowSfr2, + ModflowSfr2, + ModflowWel, ) from flopy.mt3d import Mt3dBtn, Mt3dms from flopy.plot import PlotMapView @@ -140,10 +141,10 @@ def test_mt_modelgrid(tmpdir): ) assert ( - swt.modelgrid.xoffset == mt.modelgrid.xoffset == ml.modelgrid.xoffset + swt.modelgrid.xoffset == mt.modelgrid.xoffset == ml.modelgrid.xoffset ) assert ( - swt.modelgrid.yoffset == mt.modelgrid.yoffset == ml.modelgrid.yoffset + swt.modelgrid.yoffset == mt.modelgrid.yoffset == ml.modelgrid.yoffset ) assert mt.modelgrid.epsg == ml.modelgrid.epsg == swt.modelgrid.epsg assert mt.modelgrid.angrot == ml.modelgrid.angrot == swt.modelgrid.angrot @@ -174,10 +175,10 @@ def test_mt_modelgrid(tmpdir): ) assert ( - ml.modelgrid.xoffset == mt.modelgrid.xoffset == swt.modelgrid.xoffset + ml.modelgrid.xoffset == mt.modelgrid.xoffset == swt.modelgrid.xoffset ) assert ( - mt.modelgrid.yoffset == ml.modelgrid.yoffset == swt.modelgrid.yoffset + mt.modelgrid.yoffset == ml.modelgrid.yoffset == swt.modelgrid.yoffset ) assert mt.modelgrid.epsg == ml.modelgrid.epsg == swt.modelgrid.epsg assert mt.modelgrid.angrot == ml.modelgrid.angrot == swt.modelgrid.angrot @@ -299,12 +300,12 @@ def test_load_twri_grid(example_data_path): ), "modelgrid is not an StructuredGrid instance" shape = (3, 15, 15) assert ( - mg.shape == shape + mg.shape == shape ), f"modelgrid shape {mg.shape} not equal to {shape}" thick = mg.thick shape = (5, 15, 15) assert ( - thick.shape == shape + thick.shape == shape ), f"thickness shape {thick.shape} not equal to {shape}" @@ -547,8 +548,8 @@ def parameters_model_namfiles(): @requires_exe("mf2005") @pytest.mark.parametrize( - "namfile", - mf2005_model_namfiles() + parameters_model_namfiles()) + "namfile", mf2005_model_namfiles() + parameters_model_namfiles() +) def test_mf2005_test_models_load(example_data_path, namfile): assert not Modflow.load( namfile, @@ -623,7 +624,9 @@ def test_write_irch(tmpdir, example_data_path): assert np.abs(d).sum() == 0 -@pytest.mark.xfail(reason="Pending https://github.com/modflowpy/flopy/issues/1472") +@pytest.mark.xfail( + reason="Pending https://github.com/modflowpy/flopy/issues/1472" +) def test_mflist_external(tmpdir): # TODO: fails if external path is absolute (not relative to model_ws), ideally support both? @@ -632,7 +635,8 @@ def test_mflist_external(tmpdir): "mflist_test", model_ws=str(tmpdir), # external_path=str(ws), # full path causes failure - external_path=ws.name) + external_path=ws.name, + ) dis = ModflowDis(ml, 1, 10, 10, nper=3, perlen=1.0) wel_data = { @@ -683,7 +687,9 @@ def test_mflist_external(tmpdir): # ml1.write_input() -@pytest.mark.xfail(reason="Pending https://github.com/modflowpy/flopy/issues/1472") +@pytest.mark.xfail( + reason="Pending https://github.com/modflowpy/flopy/issues/1472" +) def test_single_mflist_entry_load(tmpdir, example_data_path): m = Modflow.load( "freyberg.nam", @@ -734,11 +740,14 @@ def test_mflist_add_record(): __mf2005_test_path = get_example_data_path(Path(__file__)) / "mf2005_test" -@pytest.mark.parametrize("namfile", [ - os.path.join(__mf2005_test_path, f) - for f in os.listdir(__mf2005_test_path) - if f.endswith(".nam") -]) +@pytest.mark.parametrize( + "namfile", + [ + os.path.join(__mf2005_test_path, f) + for f in os.listdir(__mf2005_test_path) + if f.endswith(".nam") + ], +) def test_checker_on_load(namfile): # load all of the models in the mf2005_test folder # model level checks are performed by default on load() @@ -765,9 +774,9 @@ def test_bcs_check(tmpdir): chk = bas.check() assert chk.summary_array["desc"][0] == "isolated cells in ibound array" assert ( - chk.summary_array.i[0] == 1 - and chk.summary_array.i[0] == 1 - and chk.summary_array.j[0] == 1 + chk.summary_array.i[0] == 1 + and chk.summary_array.i[0] == 1 + and chk.summary_array.j[0] == 1 ) assert len(chk.summary_array) == 1 @@ -841,29 +850,29 @@ def test_properties_check(tmpdir): ind3_errors = chk.summary_array[ind3]["desc"] assert ( - "zero or negative horizontal hydraulic conductivity values" - in ind1_errors + "zero or negative horizontal hydraulic conductivity values" + in ind1_errors ) assert ( - "horizontal hydraulic conductivity values below checker threshold of 1e-11" - in ind1_errors + "horizontal hydraulic conductivity values below checker threshold of 1e-11" + in ind1_errors ) assert "negative horizontal anisotropy values" in ind1_errors assert ( - "vertical hydraulic conductivity values below checker threshold of 1e-11" - in ind1_errors + "vertical hydraulic conductivity values below checker threshold of 1e-11" + in ind1_errors ) assert ( - "horizontal hydraulic conductivity values above checker threshold of 100000.0" - in ind2_errors + "horizontal hydraulic conductivity values above checker threshold of 100000.0" + in ind2_errors ) assert ( - "zero or negative vertical hydraulic conductivity values" - in ind2_errors + "zero or negative vertical hydraulic conductivity values" + in ind2_errors ) assert ( - "vertical hydraulic conductivity values above checker threshold of 100000.0" - in ind3_errors + "vertical hydraulic conductivity values above checker threshold of 100000.0" + in ind3_errors ) @@ -1031,7 +1040,7 @@ def eval_timeseries(file): # get the all of the data tsd = ts.get_alldata() assert ( - len(tsd) > 0 + len(tsd) > 0 ), f"could not load data using get_alldata() from {os.path.basename(file)}." # get the data for the last particleid @@ -1266,7 +1275,7 @@ def get_basic_modflow_model(ws, name): size = 100 nlay = 10 nper = 10 - nsfr = int((size ** 2) / 5) + nsfr = int((size**2) / 5) dis = ModflowDis( m, @@ -1282,16 +1291,16 @@ def get_basic_modflow_model(ws, name): m, rech={k: 0.001 - np.cos(k) * 0.001 for k in range(nper)} ) - ra = ModflowWel.get_empty(size ** 2) + ra = ModflowWel.get_empty(size**2) well_spd = {} for kper in range(nper): ra_per = ra.copy() ra_per["k"] = 1 ra_per["i"] = ( (np.ones((size, size)) * np.arange(size)) - .transpose() - .ravel() - .astype(int) + .transpose() + .ravel() + .astype(int) ) ra_per["j"] = list(range(size)) * size well_spd[kper] = ra @@ -1326,4 +1335,6 @@ def test_model_load_time(tmpdir, benchmark): name = inspect.getframeinfo(inspect.currentframe()).function model = get_basic_modflow_model(ws=str(tmpdir), name=name) model.write_input() - benchmark(lambda: Modflow.load(f"{name}.nam", model_ws=str(tmpdir), check=False)) \ No newline at end of file + benchmark( + lambda: Modflow.load(f"{name}.nam", model_ws=str(tmpdir), check=False) + ) diff --git a/autotest/test_modpathfile.py b/autotest/test_modpathfile.py index b454bd8825..767d105291 100644 --- a/autotest/test_modpathfile.py +++ b/autotest/test_modpathfile.py @@ -5,16 +5,49 @@ import numpy as np import pytest - -from flopy.mf6 import MFSimulation, ModflowTdis, ModflowGwf, ModflowIms, ModflowGwfic, ModflowGwfdis, ModflowGwfnpf, ModflowGwfrcha, ModflowGwfwel, \ - ModflowGwfriv, ModflowGwfoc -from flopy.modpath import Modpath7 -from flopy.utils import PathlineFile, EndpointFile - from autotest.conftest import requires_exe - -def __create_simulation(ws, name, nrow, ncol, perlen, nstp, tsmult, nper, nlay, delr, delc, top, botm, laytyp, kh, kv, rch, wel_loc, wel_q, riv_h, riv_c, riv_z): +from flopy.mf6 import ( + MFSimulation, + ModflowGwf, + ModflowGwfdis, + ModflowGwfic, + ModflowGwfnpf, + ModflowGwfoc, + ModflowGwfrcha, + ModflowGwfriv, + ModflowGwfwel, + ModflowIms, + ModflowTdis, +) +from flopy.modpath import Modpath7 +from flopy.utils import EndpointFile, PathlineFile + + +def __create_simulation( + ws, + name, + nrow, + ncol, + perlen, + nstp, + tsmult, + nper, + nlay, + delr, + delc, + top, + botm, + laytyp, + kh, + kv, + rch, + wel_loc, + wel_q, + riv_h, + riv_c, + riv_z, +): def get_nodes(locs): nodes = [] for k, i, j in locs: @@ -22,9 +55,7 @@ def get_nodes(locs): return nodes # Create the Flopy simulation object - sim = MFSimulation( - sim_name=name, exe_name="mf6", version="mf6", sim_ws=ws - ) + sim = MFSimulation(sim_name=name, exe_name="mf6", version="mf6", sim_ws=ws) # Create the Flopy temporal discretization object pd = (perlen, nstp, tsmult) @@ -65,17 +96,13 @@ def get_nodes(locs): ic = ModflowGwfic(gwf, pname="ic", strt=top) # Create the node property flow package - npf = ModflowGwfnpf( - gwf, pname="npf", icelltype=laytyp, k=kh, k33=kv - ) + npf = ModflowGwfnpf(gwf, pname="npf", icelltype=laytyp, k=kh, k33=kv) # recharge ModflowGwfrcha(gwf, recharge=rch) # wel wd = [(wel_loc, wel_q)] - ModflowGwfwel( - gwf, maxbound=1, stress_period_data={0: wd} - ) + ModflowGwfwel(gwf, maxbound=1, stress_period_data={0: wd}) # river rd = [] @@ -174,7 +201,8 @@ def mp7_small(module_tmpdir): rch=0.005, riv_h=320.0, riv_z=317.0, - riv_c=1.0e5) + riv_c=1.0e5, + ) @pytest.fixture(scope="module") @@ -201,7 +229,8 @@ def mp7_large(module_tmpdir): rch=0.005, riv_h=320.0, riv_z=317.0, - riv_c=1.0e5) + riv_c=1.0e5, + ) def test_pathline_file_sorts_in_ctor(tmpdir, module_tmpdir, mp7_small): @@ -215,14 +244,19 @@ def test_pathline_file_sorts_in_ctor(tmpdir, module_tmpdir, mp7_small): assert forward_path.is_file() pathline_file = PathlineFile(str(forward_path)) - assert np.all(pathline_file._data[:-1]['particleid'] <= pathline_file._data[1:]['particleid']) + assert np.all( + pathline_file._data[:-1]["particleid"] + <= pathline_file._data[1:]["particleid"] + ) @requires_exe("mf6", "mp7") @pytest.mark.slow @pytest.mark.parametrize("direction", ["forward", "backward"]) @pytest.mark.parametrize("locations", ["well", "river"]) -def test_get_destination_pathline_data(tmpdir, mp7_large, direction, locations, benchmark): +def test_get_destination_pathline_data( + tmpdir, mp7_large, direction, locations, benchmark +): sim, forward_model_name, backward_model_name, nodew, nodesr = mp7_large ws = tmpdir / "ws" @@ -233,15 +267,23 @@ def test_get_destination_pathline_data(tmpdir, mp7_large, direction, locations, assert forward_path.is_file() assert backward_path.is_file() - pathline_file = PathlineFile(str(backward_path) if direction == "backward" else str(forward_path)) - benchmark(lambda: pathline_file.get_destination_pathline_data(dest_cells=nodew if locations == "well" else nodesr)) + pathline_file = PathlineFile( + str(backward_path) if direction == "backward" else str(forward_path) + ) + benchmark( + lambda: pathline_file.get_destination_pathline_data( + dest_cells=nodew if locations == "well" else nodesr + ) + ) @requires_exe("mf6", "mp7") @pytest.mark.slow @pytest.mark.parametrize("direction", ["forward", "backward"]) @pytest.mark.parametrize("locations", ["well", "river"]) -def test_get_destination_endpoint_data(tmpdir, mp7_large, direction, locations, benchmark): +def test_get_destination_endpoint_data( + tmpdir, mp7_large, direction, locations, benchmark +): sim, forward_model_name, backward_model_name, nodew, nodesr = mp7_large ws = tmpdir / "ws" @@ -252,5 +294,11 @@ def test_get_destination_endpoint_data(tmpdir, mp7_large, direction, locations, assert forward_end.is_file() assert backward_end.is_file() - endpoint_file = EndpointFile(str(backward_end) if direction == "backward" else str(forward_end)) - benchmark(lambda: endpoint_file.get_destination_endpoint_data(dest_cells=nodew if locations == "well" else nodesr)) + endpoint_file = EndpointFile( + str(backward_end) if direction == "backward" else str(forward_end) + ) + benchmark( + lambda: endpoint_file.get_destination_endpoint_data( + dest_cells=nodew if locations == "well" else nodesr + ) + ) diff --git a/autotest/test_mt3d.py b/autotest/test_mt3d.py index f486f56b1e..170a4b7d1b 100644 --- a/autotest/test_mt3d.py +++ b/autotest/test_mt3d.py @@ -3,12 +3,9 @@ import numpy as np import pytest +from autotest.conftest import excludes_platform, requires_exe from flaky import flaky -from autotest.conftest import ( - excludes_platform, - requires_exe, -) from flopy.modflow import ( Modflow, ModflowBas, @@ -37,6 +34,7 @@ Mt3dSsm, Mt3dTob, ) + # Test loading of MODFLOW and MT3D models that come with MT3D distribution from flopy.utils import UcnFile @@ -300,7 +298,9 @@ def test_mf2000_zeroth(tmpdir, mf2kmt3d_model_path): @flaky(max_runs=3) @requires_exe("mfnwt", "mt3dms") -@excludes_platform("Windows", ci_only=True) # TODO remove once fixed in MT3D-USGS +@excludes_platform( + "Windows", ci_only=True +) # TODO remove once fixed in MT3D-USGS def test_mfnwt_CrnkNic(tmpdir, mfnwtmt3d_model_path): pth = str(mfnwtmt3d_model_path / "sft_crnkNic") namefile = "CrnkNic.nam" diff --git a/autotest/test_obs.py b/autotest/test_obs.py index e632b16905..2a93b5053c 100644 --- a/autotest/test_obs.py +++ b/autotest/test_obs.py @@ -3,7 +3,6 @@ import numpy as np import pytest - from autotest.conftest import requires_exe from flopy.modflow import ( diff --git a/autotest/test_plot.py b/autotest/test_plot.py index ba0cdccb1e..81ecadc668 100644 --- a/autotest/test_plot.py +++ b/autotest/test_plot.py @@ -1,6 +1,9 @@ import os import numpy as np +import pytest +from autotest.conftest import requires_pkg +from flaky import flaky from matplotlib import pyplot as plt from matplotlib import rcParams from matplotlib.collections import ( @@ -9,10 +12,6 @@ PathCollection, QuadMesh, ) -import pytest -from flaky import flaky - -from autotest.conftest import requires_pkg import flopy from flopy.discretization import StructuredGrid @@ -91,7 +90,9 @@ def test_map_view_boundary_conditions(example_data_path): raise AssertionError("Boundary condition was not drawn") for col in ax.collections: - assert isinstance(col, (QuadMesh, PathCollection)), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, (QuadMesh, PathCollection) + ), f"Unexpected collection type: {type(col)}" plt.close() mpath = example_data_path / "mf6" / "test045_lake2tr" @@ -106,7 +107,9 @@ def test_map_view_boundary_conditions(example_data_path): raise AssertionError("Boundary condition was not drawn") for col in ax.collections: - assert isinstance(col, (QuadMesh, PathCollection)), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, (QuadMesh, PathCollection) + ), f"Unexpected collection type: {type(col)}" plt.close() mpath = example_data_path / "mf6" / "test006_2models_mvr" @@ -128,7 +131,9 @@ def test_map_view_boundary_conditions(example_data_path): assert len(ax.collections) > 0, "Boundary condition was not drawn" for col in ax.collections: - assert isinstance(col, (QuadMesh, PathCollection)), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, (QuadMesh, PathCollection) + ), f"Unexpected collection type: {type(col)}" plt.close() mpath = example_data_path / "mf6" / "test001e_UZF_3lay" @@ -142,11 +147,15 @@ def test_map_view_boundary_conditions(example_data_path): raise AssertionError("Boundary condition was not drawn") for col in ax.collections: - assert isinstance(col, (QuadMesh, PathCollection)), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, (QuadMesh, PathCollection) + ), f"Unexpected collection type: {type(col)}" plt.close() -@pytest.mark.xfail(reason="sometimes get LineCollections instead of PatchCollections") +@pytest.mark.xfail( + reason="sometimes get LineCollections instead of PatchCollections" +) def test_cross_section_boundary_conditions(example_data_path): mpath = example_data_path / "mf6" / "test003_gwfs_disv" sim = MFSimulation.load(sim_ws=str(mpath)) @@ -158,7 +167,9 @@ def test_cross_section_boundary_conditions(example_data_path): assert len(ax.collections) != 0, "Boundary condition was not drawn" for col in ax.collections: - assert isinstance(col, PatchCollection), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, PatchCollection + ), f"Unexpected collection type: {type(col)}" plt.close() mpath = example_data_path / "mf6" / "test045_lake2tr" @@ -172,7 +183,9 @@ def test_cross_section_boundary_conditions(example_data_path): assert len(ax.collections) != 0, "Boundary condition was not drawn" for col in ax.collections: - assert isinstance(col, PatchCollection), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, PatchCollection + ), f"Unexpected collection type: {type(col)}" plt.close() mpath = example_data_path / "mf6" / "test006_2models_mvr" @@ -185,7 +198,9 @@ def test_cross_section_boundary_conditions(example_data_path): assert len(ax.collections) > 0, "Boundary condition was not drawn" for col in ax.collections: - assert isinstance(col, PatchCollection), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, PatchCollection + ), f"Unexpected collection type: {type(col)}" plt.close() mpath = example_data_path / "mf6" / "test001e_UZF_3lay" @@ -199,7 +214,9 @@ def test_cross_section_boundary_conditions(example_data_path): assert len(ax.collections) != 0, "Boundary condition was not drawn" for col in ax.collections: - assert isinstance(col, PatchCollection), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, PatchCollection + ), f"Unexpected collection type: {type(col)}" plt.close() diff --git a/autotest/test_scripts.py b/autotest/test_scripts.py index 7d74031f7e..2997f17f93 100644 --- a/autotest/test_scripts.py +++ b/autotest/test_scripts.py @@ -4,14 +4,14 @@ from urllib.error import HTTPError import pytest -from flaky import flaky - -from flopy.utils import get_modflow_main from autotest.conftest import ( get_project_root_path, requires_github, run_py_script, ) +from flaky import flaky + +from flopy.utils import get_modflow_main flopy_dir = get_project_root_path(__file__) get_modflow_script = flopy_dir / "flopy" / "utils" / "get_modflow.py" @@ -45,7 +45,8 @@ def test_get_modflow_script(tmp_path, downloads_dir): bindir = tmp_path / "bin1" assert not bindir.exists() stdout, stderr, returncode = run_get_modflow_script(bindir) - if rate_limit_msg in stderr: pytest.skip(f"GitHub {rate_limit_msg}") + if rate_limit_msg in stderr: + pytest.skip(f"GitHub {rate_limit_msg}") assert "does not exist" in stderr assert returncode == 1 @@ -57,7 +58,8 @@ def test_get_modflow_script(tmp_path, downloads_dir): stdout, stderr, returncode = run_get_modflow_script( bindir, "--release-id", "1.9", "--downloads-dir", downloads_dir ) - if rate_limit_msg in stderr: pytest.skip(f"GitHub {rate_limit_msg}") + if rate_limit_msg in stderr: + pytest.skip(f"GitHub {rate_limit_msg}") assert "Release '1.9' not found" in stderr assert returncode == 1 @@ -65,7 +67,8 @@ def test_get_modflow_script(tmp_path, downloads_dir): stdout, stderr, returncode = run_get_modflow_script( bindir, "--downloads-dir", downloads_dir ) - if rate_limit_msg in stderr: pytest.skip(f"GitHub {rate_limit_msg}") + if rate_limit_msg in stderr: + pytest.skip(f"GitHub {rate_limit_msg}") assert len(stderr) == returncode == 0 files = [item.name for item in bindir.iterdir() if item.is_file()] assert len(files) > 20 @@ -76,14 +79,16 @@ def test_get_modflow_script(tmp_path, downloads_dir): stdout, stderr, returncode = run_get_modflow_script( bindir, "--subset", "mfnwt,mpx", "--downloads-dir", downloads_dir ) - if rate_limit_msg in stderr: pytest.skip(f"GitHub {rate_limit_msg}") + if rate_limit_msg in stderr: + pytest.skip(f"GitHub {rate_limit_msg}") assert "subset item not found: mpx" in stderr assert returncode == 1 # now valid subset stdout, stderr, returncode = run_get_modflow_script( bindir, "--subset", "mfnwt,mp6", "--downloads-dir", downloads_dir ) - if rate_limit_msg in stderr: pytest.skip(f"GitHub {rate_limit_msg}") + if rate_limit_msg in stderr: + pytest.skip(f"GitHub {rate_limit_msg}") assert len(stderr) == returncode == 0 files = [item.stem for item in bindir.iterdir() if item.is_file()] assert sorted(files) == ["mfnwt", "mfnwtdbl", "mp6"] @@ -103,7 +108,8 @@ def test_get_modflow_script(tmp_path, downloads_dir): "--downloads-dir", downloads_dir, ) - if rate_limit_msg in stderr: pytest.skip(f"GitHub {rate_limit_msg}") + if rate_limit_msg in stderr: + pytest.skip(f"GitHub {rate_limit_msg}") assert len(stderr) == returncode == 0 files = [item.name for item in bindir.iterdir() if item.is_file()] assert sorted(files) == ["mfnwt.exe", "mfnwtdbl.exe"] @@ -122,7 +128,8 @@ def test_get_nightly_script(tmp_path, downloads_dir): "--downloads-dir", downloads_dir, ) - if rate_limit_msg in stderr: pytest.skip(f"GitHub {rate_limit_msg}") + if rate_limit_msg in stderr: + pytest.skip(f"GitHub {rate_limit_msg}") assert len(stderr) == returncode == 0 files = [item.name for item in bindir.iterdir() if item.is_file()] assert len(files) >= 4 diff --git a/autotest/test_seawat.py b/autotest/test_seawat.py index cd64f91ccd..8494c6c25c 100644 --- a/autotest/test_seawat.py +++ b/autotest/test_seawat.py @@ -195,7 +195,10 @@ def test_seawat2_henry(tmpdir): def swt4_namfiles(): return [ - str(p) for p in (get_example_data_path(__file__) / "swtv4_test").rglob("*.nam") + str(p) + for p in (get_example_data_path(__file__) / "swtv4_test").rglob( + "*.nam" + ) ] diff --git a/autotest/test_sfr.py b/autotest/test_sfr.py index f3cad7fbbd..f8ae54cc90 100644 --- a/autotest/test_sfr.py +++ b/autotest/test_sfr.py @@ -8,8 +8,8 @@ import matplotlib.pyplot as plt import numpy as np import pytest - from autotest.conftest import get_example_data_path, requires_exe, requires_pkg + from flopy.discretization import StructuredGrid from flopy.modflow import Modflow, ModflowDis, ModflowSfr2, ModflowStr from flopy.modflow.mfsfr2 import check diff --git a/autotest/test_specific_discharge.py b/autotest/test_specific_discharge.py index 8f97c1bf64..ba340ca720 100644 --- a/autotest/test_specific_discharge.py +++ b/autotest/test_specific_discharge.py @@ -485,7 +485,9 @@ def specific_discharge_comprehensive(tmpdir): plt.close() -@pytest.mark.xfail(reason="occasional Unexpected collection type: ") +@pytest.mark.xfail( + reason="occasional Unexpected collection type: " +) def test_specific_discharge_mf6(mf6_model): # build and run MODFLOW 6 model sim, tmpdir = mf6_model @@ -519,7 +521,9 @@ def test_specific_discharge_mf6(mf6_model): ax = modelmap.ax assert len(ax.collections) != 0, "Discharge vector was not drawn" for col in ax.collections: - assert isinstance(col, Quiver), f"Unexpected collection type: {type(col)}" + assert isinstance( + col, Quiver + ), f"Unexpected collection type: {type(col)}" assert np.sum(quiver.Umask) == 1 pos = np.sum(quiver.X) + np.sum(quiver.Y) assert np.allclose(pos, 1600.0) diff --git a/autotest/test_str.py b/autotest/test_str.py index 5cafdc0a07..3e90baf24d 100644 --- a/autotest/test_str.py +++ b/autotest/test_str.py @@ -1,11 +1,9 @@ import matplotlib - from autotest.conftest import requires_exe, requires_pkg from flopy.modflow import Modflow from flopy.utils import MfListBudget - str_items = { 0: { "mfnam": "str.nam", diff --git a/autotest/test_subwt.py b/autotest/test_subwt.py index 9fea729518..9aeeab0639 100644 --- a/autotest/test_subwt.py +++ b/autotest/test_subwt.py @@ -3,9 +3,9 @@ import numpy as np import pytest +from autotest.conftest import requires_exe from matplotlib import pyplot as plt -from autotest.conftest import requires_exe from flopy.modflow import ( Modflow, ModflowBas, diff --git a/autotest/test_swr_binaryread.py b/autotest/test_swr_binaryread.py index 34a284a18f..844ea45842 100644 --- a/autotest/test_swr_binaryread.py +++ b/autotest/test_swr_binaryread.py @@ -1,6 +1,5 @@ # Test SWR binary read functionality import pytest - from autotest.conftest import has_pkg from flopy.utils import ( diff --git a/autotest/test_usg.py b/autotest/test_usg.py index 076f7abf5d..3d1b9f4cad 100644 --- a/autotest/test_usg.py +++ b/autotest/test_usg.py @@ -361,7 +361,15 @@ def test_flat_array_to_util3d_usg(tmpdir, freyberg_usg_model_path): @requires_exe("mfusg") @pytest.mark.slow -@pytest.mark.parametrize("fpth", [str(p) for p in (get_example_data_path(Path(__file__)) / "mfusg_test").rglob('*.nam')]) +@pytest.mark.parametrize( + "fpth", + [ + str(p) + for p in (get_example_data_path(Path(__file__)) / "mfusg_test").rglob( + "*.nam" + ) + ], +) def test_load_usg(tmpdir, fpth): namfile = Path(fpth) diff --git a/autotest/test_util_2d_and_3d.py b/autotest/test_util_2d_and_3d.py index 5087b653fb..c24003b486 100644 --- a/autotest/test_util_2d_and_3d.py +++ b/autotest/test_util_2d_and_3d.py @@ -2,7 +2,6 @@ import numpy as np import pytest - from autotest.conftest import requires_pkg from flopy.modflow import ( diff --git a/autotest/test_uzf.py b/autotest/test_uzf.py index 06230a9211..4d47f4d3c2 100644 --- a/autotest/test_uzf.py +++ b/autotest/test_uzf.py @@ -4,7 +4,6 @@ import numpy as np import pytest - from autotest.conftest import requires_exe from flopy.modflow import ( diff --git a/autotest/test_zonbud_utility.py b/autotest/test_zonbud_utility.py index f34e0aa3f5..d57ae84a8f 100644 --- a/autotest/test_zonbud_utility.py +++ b/autotest/test_zonbud_utility.py @@ -2,7 +2,6 @@ import numpy as np import pytest - from autotest.conftest import requires_pkg from flopy.mf6 import MFSimulation diff --git a/etc/environment.yml b/etc/environment.yml index 49b5319d70..ec7b02c306 100644 --- a/etc/environment.yml +++ b/etc/environment.yml @@ -20,6 +20,7 @@ dependencies: - jupyter - jupytext - pytest + - pytest-cases - pytest-cov - pytest-xdist - pytest-benchmark diff --git a/examples/Tutorials/modflow6output/tutorial01_mf6_output.py b/examples/Tutorials/modflow6output/tutorial01_mf6_output.py index b9b130beaa..f811e531e2 100644 --- a/examples/Tutorials/modflow6output/tutorial01_mf6_output.py +++ b/examples/Tutorials/modflow6output/tutorial01_mf6_output.py @@ -53,13 +53,17 @@ def get_project_root_path(path=None): if cwd.name == "autotest": # we're in top-level autotest folder return cwd.parent - elif "autotest" in cwd.parts and cwd.parts.index("autotest") > cwd.parts.index("flopy"): + elif "autotest" in cwd.parts and cwd.parts.index( + "autotest" + ) > cwd.parts.index("flopy"): # we're somewhere inside autotests - parts = cwd.parts[0: cwd.parts.index("autotest")] + parts = cwd.parts[0 : cwd.parts.index("autotest")] return Path(*parts) - elif "examples" in cwd.parts and cwd.parts.index("examples") > cwd.parts.index("flopy"): + elif "examples" in cwd.parts and cwd.parts.index( + "examples" + ) > cwd.parts.index("flopy"): # we're somewhere inside examples folder - parts = cwd.parts[0: cwd.parts.index("examples")] + parts = cwd.parts[0 : cwd.parts.index("examples")] return Path(*parts) elif cwd.parts.count("flopy") >= 2: # we're somewhere inside the project or flopy module @@ -67,11 +71,11 @@ def get_project_root_path(path=None): if "CI" in os.environ: tries.append(2) for t in tries: - parts = cwd.parts[0: cwd.parts.index("flopy") + (t)] + parts = cwd.parts[0 : cwd.parts.index("flopy") + (t)] pth = Path(*parts) if ( - next(iter([p for p in pth.glob("setup.cfg")]), None) - is not None + next(iter([p for p in pth.glob("setup.cfg")]), None) + is not None ): return pth raise Exception( @@ -89,7 +93,9 @@ def get_project_root_path(path=None): ws = os.path.abspath(os.path.dirname("")) -sim_ws = str(get_project_root_path() / "examples" / "data" / "mf6" / "test001e_UZF_3lay") +sim_ws = str( + get_project_root_path() / "examples" / "data" / "mf6" / "test001e_UZF_3lay" +) # load the model sim = flopy.mf6.MFSimulation.load( diff --git a/examples/common/notebook_utils.py b/examples/common/notebook_utils.py index 1aab5dfba0..355a1c5c91 100644 --- a/examples/common/notebook_utils.py +++ b/examples/common/notebook_utils.py @@ -10,9 +10,9 @@ sys.path.append(fpth) import flopy - from pathlib import Path + def get_project_root_path(path=None): """ Infers the path to the project root given the path to the current working directory. @@ -31,13 +31,17 @@ def get_project_root_path(path=None): if cwd.name == "autotest": # we're in top-level autotest folder return cwd.parent - elif "autotest" in cwd.parts and cwd.parts.index("autotest") > cwd.parts.index("flopy"): + elif "autotest" in cwd.parts and cwd.parts.index( + "autotest" + ) > cwd.parts.index("flopy"): # we're somewhere inside autotests - parts = cwd.parts[0: cwd.parts.index("autotest")] + parts = cwd.parts[0 : cwd.parts.index("autotest")] return Path(*parts) - elif "examples" in cwd.parts and cwd.parts.index("examples") > cwd.parts.index("flopy"): + elif "examples" in cwd.parts and cwd.parts.index( + "examples" + ) > cwd.parts.index("flopy"): # we're somewhere inside examples folder - parts = cwd.parts[0: cwd.parts.index("examples")] + parts = cwd.parts[0 : cwd.parts.index("examples")] return Path(*parts) elif cwd.parts.count("flopy") >= 2: # we're somewhere inside the project or flopy module diff --git a/examples/scripts/flopy_henry.py b/examples/scripts/flopy_henry.py index 94fe6337f6..46744bebd7 100644 --- a/examples/scripts/flopy_henry.py +++ b/examples/scripts/flopy_henry.py @@ -145,10 +145,10 @@ def run(workspace, quiet): # Average flows to cell centers qx_avg = np.empty(qx.shape, dtype=qx.dtype) - qx_avg[:, :, 1:] = 0.5 * (qx[:, :, 0: ncol - 1] + qx[:, :, 1:ncol]) + qx_avg[:, :, 1:] = 0.5 * (qx[:, :, 0 : ncol - 1] + qx[:, :, 1:ncol]) qx_avg[:, :, 0] = 0.5 * qx[:, :, 0] qz_avg = np.empty(qz.shape, dtype=qz.dtype) - qz_avg[1:, :, :] = 0.5 * (qz[0: nlay - 1, :, :] + qz[1:nlay, :, :]) + qz_avg[1:, :, :] = 0.5 * (qz[0 : nlay - 1, :, :] + qz[1:nlay, :, :]) qz_avg[0, :, :] = 0.5 * qz[0, :, :] # Make the plot @@ -202,11 +202,13 @@ def run(workspace, quiet): parser = argparse.ArgumentParser() parser.add_argument("--keep", help="output directory") - parser.add_argument("--quiet", action="store_false", help="don't show model output") + parser.add_argument( + "--quiet", action="store_false", help="don't show model output" + ) args = vars(parser.parse_args()) - workspace = args.get('keep', None) - quiet = args.get('quiet', False) + workspace = args.get("keep", None) + quiet = args.get("quiet", False) if workspace is not None: run(workspace, quiet) diff --git a/examples/scripts/flopy_lake_example.py b/examples/scripts/flopy_lake_example.py index ea8df93c72..235f2e94db 100644 --- a/examples/scripts/flopy_lake_example.py +++ b/examples/scripts/flopy_lake_example.py @@ -144,11 +144,13 @@ def run(workspace, quiet): parser = argparse.ArgumentParser() parser.add_argument("--keep", help="output directory") - parser.add_argument("--quiet", action="store_false", help="don't show model output") + parser.add_argument( + "--quiet", action="store_false", help="don't show model output" + ) args = vars(parser.parse_args()) - workspace = args.get('keep', None) - quiet = args.get('quiet', False) + workspace = args.get("keep", None) + quiet = args.get("quiet", False) if workspace is not None: run(workspace, quiet) diff --git a/examples/scripts/flopy_swi2_ex1.py b/examples/scripts/flopy_swi2_ex1.py index 4f995f91a9..6261c2b494 100644 --- a/examples/scripts/flopy_swi2_ex1.py +++ b/examples/scripts/flopy_swi2_ex1.py @@ -1,8 +1,7 @@ -from tempfile import TemporaryDirectory - import math import os import sys +from tempfile import TemporaryDirectory import matplotlib.pyplot as plt import numpy as np @@ -270,11 +269,13 @@ def run(workspace, quiet): parser = argparse.ArgumentParser() parser.add_argument("--keep", help="output directory") - parser.add_argument("--quiet", action="store_false", help="don't show model output") + parser.add_argument( + "--quiet", action="store_false", help="don't show model output" + ) args = vars(parser.parse_args()) - workspace = args.get('keep', None) - quiet = args.get('quiet', False) + workspace = args.get("keep", None) + quiet = args.get("quiet", False) if workspace is not None: run(workspace, quiet) diff --git a/examples/scripts/flopy_swi2_ex2.py b/examples/scripts/flopy_swi2_ex2.py index 76bae4513e..d87976be24 100644 --- a/examples/scripts/flopy_swi2_ex2.py +++ b/examples/scripts/flopy_swi2_ex2.py @@ -478,11 +478,13 @@ def run(workspace, quiet): parser = argparse.ArgumentParser() parser.add_argument("--keep", help="output directory") - parser.add_argument("--quiet", action="store_false", help="don't show model output") + parser.add_argument( + "--quiet", action="store_false", help="don't show model output" + ) args = vars(parser.parse_args()) - workspace = args.get('keep', None) - quiet = args.get('quiet', False) + workspace = args.get("keep", None) + quiet = args.get("quiet", False) if workspace is not None: run(workspace, quiet) diff --git a/examples/scripts/flopy_swi2_ex3.py b/examples/scripts/flopy_swi2_ex3.py index d3bc88356d..2b1d4c8b5f 100644 --- a/examples/scripts/flopy_swi2_ex3.py +++ b/examples/scripts/flopy_swi2_ex3.py @@ -316,11 +316,13 @@ def run(workspace, quiet): parser = argparse.ArgumentParser() parser.add_argument("--keep", help="output directory") - parser.add_argument("--quiet", action="store_false", help="don't show model output") + parser.add_argument( + "--quiet", action="store_false", help="don't show model output" + ) args = vars(parser.parse_args()) - workspace = args.get('keep', None) - quiet = args.get('quiet', False) + workspace = args.get("keep", None) + quiet = args.get("quiet", False) if workspace is not None: run(workspace, quiet) diff --git a/examples/scripts/flopy_swi2_ex4.py b/examples/scripts/flopy_swi2_ex4.py index 1a2f91ea04..516493b8b8 100644 --- a/examples/scripts/flopy_swi2_ex4.py +++ b/examples/scripts/flopy_swi2_ex4.py @@ -588,11 +588,13 @@ def run(workspace, quiet): parser = argparse.ArgumentParser() parser.add_argument("--keep", help="output directory") - parser.add_argument("--quiet", action="store_false", help="don't show model output") + parser.add_argument( + "--quiet", action="store_false", help="don't show model output" + ) args = vars(parser.parse_args()) - workspace = args.get('keep', None) - quiet = args.get('quiet', False) + workspace = args.get("keep", None) + quiet = args.get("quiet", False) if workspace is not None: run(workspace, quiet) diff --git a/examples/scripts/flopy_swi2_ex5.py b/examples/scripts/flopy_swi2_ex5.py index 4a307deb4b..86105c5a4f 100644 --- a/examples/scripts/flopy_swi2_ex5.py +++ b/examples/scripts/flopy_swi2_ex5.py @@ -1,8 +1,7 @@ -from tempfile import TemporaryDirectory - import math import os import sys +from tempfile import TemporaryDirectory import matplotlib.pyplot as plt import numpy as np @@ -629,11 +628,13 @@ def run(workspace, quiet): parser = argparse.ArgumentParser() parser.add_argument("--keep", help="output directory") - parser.add_argument("--quiet", action="store_false", help="don't show model output") + parser.add_argument( + "--quiet", action="store_false", help="don't show model output" + ) args = vars(parser.parse_args()) - workspace = args.get('keep', None) - quiet = args.get('quiet', False) + workspace = args.get("keep", None) + quiet = args.get("quiet", False) if workspace is not None: run(workspace, quiet) diff --git a/flopy/discretization/grid.py b/flopy/discretization/grid.py index de66e32b06..3d9131cf65 100644 --- a/flopy/discretization/grid.py +++ b/flopy/discretization/grid.py @@ -1,10 +1,10 @@ import copy import os -import warnings import numpy as np from ..utils import geometry +from ..utils.gridutil import get_lni class CachedData: @@ -360,6 +360,10 @@ def idomain(self): def idomain(self, idomain): self._idomain = idomain + @property + def nlay(self): + raise NotImplementedError("must define nlay in child class") + @property def ncpl(self): raise NotImplementedError("must define ncpl in child class") @@ -603,6 +607,22 @@ def cross_section_set_contour_arrays( def map_polygons(self): raise NotImplementedError("must define map_polygons in child class") + def get_lni(self, *nodes): + """ + Get the layer index and within-layer node index (both 0-based). + if no nodes are specified, all are returned in ascending order. + + Parameters + ---------- + nodes : the node numbers (zero or more ints) + + Returns + ------- + A tuple (layer index, node index), or a + list of such if multiple nodes provided + """ + return get_lni([self.ncpl for _ in range(self.nlay)], *nodes) + def get_plottable_layer_array(self, plotarray, layer): raise NotImplementedError( "must define get_plottable_layer_array in child class" diff --git a/flopy/discretization/unstructuredgrid.py b/flopy/discretization/unstructuredgrid.py index 329718f2c3..390cc52c41 100644 --- a/flopy/discretization/unstructuredgrid.py +++ b/flopy/discretization/unstructuredgrid.py @@ -6,6 +6,7 @@ from matplotlib.path import Path from ..utils.geometry import is_clockwise +from ..utils.gridutil import get_lni from .grid import CachedData, Grid @@ -675,6 +676,29 @@ def get_layer_node_range(self, layer): node_layer_range = [0] + list(np.add.accumulate(self.ncpl)) return node_layer_range[layer], node_layer_range[layer + 1] + def get_lni(self, *nodes): + """ + Get the layer index and within-layer node index (both 0-based). + if no nodes are specified, all are returned in ascending order. + + Parameters + ---------- + nodes : the node numbers (zero or more ints) + + Returns + ------- + A tuple (layer index, node index), or a + list of such if multiple nodes provided + """ + + ncpl = ( + [self.ncpl for _ in range(self.nlay)] + if isinstance(self.ncpl, int) + else list(self.ncpl) + ) + + return get_lni(ncpl, *nodes) + def get_xvertices_for_layer(self, layer): xgrid = np.array(self.xvertices, dtype=object) if self.grid_varies_by_layer: diff --git a/flopy/utils/binaryfile.py b/flopy/utils/binaryfile.py index 75c43fe13d..50bdefa109 100644 --- a/flopy/utils/binaryfile.py +++ b/flopy/utils/binaryfile.py @@ -13,6 +13,7 @@ import numpy as np from ..utils.datafile import Header, LayerFile +from .gridutil import get_lni class BinaryHeader(Header): @@ -1892,7 +1893,6 @@ def __init__( bintype="Head", precision=precision ) super().__init__(filename, precision, verbose, kwargs) - return def _get_data_array(self, totim=0.0): """ @@ -1963,41 +1963,27 @@ def get_ts(self, idx): """ times = self.get_times() - - # find node number in layer that node is in data = self.get_data(totim=times[0]) - nodelay = [len(data[lay]) for lay in range(len(data))] - nodelay_cumsum = np.cumsum([0] + nodelay) + layers = len(data) + ncpl = [len(data[l]) for l in range(layers)] + result = [] if isinstance(idx, int): - layer = np.searchsorted(nodelay_cumsum, idx) - nnode = idx - nodelay_cumsum[layer - 1] - - result = [] + layer, nn = get_lni(ncpl, idx) for i, time in enumerate(times): data = self.get_data(totim=time) - result.append([time, data[layer - 1][nnode]]) - - elif isinstance(idx, list): - - result = [] + value = data[layer][nn] + result.append([time, value]) + elif isinstance(idx, list) and all(isinstance(x, int) for x in idx): for i, time in enumerate(times): data = self.get_data(totim=time) row = [time] - for node in idx: - if isinstance(node, int): - layer = np.searchsorted(nodelay_cumsum, node) - nnode = node - nodelay_cumsum[layer - 1] - row += [data[layer - 1][nnode]] - else: - errmsg = "idx must be an integer or a list of integers" - raise Exception(errmsg) - + layer, nn = get_lni(ncpl, node) + value = data[layer][nn] + row += [value] result.append(row) - else: - errmsg = "idx must be an integer or a list of integers" - raise Exception(errmsg) + raise ValueError("idx must be an integer or a list of integers") return np.array(result) diff --git a/flopy/utils/gridutil.py b/flopy/utils/gridutil.py new file mode 100644 index 0000000000..7eb46742f2 --- /dev/null +++ b/flopy/utils/gridutil.py @@ -0,0 +1,41 @@ +""" +Grid utilities +""" +from typing import Iterable, List, Tuple, Union + +import numpy as np + + +def get_lni( + ncpl: Union[int, Iterable[int]], *nodes +) -> Union[Tuple[int, int], List[Tuple[int, int]]]: + """ + Get the layer index and within-layer node index (both 0-based) + given node count per layer and node number (grid-scoped index). + if no nodes are specified, all are returned in ascending order. + + Parameters + ---------- + ncpl: node count per layer (int or list of ints) + nodes : node numbers (zero or more ints) + + Returns + ------- + A tuple (layer index, node index), or a + list of such if multiple nodes provided + """ + + v = [] + counts = [ncpl] if isinstance(ncpl, int) else list(ncpl) + + for nn in nodes if nodes else range(sum(ncpl)): + csum = np.cumsum([0] + counts) + layer = max(0, np.searchsorted(csum, nn) - 1) + nidx = nn - sum([counts[l] for l in range(0, layer)]) + + # np.searchsorted assigns the first index of each layer + # to the previous layer in layer 2+, so correct for it + correct = layer + 1 < len(csum) and nidx == counts[layer] + v.append((layer + 1, 0) if correct else (layer, nidx)) + + return v if len(v) > 1 else v[0] diff --git a/scripts/process_benchmarks.py b/scripts/process_benchmarks.py index 3225fa104c..467b250503 100644 --- a/scripts/process_benchmarks.py +++ b/scripts/process_benchmarks.py @@ -7,13 +7,13 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd +import seaborn as sns from matplotlib import cm from matplotlib.lines import Line2D -import seaborn as sns indir = Path(sys.argv[1]) outdir = Path(sys.argv[2]) -json_paths = list(Path(indir).rglob('*.json')) +json_paths = list(Path(indir).rglob("*.json")) print(f"Found {len(json_paths)} JSON files") # pprint([str(p) for p in json_paths]) @@ -24,32 +24,36 @@ def get_benchmarks(paths): num_benchmarks = 0 for path in paths: - with open(path, 'r') as file: + with open(path, "r") as file: jsn = json.load(file) - system = jsn['machine_info']['system'] - python = jsn['machine_info']['python_version'] - if len(python.split('.')) == 3: python = python.rpartition('.')[0] - tstamp = jsn['datetime'] - bmarks = jsn['benchmarks'] + system = jsn["machine_info"]["system"] + python = jsn["machine_info"]["python_version"] + if len(python.split(".")) == 3: + python = python.rpartition(".")[0] + tstamp = jsn["datetime"] + bmarks = jsn["benchmarks"] for benchmark in bmarks: num_benchmarks += 1 - fullname = benchmark['fullname'] + fullname = benchmark["fullname"] included = [ - 'min', + "min", # 'max', # 'median', - 'mean', + "mean", ] - for stat, value in benchmark['stats'].items(): - if stat not in included: continue - benchmarks.append({ - "system": system, - "python": python, - "time": tstamp, - "case": fullname, - "stat": stat, - "value": value, - }) + for stat, value in benchmark["stats"].items(): + if stat not in included: + continue + benchmarks.append( + { + "system": system, + "python": python, + "time": tstamp, + "case": fullname, + "stat": stat, + "value": value, + } + ) print("Found", num_benchmarks, "benchmarks") return benchmarks @@ -57,7 +61,7 @@ def get_benchmarks(paths): # create data frame and save to CSV benchmarks_df = pd.DataFrame(get_benchmarks(json_paths)) -benchmarks_df['time'] = pd.to_datetime(benchmarks_df['time']) +benchmarks_df["time"] = pd.to_datetime(benchmarks_df["time"]) benchmarks_df.to_csv(str(outdir / f"benchmarks.csv"), index=False) @@ -66,42 +70,66 @@ def matplotlib_plot(stats): fig, axs = plt.subplots(nstats, 1, sharex=True) # color-code according to python version - pythons = np.unique(benchmarks_df['python']) + pythons = np.unique(benchmarks_df["python"]) colors = dict(zip(pythons, cm.jet(np.linspace(0, 1, len(pythons))))) # markers according to system - systems = np.unique(benchmarks_df['system']) - markers = dict(zip(systems, ['x', 'o', 's'])) # osx, linux, windows - benchmarks_df['marker'] = benchmarks_df['system'].apply(lambda x: markers[x]) + systems = np.unique(benchmarks_df["system"]) + markers = dict(zip(systems, ["x", "o", "s"])) # osx, linux, windows + benchmarks_df["marker"] = benchmarks_df["system"].apply( + lambda x: markers[x] + ) for i, (stat_name, stat_group) in enumerate(stats): stat_df = pd.DataFrame(stat_group) ax = axs[i] if nstats > 1 else axs ax.set_title(stat_name) - ax.tick_params(axis='x', rotation=45) + ax.tick_params(axis="x", rotation=45) ax.xaxis.set_major_locator(dates.DayLocator(interval=1)) - ax.xaxis.set_major_formatter(dates.DateFormatter('\n%m-%d-%Y')) + ax.xaxis.set_major_formatter(dates.DateFormatter("\n%m-%d-%Y")) for si, system in enumerate(systems): - ssub = stat_df[stat_df['system'] == system] + ssub = stat_df[stat_df["system"] == system] marker = markers[system] for pi, python in enumerate(pythons): - psub = ssub[ssub['python'] == python] + psub = ssub[ssub["python"] == python] color = colors[python] - ax.scatter(psub['time'], psub['value'], color=color, marker=marker) - ax.plot(psub['time'], psub['value'], linestyle="dotted", color=color) + ax.scatter( + psub["time"], psub["value"], color=color, marker=marker + ) + ax.plot( + psub["time"], + psub["value"], + linestyle="dotted", + color=color, + ) # configure legend patches = [] for system in systems: for python in pythons: - patches.append(Line2D([0], [0], color=colors[python], marker=markers[system], label=f"{system} Python{python}")) - leg = plt.legend(handles=patches, loc='upper left', ncol=3, bbox_to_anchor=(0, 0), framealpha=0.5, bbox_transform=ax.transAxes) + patches.append( + Line2D( + [0], + [0], + color=colors[python], + marker=markers[system], + label=f"{system} Python{python}", + ) + ) + leg = plt.legend( + handles=patches, + loc="upper left", + ncol=3, + bbox_to_anchor=(0, 0), + framealpha=0.5, + bbox_transform=ax.transAxes, + ) for lh in leg.legendHandles: lh.set_alpha(0.5) fig.suptitle(case_name) - plt.ylabel('ms') + plt.ylabel("ms") fig.tight_layout() fig.set_size_inches(8, 8) @@ -116,13 +144,21 @@ def seaborn_plot(stats): for i, (stat_name, stat_group) in enumerate(stats): stat_df = pd.DataFrame(stat_group) ax = axs[i] if nstats > 1 else axs - ax.tick_params(axis='x', rotation=45) - - sp = sns.scatterplot(x='time', y='value', style='system', hue='python', data=stat_df, ax=ax, palette="YlOrBr") + ax.tick_params(axis="x", rotation=45) + + sp = sns.scatterplot( + x="time", + y="value", + style="system", + hue="python", + data=stat_df, + ax=ax, + palette="YlOrBr", + ) sp.set(xlabel=None) ax.set_title(stat_name) ax.get_legend().remove() - ax.set_ylabel('ms') + ax.set_ylabel("ms") fig.suptitle(case_name) fig.tight_layout() @@ -134,12 +170,10 @@ def seaborn_plot(stats): # create and save plots -cases = benchmarks_df.groupby('case') +cases = benchmarks_df.groupby("case") for case_name, case in cases: - stats = pd.DataFrame(case).groupby('stat') - case_name = str(case_name) \ - .replace('/', '_') \ - .replace(':', '_') + stats = pd.DataFrame(case).groupby("stat") + case_name = str(case_name).replace("/", "_").replace(":", "_") # fig = matplotlib_plot(stats) fig = seaborn_plot(stats) diff --git a/setup.cfg b/setup.cfg index b3700b6b77..49e1f95965 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,7 @@ test = mfpymake pytest pytest-benchmark + pytest-cases pytest-cov pytest-xdist optional =