diff --git a/.docs/md/get_modflow.md b/.docs/md/get_modflow.md index 63ab79c6af..31ea146b6b 100644 --- a/.docs/md/get_modflow.md +++ b/.docs/md/get_modflow.md @@ -82,10 +82,10 @@ By default the distribution from the [`MODFLOW-ORG/executables` repository](http - `zbud6` - `libmf6.dylib` -To select a distribution, specify a repository name with the `--repo` command line option or the `repo` function argument. Valid names are: +To select a distribution, specify a repository name with the `--repo` command line option or the `repo` function argument. The official repositories are: - `executables` (default) - `modflow6` - `modflow6-nightly-build` -The repository owner can also be configured with the `--owner` option. This can be useful for installing from unreleased MODFLOW 6 feature branches still in development — the only compatibility requirement is that release assets be named identically to those on the official repositories. +The repository owner can also be configured with the `--owner` option. Together with `--repo`, this can be used to install from any repository, such as one that builds an unreleased MODFLOW 6 feature branch still in development — the only compatibility requirement is that release assets be named like those on the official repositories, with the operating system tag (e.g. `linux.zip`, `mac.zip`, `macarm.zip`, or `win64.zip`). diff --git a/.docs/md/version_changes.md b/.docs/md/version_changes.md index 8b1906914a..eae62a9111 100644 --- a/.docs/md/version_changes.md +++ b/.docs/md/version_changes.md @@ -1,4 +1,117 @@ # Changelog +### Version 3.12.0 + +#### New features + +* [feat(compare_cell_budget)](https://github.com/modflowpy/flopy/commit/a84ca567b5f5b344d7592c82c2ce3895e04ef04c): Move cbb comparison function from mf6 tests (#2636). Committed by wpbonelli on 2025-10-14. +* [feat(grid)](https://github.com/modflowpy/flopy/commit/2f24768fa3fac668f6cb988a57c5c5abe6e2e237): Add array support to .intersect() (#2646). Committed by aacovski on 2025-11-17. +* [feat](https://github.com/modflowpy/flopy/commit/3e1b7a9d43398fc1ca5c7b900bf328e7e3f046b7): Add optional replace_existing param to set_all_data_external (#2666). Committed by wpbonelli on 2025-12-16. +* [feat(lgr)](https://github.com/modflowpy/flopy/commit/1ad30ae6720939085523756e7f25ca94c01da2e9): Add from_parent_grid classmethod to Lgr class (#2668). Committed by wpbonelli on 2025-12-18. +* [feat](https://github.com/modflowpy/flopy/commit/8d8097a71cd233d7327ab11ed1a2e57508c33c7d): Optional replacement semantics for stress period data (#2664). Committed by wpbonelli on 2026-01-08. +* [feat(GridIntersect)](https://github.com/modflowpy/flopy/commit/cd4e6e9013b9c5fa9231742857ba3fe5ae722cc8): Add array support, fast point locating and support for points with z-coordinates (#2657). Committed by Davíd Brakenhoff on 2026-01-16. +* [feat(grid)](https://github.com/modflowpy/flopy/commit/94f3c8ca17f22d7f7abf09417d4d0053bc8e8301): Add get_node() method to all grid types (#2680). Committed by wpbonelli on 2026-01-16. +* [feat(grid)](https://github.com/modflowpy/flopy/commit/905d20b3a70fa0cb8f02f8e78fb7d03e5cc82c0d): Add ihc to UnstructuredGrid (#2681). Committed by wpbonelli on 2026-01-16. +* [feat(GeoDataFrame support)](https://github.com/modflowpy/flopy/commit/ae1533fc0df95edfd7587c14195b40c13b72b293): Geopandas support for shapefile exporting (#2671). Committed by Joshua Larsen on 2026-01-21. +* [feat(plot_bc)](https://github.com/modflowpy/flopy/commit/fea6fe60efcec7320230e94c5e6052a7c9c65f9d): Add subset argument (#2694). Committed by Joseph Hughes on 2026-01-28. +* [feat(get_transmissivities)](https://github.com/modflowpy/flopy/commit/b48d7e3d20ce07fbcebb58340813326bcc3e8a67): Interpret head=None as fully saturated (#2690). Committed by wpbonelli on 2026-01-29. +* [feat(arrays)](https://github.com/modflowpy/flopy/commit/5e64da82af11b4cbf0bf145be86fdcbe0c183add): Add model.free_format_npl to control values-per-line in free-format array output (#2696). Committed by René Angermeyer on 2026-01-30. +* [feat(binaryfile)](https://github.com/modflowpy/flopy/commit/e6951c4639387ae0935a3d3a3a9291aee186d02e): Add write methods (#2722). Committed by wpbonelli on 2026-04-10. +* [feat(mve)](https://github.com/modflowpy/flopy/commit/71c82d86d0a22fab5420cb40872b581a9270257c): Add support for gwe-gwe exchanges that include mve package (#2750). Committed by Eric Morway on 2026-04-22. +* [feat(hfb_util)](https://github.com/modflowpy/flopy/commit/3405517c7d2fed2667722fe8dc07b0d2af269fbe): Add method for creating HFBs (#2745). Committed by Joshua Larsen on 2026-05-11. +* [feat(cellbudgetfile)](https://github.com/modflowpy/flopy/commit/098060699b6d34efabff690ac35b0209148b23f3): Support aux vars with full3D in get_data() (#2776). Committed by wpbonelli on 2026-06-29. +* [feat(gnc)](https://github.com/modflowpy/flopy/commit/085e6a11f8fdb4091755034dbe6ca9ac2604506b): Add ghost node correction data generation (#2787). Committed by Joseph Hughes on 2026-08-04. +* [feat(gridgen)](https://github.com/modflowpy/flopy/commit/4c8c52394878a8bd27093661950824ad144ee799): Build GNC package input from gridgen output (#2789). Committed by Joseph Hughes on 2026-08-05. +* [feat(lgr)](https://github.com/modflowpy/flopy/commit/5cc72048e62e94560aeffd551d6077728ff786c2): Add ghost node correction data for a GWF-GWF exchange (#2790). Committed by Joseph Hughes on 2026-08-05. +* [feat(lakpak_utils)](https://github.com/modflowpy/flopy/commit/8e8feaf93744bf7004b7d7b8185439f8c97f323f): Add support for VertexGrid embedded lakes (#2786). Committed by afc98 on 2026-08-05. +* [feat(dis_properties)](https://github.com/modflowpy/flopy/commit/b8dfaeb5456708f9034a9701862b3a215c732d21): Add grid method to get discretization properties (#2795). Committed by Joshua Larsen on 2026-08-12. +* [feat(Mf6Splitter)](https://github.com/modflowpy/flopy/commit/41b47b9e54f00c954940233c1ced4b21f86dea49): Weight horizontal flow barrier faces for metis (#2826). Committed by Joseph Hughes on 2026-09-03. +* [feat(get-modflow)](https://github.com/modflowpy/flopy/commit/1a783da6582f8203190a8d6b1d7a60659022afdb): Consistently retry transient network failures (#2849). Committed by wpbonelli on 2026-09-14. +* [feat(get-modflow)](https://github.com/modflowpy/flopy/commit/b9ee6fa20db9cf974d4b2c7b759288d84f1f9175): Accept any repository with standard release assets (#2872). Committed by Joseph Hughes on 2026-10-21. + +#### Bug fixes + +* [fix(mflist)](https://github.com/modflowpy/flopy/commit/d5cc96208db3247fb097b6c0a0520a3e6a8601f3): Fix open/close bugs (#2638). Committed by Matt Dumont on 2025-10-17. +* [fix(mp7)](https://github.com/modflowpy/flopy/commit/44c90717afd0f4d6dc58907b88b8093736af8906): Fix shape assignment for DISU package (#2651). Committed by Mason Marchildon on 2025-11-18. +* [fix(MfUsgWel)](https://github.com/modflowpy/flopy/commit/5a945268913b6f4d07104d7c4ab5a964a37211e1): Add get_empty() override to support WELLBOT parameter (#2656). Committed by René Angermeyer on 2025-12-02. +* [fix](https://github.com/modflowpy/flopy/commit/75b34a09a629b60fda5863135a3d4b24dfb760f4): Fix duplicate unit numbers in NAM file for MFUSG external files (#2659). Committed by René Angermeyer on 2025-12-02. +* [fix(mfusg)](https://github.com/modflowpy/flopy/commit/7468302bdf33c13e770888f2e71c636510e8b259): Fix precision loss in CLN/GNC packages writing coordinates (#2658). Committed by René Angermeyer on 2025-12-04. +* [fix(mfsimulation)](https://github.com/modflowpy/flopy/commit/543130b224bff0c12c81d6c381048f92275129b4): Respect max_columns_of_data for internal arrays (#2665). Committed by wpbonelli on 2025-12-12. +* [fix(cellbudgetfile)](https://github.com/modflowpy/flopy/commit/b16eb2ba93b7f8b09ba0f1455360cd919e1ff887): Fix get_ts support for aux vars (#2648). Committed by wpbonelli on 2025-12-16. +* [fix(lgrutil)](https://github.com/modflowpy/flopy/commit/ada6e63972c3b0ba33d4c0e6abc4e7cfa917341f): Remove duplicate hanging vertices (#2672). Committed by wpbonelli on 2025-12-20. +* [fix(plot)](https://github.com/modflowpy/flopy/commit/aad068289ef41e375986bd46dff3977ba5cc2320): Support plotting hfb packages (#2677). Committed by wpbonelli on 2026-01-09. +* [fix(mp7)](https://github.com/modflowpy/flopy/commit/bef063f321b6e1387bebb83cc2ec2165e6f6367f): Fix input file writing (#2679). Committed by wpbonelli on 2026-01-09. +* [fix(evt)](https://github.com/modflowpy/flopy/commit/a88f0e179f49768f1d6c13549b7f8fcc0ff96779): Fix optional field handling at write time (#2684). Committed by wpbonelli on 2026-01-24. +* [fix(get_transmissivities)](https://github.com/modflowpy/flopy/commit/978d7d04346e109798575b24f50d0542eda53224): Support DISU (#2685). Committed by wpbonelli on 2026-01-24. +* [fix(rasters)](https://github.com/modflowpy/flopy/commit/61e9e3196f2fd2dfaf119a7f734be94e84c317ce): Fix raster_from_array transformation order (#2689). Committed by wpbonelli on 2026-01-28. +* [fix(get_transmissivities)](https://github.com/modflowpy/flopy/commit/7758c77edf90309a2c7618bc4c85de5d91a56253): Handle single r/c (#2697). Committed by wpbonelli on 2026-01-29. +* [fix(cvfdutil)](https://github.com/modflowpy/flopy/commit/289394901778916aa63f53362d17d906427e0d9a): Add non-convergence check to shapefile_to_cvfd (#2688). Committed by wpbonelli on 2026-01-30. +* [fix(HeadUFile)](https://github.com/modflowpy/flopy/commit/6bb9c1aa1ac43300b36cbcf7bf51c638ce4dd65f): Disable get_alldata without layer (#2686). Committed by wpbonelli on 2026-01-30. +* [fix(mfdataarray)](https://github.com/modflowpy/flopy/commit/2092f70225fbd1e4e7eb1a92975e5574d30d2fc9): Support time array series (#2703). Committed by wpbonelli on 2026-02-04. +* [fix(mfdataarray)](https://github.com/modflowpy/flopy/commit/ff26d7238a8f70dc6649c327319d83d86d1a7577): Use integer indexed filenames (#2704). Committed by wpbonelli on 2026-02-04. +* [fix(model_splitter)](https://github.com/modflowpy/flopy/commit/7746e0995a0e72716e0012b4c84d1aac4327a566): Fix spurious ssm creation (#2716). Committed by wpbonelli on 2026-02-25. +* [fix(UcnFile)](https://github.com/modflowpy/flopy/commit/224432b81ce862632463ed652e48d97c3952ff43): Add check for modflow-6 GWT concentration file header type (#2718). Committed by Joshua Larsen on 2026-03-05. +* [fix](https://github.com/modflowpy/flopy/commit/066270c1e1bcfb5df80d5e3ff9479f0d06a5d340): Wipe toml dfn dir before regenerating mf6 module (#2730). Committed by wpbonelli on 2026-03-14. +* [fix(vtk)](https://github.com/modflowpy/flopy/commit/20ce356115ec8012b435e8c074cdc46a06e6d108): Fix add_pathline_points prt data handling (#2732). Committed by wpbonelli on 2026-03-18. +* [fix(vtk)](https://github.com/modflowpy/flopy/commit/11432e43a81f62ada5d62425ab13bf030294add2): Transpose vector before numpy_to_vtk (#2733). Committed by wpbonelli on 2026-03-19. +* [fix(model_splitter)](https://github.com/modflowpy/flopy/commit/c80fc6c6e4b3a15ba1b8e9e90e7b6ccd2a052ec0): Multiple bug fixes (#2728). Committed by Joshua Larsen on 2026-03-23. +* [fix(mp7particledata)](https://github.com/modflowpy/flopy/commit/f0c043255a7134e121b42a4dbdeca988b26394cd): To_prp bug fix (#2737). Committed by Kalle Jahn on 2026-03-26. +* [fix(mfpackage)](https://github.com/modflowpy/flopy/commit/6be21ac6ef228e8a0b08dfca9a1a4344b8d4a9b9): Clear timeseries files on ts initialize (#2749). Committed by wpbonelli on 2026-04-16. +* [fix(mnw1)](https://github.com/modflowpy/flopy/commit/66b442354d124243e989ea3015b9ab58b3d1c6a7): Fix mnw1 load (#2752). Committed by Joseph Hughes on 2026-05-10. +* [fix(mf6)](https://github.com/modflowpy/flopy/commit/c4d445e16ae8f82902aaa8fbaf8ddece0c000243): Defensive devtools compatibility fix (#2762). Committed by wpbonelli on 2026-06-02. +* [fix(GeoSpatialUtil)](https://github.com/modflowpy/flopy/commit/8a2d0552ab48f0b7fdc4a952d416f690c9b3cab0): Accommodate stricter pyshp API (#2765). Committed by wpbonelli on 2026-06-08. +* [fix(mfdataplist)](https://github.com/modflowpy/flopy/commit/c27af43d2be01e335ed3d908c2af67cd5f851b78): Raise proper MFDataException on wrong col count (#2766). Committed by wpbonelli on 2026-06-08. +* [fix(mfdataplist)](https://github.com/modflowpy/flopy/commit/228668c548e97fcfdeea71bb354df99e2eb1065d): Accept DataFrame with tuple cellid (#2767). Committed by wpbonelli on 2026-06-09. +* [fix(model_attributes_to_shapefile)](https://github.com/modflowpy/flopy/commit/6a5d271fd41d7234b37ecc4be4c765ed2799ee15): Use modelgrid kwarg if provided (#2768). Committed by wpbonelli on 2026-06-09. +* [fix(model_splitter)](https://github.com/modflowpy/flopy/commit/8d5c41001cd86b8286981317eda314ac3d477352): Copy on boundname remap (#2769). Committed by wpbonelli on 2026-06-09. +* [fix(mfusgt)](https://github.com/modflowpy/flopy/commit/60db7e721ffe8961c5a9f71382598d54cf19115b): Fix multiple bugs (#2770). Committed by wpbonelli on 2026-06-10. +* [fix(mf6-dis)](https://github.com/modflowpy/flopy/commit/6910978ce3d72cb1cb0d6f35327206a442bf2106): Auto-set max_columns_of_data to ncol for external writes (#2771). Committed by wpbonelli on 2026-06-12. +* [fix(CellBudgetFile)](https://github.com/modflowpy/flopy/commit/7465a329ab50f45251f40f92a1d18db6a51296fb): Trap OSError when detecting precision (#2772). Committed by wpbonelli on 2026-06-17. +* [fix(mfdataplist)](https://github.com/modflowpy/flopy/commit/018bd9ca60a40995c5cba5e88e1630f30e7532e1): Fix multiple issues (#2775). Committed by wpbonelli on 2026-06-29. +* [fix(rasters)](https://github.com/modflowpy/flopy/commit/38b6a10f19c0192369ec7fcbe3be7fba8ae5a5c2): Accommodate rasterio>=1.5 (#2781). Committed by wpbonelli on 2026-07-11. +* [fix(autotest)](https://github.com/modflowpy/flopy/commit/1885436ac5e829b597e4fa970ff03094b31d9ad8): Skip tests that need an optional dependency (#2797). Committed by Joseph Hughes on 2026-08-07. +* [fix(autotest)](https://github.com/modflowpy/flopy/commit/8c0059d1e008d3707a1c9578de1d22ec6b336cab): Skip export tests that need geopandas (#2800). Committed by Joseph Hughes on 2026-08-07. +* [fix(zonebudget_example)](https://github.com/modflowpy/flopy/commit/79a031a413e4c6f7acfdfb2996a899039ee4e6a1): Index the budget values by position (#2801). Committed by Joseph Hughes on 2026-08-07. +* [fix(vtk)](https://github.com/modflowpy/flopy/commit/74d7b9ef2d232d3917f9590f8bfaeb9df2a7a3f8): Transpose the vector after the point scalar expansion (#2799). Committed by Joseph Hughes on 2026-08-07. +* [fix(mf6)](https://github.com/modflowpy/flopy/commit/78ab37dabc67c00ecd77c65fdd86f7f4e82a7ef8): Omit removed fields from generated code (#2804). Committed by wpbonelli on 2026-08-12. +* [fix(get-modflow)](https://github.com/modflowpy/flopy/commit/38acfae4ca0039943890f795c8f0ba2a4205c7b5): Use urlopen for download (#2807). Committed by wpbonelli on 2026-08-12. +* [fix(mfusglpf)](https://github.com/modflowpy/flopy/commit/a222eba82d3679a5c870833616161ba92cbf4f4d): Fix condition for richards laytyp check (#2812). Committed by Martin Vonk on 2026-08-13. +* [fix(crosssection)](https://github.com/modflowpy/flopy/commit/65b6c5541cf1deb7a73d92839e58a3f5876e873b): Make view x/y override symmetric, validate input (#2805). Committed by wpbonelli on 2026-08-19. +* [fix(utils)](https://github.com/modflowpy/flopy/commit/6c8b29bab37f604ae4a42bd7f92490519434b575): Fix zone off-by-one, add test for #2612 (#2818). Committed by wpbonelli on 2026-08-20. +* [fix(binarygrid_util)](https://github.com/modflowpy/flopy/commit/73af2a7395a8fa8ceaf7a056970bb50d353be9e4): Restore top assignment dropped with reshape (#2833). Committed by wpbonelli on 2026-08-31. +* [fix(test_save_load_node_mapping_structured)](https://github.com/modflowpy/flopy/commit/b59605b55a6a0b9f82e0bb64ff430f032905955a): (#2838). Committed by Joshua Larsen on 2026-09-03. +* [fix(ModflowSfr2)](https://github.com/modflowpy/flopy/commit/fe1241621c237bfff604f9b9f290d516d9232634): Support single reach models (#2841). Committed by Joshua Larsen on 2026-09-04. +* [fix(_remap_obs)](https://github.com/modflowpy/flopy/commit/f9137c53175c17ee98642adefce45ba31587a73a): Fix support for GWF observation types with 2 cellids (#2842). Committed by Joshua Larsen on 2026-09-18. +* [fix(mflistfile)](https://github.com/modflowpy/flopy/commit/0901dc034c1377a5f3e23642b625b9926c158ace): Parse budget values missing the exponent "E" (#2858). Committed by wpbonelli on 2026-09-29. +* [fix(to_geodataframe)](https://github.com/modflowpy/flopy/commit/d3cac83dd17cd6155d3779a0d1c3ec12ade5cc67): Add logic for implicit stress period data (#2852). Committed by Joshua Larsen on 2026-09-30. +* [fix(optimize_splitting_mask)](https://github.com/modflowpy/flopy/commit/d22cf257f7373f96750696f22b17d3a2db5711eb): Add edge weights for LAK cells (#2853). Committed by Joshua Larsen on 2026-09-30. +* [fix(mflistfile)](https://github.com/modflowpy/flopy/commit/68b3c6f928d8020e2591d8caab6f3c754aa6ec43): Raise informative error if first budget can't be parsed (#2859). Committed by wpbonelli on 2026-10-03. +* [fix(cvfdutil)](https://github.com/modflowpy/flopy/commit/f21293ad0aecc587b46f99a63d58599533306531): Preserve conforming shared faces during grid conversion (#2867). Committed by Oscar Sanchez on 2026-10-16. +* [fix(mf6)](https://github.com/modflowpy/flopy/commit/ad4623ee6a06ed96edf0433929263557323119e5): Find external files in model subdirectories with any path separator (#2879). Committed by Mike Müller on 2026-10-31. + +#### Mfgrdfile + +* [mfgrdfile](https://github.com/modflowpy/flopy/commit/079298e866e54d0e7e95c07030752d382225f144): Add version and crs properties (#2711). Committed by mjreno on 2026-06-30. +* [mfgrdfile](https://github.com/modflowpy/flopy/commit/7c717126d81259eb9ae6196f420e56aa56744a86): Export follow-on (#2779). Committed by mjreno on 2026-07-01. + +#### Refactoring + +* [refactor](https://github.com/modflowpy/flopy/commit/6b40d8efe2f361bdbdc8db1b3b790f0e6fb75092): Remove get_version_string from MFStructure (#2630). Committed by wpbonelli on 2025-10-04. +* [refactor(codegen)](https://github.com/modflowpy/flopy/commit/32b71c56c8729c65b752765d9b151b823c211a62): Store new spec on generated classes (#2631). Committed by wpbonelli on 2025-10-04. +* [refactor(mfstructure)](https://github.com/modflowpy/flopy/commit/2be45f7ad8d9ef9841bee76e8397363548a5b456): Miscellaneous (#2633). Committed by wpbonelli on 2025-10-09. +* [refactor(codegen)](https://github.com/modflowpy/flopy/commit/123487fcf8e62ee093cae750e809e0e0683077ba): Support 'prerelease' or 'developmode' in dfns (#2635). Committed by wpbonelli on 2025-10-11. +* [refactor(mflistfile)](https://github.com/modflowpy/flopy/commit/c0cae8112cbceb9fe2931e7d273db41dff8daabc): Deprecate set_budget_key for init (#2637). Committed by wpbonelli on 2025-10-14. +* [refactor(grid)](https://github.com/modflowpy/flopy/commit/aaa38e7b8b34bd1e0215bb8237ade390e6c8dac3): Consistent API for get_cell_vertices (#2678). Committed by wpbonelli on 2026-01-08. +* [refactor(plot)](https://github.com/modflowpy/flopy/commit/41f1840bc55370c5eb4be4f4313400048f64bf43): Better shared face finding for HFB plotting (#2682). Committed by wpbonelli on 2026-01-31. +* [refactor(_remap_array)](https://github.com/modflowpy/flopy/commit/68880321ee3bd1fe34eed0053b697ab787758b00): Support non-layered external data (#2747). Committed by Joshua Larsen on 2026-04-22. +* [refactor(intersect)](https://github.com/modflowpy/flopy/commit/b2ceda31a620b135ea4cafc10f55e2cc148af4d9): Update days calculation to fix dropped precision (#2754). Committed by Joshua Larsen on 2026-05-12. +* [refactor(splitter)](https://github.com/modflowpy/flopy/commit/579507e227149d481212148835420d2af2da3eed): Updates for advanced transport packages (#2761). Committed by JLarsen on 2026-05-28. +* [refactor(ModflowWel)](https://github.com/modflowpy/flopy/commit/866bb586380d934f18e7e666357e6625cbdf874c): Add basic support for mfnwt tabfile based well files (#2755). Committed by Joshua Larsen on 2026-05-28. +* [refactor(PlotCrossSection)](https://github.com/modflowpy/flopy/commit/39aaa73fa9a6d9fd87483b46cee8915c3e741f5e): Add additional flexibility to cross sectional plotting (#2782). Committed by Joshua Larsen on 2026-08-05. +* [refactor(Mf6Splitter)](https://github.com/modflowpy/flopy/commit/30596be4ca1905a0f5c44a3e2c13c1fa1687ade2): Update data types for memory reduction (#2819). Committed by Joshua Larsen on 2026-09-03. +* [refactor(_remap_sfr)](https://github.com/modflowpy/flopy/commit/6f5d14251114da5e61853ca67b2711e6f6984d33): Add more robust handling for (0, 0, 0) SFR cells (#2840). Committed by Joshua Larsen on 2026-09-18. +* [refactor(mfmodel)](https://github.com/modflowpy/flopy/commit/3904a459ad319a08f08095eaf380e9870280b4de): Remove unused package_extension in register_package (#2871). Committed by wpbonelli on 2026-10-19. +* [refactor(dependencies)](https://github.com/modflowpy/flopy/commit/0cbb5c7c725d76d670274ecdfe9b98b1e80f79fc): Require Python >=3.12 per SPEC 0 (#2843). Committed by wpbonelli on 2026-10-31. + ### Version 3.11.0 #### New features diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 5048bc2384..f19c1a4247 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - python-version: [ "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.12", "3.13", "3.14" ] defaults: run: shell: bash -l {0} @@ -82,6 +82,7 @@ jobs: - name: Setup Python uses: astral-sh/setup-uv@v7 with: + python-version: "3.12" cache-dependency-glob: "**/pyproject.toml" - name: Install FloPy diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index 59f2e15eaf..30e7fa1c41 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - python-version: [ "3.11", "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.12", "3.13", "3.14" ] ref: ["develop", "master", "6.4.1", "6.4.0"] defaults: run: diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 3db764b935..616c0978f9 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -71,7 +71,7 @@ jobs: shell: bash timeout-minutes: 10 env: - PYTHON_VERSION: '3.11' + PYTHON_VERSION: '3.12' steps: - name: Checkout repo @@ -80,6 +80,7 @@ jobs: - name: Setup Python uses: astral-sh/setup-uv@v7 with: + python-version: ${{ env.PYTHON_VERSION }} cache-dependency-glob: "**/pyproject.toml" - name: Install Python dependencies @@ -115,7 +116,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - python-version: [ "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.12", "3.13", "3.14" ] defaults: run: shell: bash -l {0} @@ -150,9 +151,9 @@ jobs: version: 13 - name: Setup pixi - uses: prefix-dev/setup-pixi@v0.10.2 + uses: prefix-dev/setup-pixi@v0.11.2 with: - pixi-version: v0.70.1 + pixi-version: v0.82.1 manifest-path: modflow6/pixi.toml - name: Install dependencies diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 6f8f1afcc8..bee3f4dab9 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - python-version: [ "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.12", "3.13", "3.14" ] defaults: run: shell: bash @@ -30,12 +30,9 @@ jobs: - name: Install FloPy run: uv sync --all-extras - - name: OpenGL workaround on Linux + - name: Set up headless display on Linux if: runner.os == 'Linux' - run: | - # referenced from https://github.com/pyvista/pyvista/blob/main/.github/workflows/vtk-pre-test.yml#L53 - uv pip uninstall vtk - uv pip install --extra-index-url https://wheels.vtk.org trame vtk-osmesa + uses: pyvista/setup-headless-display-action@v5 - name: Install OpenGL on Windows if: runner.os == 'Windows' diff --git a/.github/workflows/mf6.yml b/.github/workflows/mf6.yml index 755fb9f509..2126a43082 100644 --- a/.github/workflows/mf6.yml +++ b/.github/workflows/mf6.yml @@ -39,9 +39,9 @@ jobs: version: 13 - name: Setup pixi - uses: prefix-dev/setup-pixi@v0.10.2 + uses: prefix-dev/setup-pixi@v0.11.2 with: - pixi-version: v0.70.1 + pixi-version: v0.82.1 manifest-path: modflow6/pixi.toml - name: Install dependencies @@ -122,9 +122,9 @@ jobs: version: 13 - name: Setup pixi - uses: prefix-dev/setup-pixi@v0.10.2 + uses: prefix-dev/setup-pixi@v0.11.2 with: - pixi-version: v0.70.1 + pixi-version: v0.82.1 manifest-path: modflow6/pixi.toml - name: Install dependencies diff --git a/.github/workflows/optional.yml b/.github/workflows/optional.yml index 4e1b18b3c1..39b0a6404e 100644 --- a/.github/workflows/optional.yml +++ b/.github/workflows/optional.yml @@ -15,7 +15,7 @@ jobs: shell: bash timeout-minutes: 10 env: - PYTHON_VERSION: '3.11' + PYTHON_VERSION: '3.12' strategy: fail-fast: false matrix: @@ -31,6 +31,7 @@ jobs: - name: Setup Python uses: astral-sh/setup-uv@v7 with: + python-version: ${{ env.PYTHON_VERSION }} cache-dependency-glob: "**/pyproject.toml" - name: Install FloPy without optional dependencies diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000000..3bf558e17e --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,43 @@ +name: pull request + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + branches: [develop] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + + title: + name: check title + # the release and reset pull requests are opened by a bot and are not + # squash merged, so their titles do not reach the changelog + if: github.event.pull_request.user.type != 'Bot' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + + - name: Check conventional commit format + env: + TITLE: ${{ github.event.pull_request.title }} + run: | + # pull requests are squash merged, so the title becomes the commit + # message in develop, and the changelog is generated from those + # messages. a title that is not a conventional commit is dropped + # from the release notes without warning. + types="build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test" + if [[ ! "$TITLE" =~ ^($types)(\([^\)]+\))?!?:\ .+ ]]; then + echo "::error::'$TITLE' is not a conventional commit header." + echo "" + echo "Use '(): ', for example:" + echo " feat(mf6): support adaptive time step retries" + echo "" + echo "Type is one of: ${types//|/, }." + echo "Only feat, fix, perf and refactor reach the release notes;" + echo "see cliff.toml. A user facing change needs one of those." + exit 1 + fi + echo "'$TITLE' is a conventional commit header" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25e0ec7814..cd01783149 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,7 @@ jobs: - name: Setup Python uses: astral-sh/setup-uv@v7 with: + python-version: "3.12" enable-cache: false - name: Install FloPy @@ -175,7 +176,7 @@ jobs: # actions/download-artifact won't look at previous workflow runs but we need to in order to get changelog - name: Download artifacts - uses: dawidd6/action-download-artifact@v24 + uses: dawidd6/action-download-artifact@v27 - name: Draft release env: @@ -213,6 +214,7 @@ jobs: - name: Setup Python uses: astral-sh/setup-uv@v7 with: + python-version: "3.12" enable-cache: false - name: Build package @@ -223,3 +225,67 @@ jobs: - name: Publish package run: uv publish --verbose --trusted-publishing always + + reset: + name: Draft develop reset PR + # runs after a release is published, opens a PR merging master back into develop + if: ${{ github.event_name == 'release' }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + defaults: + run: + shell: bash + steps: + + - name: Checkout master branch + uses: actions/checkout@v7 + with: + ref: master + fetch-depth: 0 + + - name: Setup Python + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + enable-cache: false + + - name: Install FloPy + run: uv sync --all-extras + + - name: Update version numbers + id: version + run: | + ver=$(cat version.txt) + next=$(uv run scripts/update_version.py --post-release) + echo "version=$ver" >> $GITHUB_OUTPUT + echo "next=$next" >> $GITHUB_OUTPUT + + - name: Format and lint Python files + run: | + uvx ruff format flopy/version.py + uvx ruff check --fix flopy/version.py + + - name: Draft pull request + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + ver="${{ steps.version.outputs.version }}" + next="${{ steps.version.outputs.next }}" + branch="post-release-$ver-reset" + + git config core.sharedRepository true + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -c "$branch" + git add flopy/version.py version.txt CITATION.cff README.md docs/PyPI_release.md + git commit -m "ci(release): update version to $next" + git push origin "$branch" + + body=' + # Reset `develop` after release '$ver' + + Merge (do not squash) this pull request to bring `master` back into `develop` and set the development version to `'$next'`. + ' + gh pr create -B "develop" -H "$branch" --title "Reset develop after release $ver" --draft --body "$body" diff --git a/.github/workflows/rtd.yml b/.github/workflows/rtd.yml index aea526465d..53cb6bc3d2 100644 --- a/.github/workflows/rtd.yml +++ b/.github/workflows/rtd.yml @@ -87,9 +87,9 @@ jobs: echo $GITHUB_EVENT_NAME - name: Setup pixi - uses: prefix-dev/setup-pixi@v0.10.2 + uses: prefix-dev/setup-pixi@v0.11.2 with: - pixi-version: v0.70.1 + pixi-version: v0.82.1 manifest-path: modflow6/pixi.toml - name: Install Python dependencies @@ -98,14 +98,6 @@ jobs: pixi run -e rtd install pixi run -e rtd pip install "../flopy[optional]" - - name: Workaround OpenGL issue on Linux - if: runner.os == 'Linux' - working-directory: modflow6 - run: | - # referenced from https://github.com/pyvista/pyvista/blob/main/.github/workflows/vtk-pre-test.yml#L53 - pixi run -e rtd pip uninstall vtk - pixi run -e rtd pip install --extra-index-url https://wheels.vtk.org trame vtk-osmesa - - name: Install fonts on Linux if: runner.os == 'Linux' run: | diff --git a/.readthedocs.yml b/.readthedocs.yml index 43dde5ed0a..0bba7e50f6 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,17 +9,20 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" - # todo: uncomment below if notebooks are to be executed in RTD build - # instead of in CI as currently, requires runtime reduction <15 mins - # jobs: - # post_create_environment: - # # install MODFLOW6 and related executables (notebooks need them) - # - get-modflow :python - # # swap default vtk for vtk-osmesa (avoid PyVista openGL errors) - # # https://github.com/pyvista/pyvista/blob/main/.github/workflows/vtk-pre-test.yml#L53 - # - pip uninstall -y vtk - # - pip install --extra-index-url https://wheels.vtk.org trame vtk-osmesa + python: "3.12" + jobs: + install: + - pip install --upgrade "pip>=25.1" + - pip install --group docs . + # todo: uncomment below if notebooks are to be executed in RTD build + # instead of in CI as currently, requires runtime reduction <15 mins + # post_install: + # # install MODFLOW6 and related executables (notebooks need them) + # - get-modflow :python + # # swap default vtk for vtk-osmesa (avoid PyVista openGL errors) + # # https://github.com/pyvista/pyvista/blob/main/.github/workflows/vtk-pre-test.yml#L53 + # - pip uninstall -y vtk + # - pip install --extra-index-url https://wheels.vtk.org trame vtk-osmesa # Build documentation in the docs/ directory with Sphinx sphinx: @@ -28,11 +31,3 @@ sphinx: # Build only html and htmlzip (skip others, including PDF and EPUB) formats: - htmlzip - -# Set the Python version and requirements -python: - install: - - method: pip - path: . - extra_requirements: - - doc diff --git a/CITATION.cff b/CITATION.cff index 102a981c17..7f15e11f4a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -3,8 +3,8 @@ message: If you use this software, please cite both the article from preferred-c references, and the software itself. type: software title: FloPy -version: 3.11.0 -date-released: '2026-09-03' +version: 3.12.0 +date-released: '2026-10-31' doi: 10.5066/F7BK19FH abstract: A Python package to create, run, and post-process MODFLOW-based models. repository-artifact: https://pypi.org/project/flopy diff --git a/DEVELOPER.md b/DEVELOPER.md index 9399fcf0a6..72d4f86a0e 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -33,6 +33,7 @@ This document describes how to set up a FloPy development environment, run the e - [Snapshot testing](#snapshot-testing) - [Branching model](#branching-model) - [Deprecation policy](#deprecation-policy) + - [Releasing](#releasing) - [Miscellaneous](#miscellaneous) - [Locating the root](#locating-the-root) - [Dependency analysis](#dependency-analysis) @@ -60,9 +61,9 @@ git config blame.ignoreRevsFile .git-blame-ignore-revs ### Python -This project has historically aimed to support a wide range of [Python versions](https://devguide.python.org/versions/). In current and future development this window may narrow to follow [SPEC 0](https://scientific-python.org/specs/spec-0000/#support-window) instead. +This project has historically aimed to support a wide range of [Python versions](https://devguide.python.org/versions/). Development now follows the [SPEC 0](https://scientific-python.org/specs/spec-0000/#support-window) support window instead. -Install Python >=3.11 via [standalone download](https://www.python.org/downloads/), [uv](https://docs.astral.sh/uv/), or a conda-like distribution like [Anaconda](https://www.anaconda.com/products/individual), [miniconda](https://docs.conda.io/en/latest/miniconda.html) or [miniforge](https://github.com/conda-forge/miniforge) . +Install Python >=3.12 via [standalone download](https://www.python.org/downloads/), [uv](https://docs.astral.sh/uv/), or a conda-like distribution like [Anaconda](https://www.anaconda.com/products/individual), [miniconda](https://docs.conda.io/en/latest/miniconda.html) or [miniforge](https://github.com/conda-forge/miniforge) . Then install FloPy and core dependencies from the project root path: @@ -459,6 +460,10 @@ This project loosely follows [NEP 23](https://numpy.org/neps/nep-0023-backwards- See the linked article for more detail. +## Releasing + +The release procedure is documented separately in [docs/make_release.md](docs/make_release.md). FloPy releases are automated from a pushed `v` branch, publish to PyPI with [trusted publishing](https://docs.pypi.org/trusted-publishers/) from a `release` environment, and reach conda-forge through the autotick bot. There are also a few manual steps (updating citations, reviewing deprecations, regenerating the MF6 module); see the release guide for the full sequence and the caveats. + ## Miscellaneous ### Locating the root diff --git a/README.md b/README.md index f2f308e13c..0d10b7813c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ flopy3 -### Version 3.11.0 +### Version 3.12.0 [![flopy continuous integration](https://github.com/modflowpy/flopy/actions/workflows/commit.yml/badge.svg?branch=develop)](https://github.com/modflowpy/flopy/actions/workflows/commit.yml) [![Read the Docs](https://github.com/modflowpy/flopy/actions/workflows/rtd.yml/badge.svg?branch=develop)](https://github.com/modflowpy/flopy/actions/workflows/rtd.yml) @@ -32,12 +32,12 @@ Documentation Installation ----------------------------------------------- -FloPy requires **Python** 3.11+ with: +FloPy requires **Python** 3.12+ with: ``` -numpy >=1.20.3 +numpy >=2.2.0 matplotlib >=1.4.0 -pandas >=2.0.0 +pandas >=2.2.0 ``` Dependencies for optional features are summarized [here](https://flopy.readthedocs.io/en/latest/md/optional_dependencies.html). @@ -150,7 +150,7 @@ How to Cite ##### ***Software/Code citation for FloPy:*** -[Bakker, Mark, Post, Vincent, Hughes, J. D., Langevin, C. D., White, J. T., Leaf, A. T., Paulinski, S. R., Bellino, J. C., Morway, E. D., Toews, M. W., Larsen, J. D., Fienen, M. N., Starn, J. J., Brakenhoff, D. A., and Bonelli, W. P., 2026, FloPy v3.11.0: U.S. Geological Survey Software Release, 03 September 2026, https://doi.org/10.5066/F7BK19FH](https://doi.org/10.5066/F7BK19FH) +[Bakker, Mark, Post, Vincent, Hughes, J. D., Langevin, C. D., White, J. T., Leaf, A. T., Paulinski, S. R., Bellino, J. C., Morway, E. D., Toews, M. W., Larsen, J. D., Fienen, M. N., Starn, J. J., Brakenhoff, D. A., and Bonelli, W. P., 2026, FloPy v3.12.0: U.S. Geological Survey Software Release, 31 October 2026, https://doi.org/10.5066/F7BK19FH](https://doi.org/10.5066/F7BK19FH) Additional FloPy Related Publications diff --git a/autotest/test_export.py b/autotest/test_export.py index 1812418ef0..f3f6eb4ff5 100644 --- a/autotest/test_export.py +++ b/autotest/test_export.py @@ -977,6 +977,77 @@ def test_export_huge_shapefile(function_tmpdir): gdf.to_file(function_tmpdir / "huge.shp") +@requires_pkg("geopandas") +def test_to_geodataframe_incomplete_stress_period_data(): + sim = flopy.mf6.MFSimulation() + tdis = flopy.mf6.ModflowTdis( + sim, + nper=2, + perioddata=[(1, 1, 1), (1, 1, 1)], + ) + ims = flopy.mf6.ModflowIms(sim) + + gwf = flopy.mf6.ModflowGwf(sim, modelname="dev_gdf") + + dis = flopy.mf6.ModflowGwfdis( + gwf, nlay=1, nrow=10, ncol=11, delc=100, delr=100, top=100, botm=0, idomain=1 + ) + + npf = flopy.mf6.ModflowGwfnpf( + gwf, + k=10, + ) + + ic = flopy.mf6.ModflowGwfic(gwf, strt=99) + + chd_rec = [(0, i, 0, 95) for i in range(10)] + chd = flopy.mf6.ModflowGwfchd(gwf, stress_period_data={0: chd_rec}) + + ghb_rec = [(0, i, 10, 85.0, 10.0) for i in range(10)] + ghb = flopy.mf6.ModflowGwfghb(gwf, stress_period_data={0: ghb_rec, 1: ghb_rec}) + + rch_rec = np.full((10, 11), 0.0005) + rch_rec[:, 0] = 0 + rch_rec[:, -1] = 0 + rch = flopy.mf6.ModflowGwfrcha(gwf, recharge={0: rch_rec}) + + wel_rec = [ + (0, 4, 5, -1500.0), + ] + wel = flopy.mf6.ModflowGwfwel(gwf, stress_period_data={0: wel_rec}) + + recharge = rch.recharge.array[0].ravel() + gdf = rch.to_geodataframe(kper=1) + + np.testing.assert_allclose( + recharge, + gdf["rcha_recharge_1"].values, + err_msg="GeoDataFrame does not match recharge values from package", + ) + + wel_data = wel.stress_period_data.to_array(kper=0, mask=True)["q"].ravel() + gdf = wel.to_geodataframe(kper=1) + np.testing.assert_allclose( + wel_data, + gdf["wel_q_0_1"].values, + err_msg="GeoDataFrame does not match pumping values from wel package", + ) + + gdf = gwf.to_geodataframe(kper=1) + + np.testing.assert_allclose( + recharge, + gdf["rcha_recharge_1"].values, + err_msg="GeoDataFrame from gwf does not match recharge values from package", + ) + + np.testing.assert_allclose( + wel_data, + gdf["wel_q_0_1"].values, + err_msg="GeoDataFrame from gwf does not match pumping values from wel package", + ) + + @requires_pkg("netCDF4", "pyproj") def test_polygon_from_ij(function_tmpdir): """test creation of a polygon from an i, j location using get_vertices().""" diff --git a/autotest/test_get_modflow.py b/autotest/test_get_modflow.py index 103e573d83..66a572ed61 100644 --- a/autotest/test_get_modflow.py +++ b/autotest/test_get_modflow.py @@ -1,11 +1,13 @@ """Test get-modflow utility.""" +import io import os import sys +import urllib.request from os.path import expandvars from pathlib import Path from platform import system -from urllib.error import HTTPError +from urllib.error import HTTPError, URLError import pytest from flaky import flaky @@ -13,8 +15,8 @@ from modflow_devtools.misc import run_py_script from autotest.conftest import get_project_root_path -from flopy.utils import get_modflow -from flopy.utils.get_modflow import get_release, get_releases, select_bindir +from flopy.utils import get_modflow_module as get_modflow +from flopy.utils.get_modflow import get_release, get_releases, run_main, select_bindir rate_limit_msg = "rate limit exceeded" flopy_dir = get_project_root_path() @@ -81,6 +83,13 @@ def create_home_local_bin(): home_local.mkdir(parents=True, exist_ok=True) +@pytest.fixture(autouse=True) +def fast_retries(monkeypatch): + # make retries instant + monkeypatch.setattr(get_modflow, "http_retry_delay", 0.0) + monkeypatch.setenv("GET_MODFLOW_RETRY_DELAY", "0") + + def run_get_modflow_script(*args): return run_py_script(get_modflow_script, *args, verbose=True) @@ -100,6 +109,213 @@ def test_get_releases_bad_page_size(per_page): get_releases(repo="executables", per_page=per_page) +class FakeResponse(io.BytesIO): + def __init__(self, data=b"", headers=None): + super().__init__(data) + self.headers = headers or {} + + +def fake_urlopen(fail_times, exc, data=b"{}", headers=None): + # raises exc the first fail_times calls, then returns a fresh FakeResponse; + # call count is tracked on .calls + state = {"n": 0} + + def _fake(request, timeout=10, quiet=False): + state["n"] += 1 + if state["n"] <= fail_times: + raise exc + return FakeResponse(data, headers) + + _fake.calls = state + return _fake + + +def reset_error(): + return URLError(ConnectionResetError(104, "Connection reset by peer")) + + +def test_fetch_returns_body_and_headers(monkeypatch): + fake = fake_urlopen( + 0, None, data=b'{"a": 1}', headers={"x-ratelimit-remaining": "42"} + ) + monkeypatch.setattr(get_modflow, "urlopen", fake) + body, headers = get_modflow.fetch(object()) + assert body == b'{"a": 1}' + assert headers.get("x-ratelimit-remaining") == "42" + + +def test_fetch_retries_transient_then_succeeds(monkeypatch): + fake = fake_urlopen(2, reset_error(), data=b"OK") + monkeypatch.setattr(get_modflow, "urlopen", fake) + + body, _ = get_modflow.fetch(object(), tries=3, delay=0) + + assert body == b"OK" + assert fake.calls["n"] == 3 + + +def test_fetch_default_tries_from_module(monkeypatch): + # no tries= arg: falls back to the module-level max_http_tries + fake = fake_urlopen(99, reset_error()) + monkeypatch.setattr(get_modflow, "urlopen", fake) + monkeypatch.setattr(get_modflow, "max_http_tries", 4) + + with pytest.raises(URLError): + get_modflow.fetch(object(), delay=0) + assert fake.calls["n"] == 4 + + +def test_fetch_reraises_after_cap(monkeypatch): + fake = fake_urlopen(99, reset_error()) + monkeypatch.setattr(get_modflow, "urlopen", fake) + + with pytest.raises(URLError): + get_modflow.fetch(object(), tries=3, delay=0) + assert fake.calls["n"] == 3 + + +def test_fetch_tries_1_disables_retry(monkeypatch): + fake = fake_urlopen(99, reset_error()) + monkeypatch.setattr(get_modflow, "urlopen", fake) + + with pytest.raises(URLError): + get_modflow.fetch(object(), tries=1) + assert fake.calls["n"] == 1 + + +def test_fetch_non_transient_raises_immediately(monkeypatch): + slept = [] + monkeypatch.setattr(get_modflow.time, "sleep", slept.append) + fake = fake_urlopen(99, HTTPError("u", 401, "Unauthorized", {}, None)) + monkeypatch.setattr(get_modflow, "urlopen", fake) + + with pytest.raises(HTTPError): + get_modflow.fetch(object(), tries=3, delay=0) + assert fake.calls["n"] == 1 + assert slept == [] + + +def test_fetch_404_not_retried(monkeypatch): + slept = [] + monkeypatch.setattr(get_modflow.time, "sleep", slept.append) + fake = fake_urlopen(99, HTTPError("u", 404, "Not Found", {}, None)) + monkeypatch.setattr(get_modflow, "urlopen", fake) + + with pytest.raises(HTTPError) as exc_info: + get_modflow.fetch(object(), tries=3, delay=0) + assert exc_info.value.code == 404 + assert fake.calls["n"] == 1 + assert slept == [] + + +def test_sleep_before_retry_backoff_and_cap(monkeypatch): + slept = [] + monkeypatch.setattr(get_modflow.time, "sleep", slept.append) + err = reset_error() + + get_modflow._sleep_before_retry(1, 3, 2.0, err, quiet=True) + get_modflow._sleep_before_retry(2, 3, 2.0, err, quiet=True) + get_modflow._sleep_before_retry(3, 3, 2.0, err, quiet=True) + assert slept == [2.0, 4.0, 8.0] # exponential + + # ceiling + get_modflow._sleep_before_retry(1, 3, 100.0, err, quiet=True) + assert slept[-1] == get_modflow._max_retry_delay + + # Retry-After header wins when larger + retry_after = HTTPError("u", 503, "err", {"Retry-After": "30"}, None) + get_modflow._sleep_before_retry(1, 3, 2.0, retry_after, quiet=True) + assert slept[-1] == 30.0 + + # zero delay stays zero + get_modflow._sleep_before_retry(5, 3, 0.0, err, quiet=True) + assert slept[-1] == 0.0 + + +def test_download_atomic_success(tmp_path, monkeypatch): + monkeypatch.setattr( + get_modflow, "urlopen", fake_urlopen(1, reset_error(), data=b"PK\x03\x04zip") + ) + dest = tmp_path / "asset.zip" + + request = urllib.request.Request("http://example/asset.zip") + get_modflow.download(request, dest, quiet=True, tries=3, delay=0) + + assert dest.read_bytes() == b"PK\x03\x04zip" + assert not (tmp_path / "asset.zip.part").exists() + + +def test_download_failure_preserves_existing(tmp_path, monkeypatch): + monkeypatch.setattr(get_modflow, "urlopen", fake_urlopen(99, reset_error())) + dest = tmp_path / "asset.zip" + dest.write_bytes(b"OLD-GOOD-CACHE") + + request = urllib.request.Request("http://example/asset.zip") + with pytest.raises(URLError): + get_modflow.download(request, dest, quiet=True, tries=3, delay=0) + + assert dest.read_bytes() == b"OLD-GOOD-CACHE" + assert not (tmp_path / "asset.zip.part").exists() + + +def test_cli_forwards_retry_flags_to_run_main(monkeypatch): + captured = {} + monkeypatch.setattr( + get_modflow, "run_main", lambda **kwargs: captured.update(kwargs) + ) + monkeypatch.setattr( + sys, + "argv", + ["get_modflow.py", str(Path.home()), "--tries", "9", "--retry-delay", "0.5"], + ) + tries_before = get_modflow.max_http_tries + + get_modflow.cli_main() + + assert captured["tries"] == 9 + assert captured["retry_delay"] == 0.5 + assert get_modflow.max_http_tries == tries_before + + +def test_cli_omitted_retry_flags_are_none(monkeypatch): + captured = {} + monkeypatch.setattr( + get_modflow, "run_main", lambda **kwargs: captured.update(kwargs) + ) + monkeypatch.setattr(sys, "argv", ["get_modflow.py", str(Path.home())]) + + get_modflow.cli_main() + + assert captured["tries"] is None + assert captured["retry_delay"] is None + + +def test_cli_accepts_any_repo(monkeypatch): + captured = {} + monkeypatch.setattr( + get_modflow, "run_main", lambda **kwargs: captured.update(kwargs) + ) + monkeypatch.setattr( + sys, + "argv", + ["get_modflow.py", str(Path.home()), "--owner", "me", "--repo", "my-build"], + ) + + get_modflow.cli_main() + + assert captured["owner"] == "me" + assert captured["repo"] == "my-build" + + +def test_run_main_any_repo(tmp_path, monkeypatch): + # a repo that is not an official repository reaches the release assets + release = {"tag_name": "v1", "assets": [], "html_url": "https://example.com"} + monkeypatch.setattr(get_modflow, "get_release", lambda *args, **kwargs: release) + + with pytest.raises(ValueError, match="could not find ostag"): + run_main(tmp_path, owner="me", repo="my-build", ostag="linux", quiet=True) + + @flaky @requires_github @pytest.mark.parametrize("repo", repo_options.keys()) @@ -267,7 +483,7 @@ def test_script(function_tmpdir, owner, repo, downloads_dir): def test_python_api(function_tmpdir, owner, repo, downloads_dir): bindir = str(function_tmpdir) try: - get_modflow(bindir, owner=owner, repo=repo, downloads_dir=downloads_dir) + run_main(bindir, owner=owner, repo=repo, downloads_dir=downloads_dir) except HTTPError as err: if err.code == 403: pytest.skip(f"GitHub {rate_limit_msg}") diff --git a/autotest/test_grid.py b/autotest/test_grid.py index 2cd4e90961..61bb31c858 100644 --- a/autotest/test_grid.py +++ b/autotest/test_grid.py @@ -29,6 +29,7 @@ from flopy.utils.cvfdutil import ( area_of_polygon, centroid_of_polygon, + shared_face, to_cvfd, ) from flopy.utils.gridutil import get_disu_kwargs, get_disv_kwargs @@ -1087,6 +1088,70 @@ def test_grid_crs_exceptions(): sg.set_coord_info(prj=not_a_file) +@pytest.mark.parametrize( + "ivlist1, ivlist2, expected", + [ + ([0, 1, 2, 0], [1, 0, 3, 1], True), + ([0, 1, 2, 0], [0, 2, 3, 0], True), + ([0, 1, 2, 0], [0, 3, 4, 0], False), + ([0, 1, 2, 0], [3, 4, 5, 3], False), + ], + ids=["shared_edge", "closing_edge", "shared_vertex", "disjoint"], +) +def test_shared_face(ivlist1, ivlist2, expected): + assert shared_face(ivlist1, ivlist2) is expected + assert shared_face(ivlist2, ivlist1) is expected + + +@requires_pkg("shapely") +def test_tocvfd_conforming_voronoi(): + # Four conforming Voronoi cells with a legitimate 0.205 m edge h1-h2. + # A nearby vertex must not be inserted into an already shared long face. + h1 = (761.6588, 226.6929) + h2 = (761.8516, 226.7616) + vertdict = { + 0: [ + (1377.0698, 53.9094), + (1531.84, -218.4504), + (983.2838, -719.9512), + (653.5306, -403.9829), + h1, + h2, + (1377.0698, 53.9094), + ], + 1: [ + h1, + (653.5306, -403.9829), + (29.5072, -478.2542), + (-263.5475, 28.6513), + (-84.6841, 419.1418), + (181.5654, 493.3532), + h1, + ], + 2: [ + (1518.7649, 417.5726), + (1377.0698, 53.9094), + h2, + (1010.2436, 646.2797), + (1379.6291, 658.2399), + (1518.7649, 417.5726), + ], + 3: [ + h2, + h1, + (181.5654, 493.3532), + (486.2154, 1094.5056), + (899.4437, 988.0309), + (1010.2436, 646.2797), + h2, + ], + } + + verts, iverts = to_cvfd(vertdict) + for icell, points in vertdict.items(): + np.testing.assert_array_equal(verts[iverts[icell]], points) + + @requires_pkg("shapely") def test_tocvfd1(): vertdict = {} diff --git a/autotest/test_listbudget.py b/autotest/test_listbudget.py index aa6a586f69..b0bcdacdaf 100644 --- a/autotest/test_listbudget.py +++ b/autotest/test_listbudget.py @@ -141,3 +141,73 @@ def test_mtlist(example_data_path): assert len(w) == 1, len(w) assert w[0].category == UserWarning, w[0] assert "error parsing SW mass budget" in str(w[0].message) + + +@pytest.mark.parametrize( + "value, expected", + [ + ("8.5159-100", 8.5159e-100), + ("-8.5159-100", -8.5159e-100), + ("1.0000+100", 1.0e100), + ("-1.2000+150", -1.2e150), + ("3.9204E-103", 3.9204e-103), + ("60.3962", 60.3962), + ], +) +def test_mf6listfile_parse_budget_line_missing_e(value, expected): + line = f" FLOW-JA-FACE = {value:>16} FLOW-JA-FACE = {value:>16}" + entry, flux, cumu = Mf6ListBudget._parse_budget_line(None, line) + assert entry == "FLOW-JA-FACE" + assert flux == expected + assert cumu == expected + + +MF6_BUDGET_TEMPLATE = """ + VOLUME BUDGET FOR ENTIRE MODEL AT END OF TIME STEP 1, STRESS PERIOD {kper} + ------------------------------------------------------------------------------ + + CUMULATIVE VOLUME L**3 RATES FOR THIS TIME STEP L**3/T + + IN: IN: + --- --- + STO = 100.0000 STO = 100.0000 + CHD = {chd:>8} CHD = {chd:>8} + + TOTAL IN = 100.0000 TOTAL IN = 100.0000 + + OUT: OUT: + ---- ---- + STO = 100.0000 STO = 100.0000 + CHD = 0.0000 CHD = 0.0000 + + TOTAL OUT = 100.0000 TOTAL OUT = 100.0000 + + IN - OUT = 0.0000 IN - OUT = 0.0000 + + PERCENT DISCREPANCY = 0.00 PERCENT DISCREPANCY = 0.00 +""" + + +def test_mf6listfile_unparseable_first_entry(function_tmpdir): + # see https://github.com/modflowpy/flopy/issues/2856 + list_file = function_tmpdir / "gwf.lst" + list_file.write_text( + MF6_BUDGET_TEMPLATE.format(kper=1, chd="********") + + MF6_BUDGET_TEMPLATE.format(kper=2, chd="0.0000") + ) + match = r"first budget.*CHD \(IN\) at time step 1, stress period 1: .*\*{8}" + with pytest.raises(ValueError, match=match): + Mf6ListBudget(list_file) + + +def test_mf6listfile_unparseable_later_entry(function_tmpdir): + list_file = function_tmpdir / "gwf.lst" + list_file.write_text( + MF6_BUDGET_TEMPLATE.format(kper=1, chd="0.0000") + + MF6_BUDGET_TEMPLATE.format(kper=2, chd="********") + ) + mflist = Mf6ListBudget(list_file) + inc, cum = mflist.get_budget() + assert inc["STO_IN"][0] == pytest.approx(100.0) + assert np.isnan(inc["STO_IN"][1]) + assert np.isnan(cum["CHD_IN"][1]) diff --git a/autotest/test_mf6.py b/autotest/test_mf6.py index 4a73f90ba1..7a455ba754 100644 --- a/autotest/test_mf6.py +++ b/autotest/test_mf6.py @@ -499,6 +499,50 @@ def test_subdir(function_tmpdir): ) +@pytest.mark.parametrize("sep", ["win", "posix"]) +def test_load_subdir_ext_files_with_any_sep(function_tmpdir, sep): + if sep == "win" and platform.system() != "Windows": + pytest.skip("backslash is not a path separator on this OS") + + # model in a subdirectory with external array and list files + sim = MFSimulation(sim_ws=function_tmpdir) + ModflowTdis(sim) + ims = ModflowIms(sim) + gwf = ModflowGwf(sim, modelname="gwf", model_rel_path="level2") + sim.register_ims_package(ims, [gwf.name]) + botm = np.array([[[-1.0, -2.0, -3.0], [-4.0, -5.0, -6.0]]]) + ModflowGwfdis(gwf, nlay=1, nrow=2, ncol=3, top=10.0, botm=botm) + spd = [[(0, 0, 0), 1.0], [(0, 1, 2), 0.0]] + ModflowGwfchd(gwf, stress_period_data={0: spd}) + sim.set_all_data_external(external_data_folder="dat") + sim.write_simulation() + + # move external files to level2/dat and reference them relative to the + # simulation folder with the given separator, e.g. "level2/dat/botm.txt" + # as written by other tools such as iMOD Python + ext_dir = function_tmpdir / "level2" / "dat" + for ext_file in (function_tmpdir / "dat").glob("*"): + ext_file.rename(ext_dir / ext_file.name) + s = "\\" if sep == "win" else "/" + for pkg_file in (function_tmpdir / "level2").glob("gwf.*"): + lines = pkg_file.read_text().splitlines(keepends=True) + with open(pkg_file, "w") as f: + for l in lines: + tokens = l.split() + if tokens and tokens[0].lower() in ("open/close", "open\\close"): + name = Path(to_posix_sep(tokens[1].strip("'"))).name + tokens[:2] = ["open/close", s.join(["level2", "dat", name])] + l = f" {' '.join(tokens)}\n" + f.write(l) + + # external file paths must resolve to the files written above + sim_r = MFSimulation.load(sim_ws=function_tmpdir) + gwf_r = sim_r.get_model("gwf") + np.testing.assert_allclose(gwf_r.dis.botm.array, botm) + spd_r = gwf_r.chd.stress_period_data.get_data(0) + assert [tuple(r) for r in spd_r] == [tuple(r) for r in spd] + + @requires_exe("mf6") @pytest.mark.parametrize("layered", [True, False]) def test_binary_write(function_tmpdir, layered): diff --git a/autotest/test_model_splitter.py b/autotest/test_model_splitter.py index 6bd90dff82..041ecc4304 100644 --- a/autotest/test_model_splitter.py +++ b/autotest/test_model_splitter.py @@ -174,6 +174,98 @@ def test_model_with_lak_sfr_mvr(function_tmpdir): np.testing.assert_allclose(new_heads, original_heads, err_msg=err_msg) +@requires_exe("mf6") +def test_hfb_model_splitter(function_tmpdir): + nlay, nrow, ncol = 3, 10, 10 + + sim = flopy.mf6.MFSimulation(sim_name="hfb", sim_ws=function_tmpdir, exe_name="mf6") + flopy.mf6.ModflowTdis(sim) + flopy.mf6.ModflowIms( + sim, complexity="simple", outer_dvclose=1e-9, inner_dvclose=1e-10 + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname="hfb", save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=100.0, + delc=100.0, + top=30.0, + botm=[20.0, 10.0, 0.0], + ) + flopy.mf6.ModflowGwfnpf(gwf, k=1.0) + flopy.mf6.ModflowGwfic(gwf, strt=25.0) + chd = [[(0, i, 0), 25.0] for i in range(nrow)] + chd += [[(0, i, ncol - 1), 20.0] for i in range(nrow)] + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd) + + # four barriers forming a plus on the corner shared by the cells + # (4, 4), (4, 5), (5, 4), and (5, 5): two between east west neighbors + # and two between north south neighbors + hfb = [ + [(0, 4, 4), (0, 4, 5), 1e-5], # east west, north of the corner + [(0, 5, 4), (0, 5, 5), 1e-5], # east west, south of the corner + [(0, 4, 4), (0, 5, 4), 1e-5], # north south, west of the corner + [(0, 4, 5), (0, 5, 5), 1e-5], # north south, east of the corner + ] + # a barrier on a vertical connection, which the splitter never divides + # because both of its cells are in the same stack + hfb += [[(0, i, j), (1, i, j), 1e-5] for i in range(3) for j in range(7, 10)] + flopy.mf6.ModflowGwfhfb(gwf, stress_period_data={0: hfb}) + flopy.mf6.ModflowGwfoc(gwf, head_filerecord="hfb.hds", saverecord=[("HEAD", "ALL")]) + sim.write_simulation() + sim.run_simulation() + + original_heads = gwf.output.head().get_alldata()[-1] + + # split away from every barrier + array = np.zeros((nrow, ncol), dtype=int) + array[:, 7:] = 1 + + mfsplit = Mf6Splitter(sim) + new_sim = mfsplit.split_model(array) + new_sim.set_sim_path(function_tmpdir / "split_model") + new_sim.write_simulation() + new_sim.run_simulation() + + heads = {} + nbarrier = 0 + for mkey in (0, 1): + ml = new_sim.get_model(f"hfb_{mkey}") + pkg = ml.get_package("hfb") + if pkg is None: + raise AssertionError(f"Model {mkey} has no HFB package") + + nbarrier += len(pkg.stress_period_data.get_data(0)) + heads[mkey] = ml.output.head().get_alldata()[-1] + + if nbarrier != len(hfb): + raise AssertionError( + f"Split models have {nbarrier} barriers, expected {len(hfb)}" + ) + + new_heads = mfsplit.reconstruct_array(heads) + + err_msg = "Heads from original and split models do not match" + np.testing.assert_allclose(new_heads, original_heads, atol=1e-6, err_msg=err_msg) + + # a barrier cannot be split, both of its cells must be in one model. + # splitting on the column the plus straddles cuts its east west barriers + # and splitting on the row cuts its north south barriers + for axis in (0, 1): + sim = MFSimulation.load(sim_ws=function_tmpdir) + array = np.zeros((nrow, ncol), dtype=int) + if axis == 0: + array[5:, :] = 1 + else: + array[:, 5:] = 1 + + mfsplit = Mf6Splitter(sim) + with pytest.raises(AssertionError, match="split along faults"): + mfsplit.split_model(array) + + @requires_exe("mf6") @requires_pkg("pymetis") @pytest.mark.slow @@ -199,6 +291,14 @@ def test_metis_splitting_with_lak_sfr(function_tmpdir): "optimize_splitting_mask is not correcting for lakes properly" ) + counts = [np.count_nonzero(array == i) for i in np.unique(array)] + ratio = np.max(counts) / np.mean(counts) + if ratio > 1.05: + raise AssertionError( + "load balancing ratio is higher than expected, LAK_EDGE_WEIGHTS " + "may be too low or not properly applied" + ) + new_sim = mfsplit.split_model(array) new_sim.set_sim_path(function_tmpdir / "split_model") new_sim.write_simulation() @@ -218,10 +318,77 @@ def test_metis_splitting_with_lak_sfr(function_tmpdir): np.testing.assert_allclose(new_heads, original_heads, err_msg=err_msg) +@requires_exe("mf6") +@requires_pkg("pymetis") +def test_metis_splitting_with_hfb(function_tmpdir): + import pymetis + + nlay, nrow, ncol = 1, 20, 20 + + sim = flopy.mf6.MFSimulation(sim_name="hfb", sim_ws=function_tmpdir, exe_name="mf6") + flopy.mf6.ModflowTdis(sim) + flopy.mf6.ModflowIms( + sim, complexity="simple", outer_dvclose=1e-9, inner_dvclose=1e-10 + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname="hfb", save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=100.0, + delc=100.0, + top=10.0, + botm=[0.0], + ) + flopy.mf6.ModflowGwfnpf(gwf, k=1.0) + flopy.mf6.ModflowGwfic(gwf, strt=10.0) + chd = [[(0, i, 0), 10.0] for i in range(nrow)] + chd += [[(0, i, ncol - 1), 1.0] for i in range(nrow)] + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd) + + # two barriers that a partition would otherwise be free to cut + hfb = [[(0, i, 6), (0, i, 7), 1e-5] for i in range(nrow)] + hfb += [[(0, 9, j), (0, 10, j), 1e-5] for j in range(ncol)] + flopy.mf6.ModflowGwfhfb(gwf, stress_period_data={0: hfb}) + flopy.mf6.ModflowGwfoc(gwf, head_filerecord="hfb.hds", saverecord=[("HEAD", "ALL")]) + sim.write_simulation() + sim.run_simulation() + + original_heads = gwf.output.head().get_alldata()[-1] + + mfsplit = Mf6Splitter(sim) + array = mfsplit.optimize_splitting_mask( + nparts=4, options=pymetis.Options(seed=42, contig=1) + ) + + # a barrier must lie within one model, not on a model boundary + membership = array.ravel() + for recarray in gwf.get_package("hfb").stress_period_data.data.values(): + _, nodes1 = mfsplit._cellid_to_layer_node(recarray.cellid1) + _, nodes2 = mfsplit._cellid_to_layer_node(recarray.cellid2) + if not np.array_equal(membership[nodes1], membership[nodes2]): + raise AssertionError("A horizontal flow barrier was split") + + new_sim = mfsplit.split_model(array) + new_sim.set_sim_path(function_tmpdir / "split_model") + new_sim.write_simulation() + new_sim.run_simulation() + + heads = {} + for mkey in np.unique(membership): + ml = new_sim.get_model(f"hfb_{mkey}") + heads[mkey] = ml.output.head().get_alldata()[-1] + + new_heads = mfsplit.reconstruct_array(heads) + + err_msg = "Heads from original and split models do not match" + np.testing.assert_allclose(new_heads, original_heads, atol=1e-6, err_msg=err_msg) + + @requires_exe("mf6") @requires_pkg("pymetis") @requires_pkg("h5py") -@requires_pkg("scikit-learn", name_map={"scikit-learn": "sklearn"}) def test_save_load_node_mapping_structured(function_tmpdir): import pymetis @@ -245,19 +412,20 @@ def test_save_load_node_mapping_structured(function_tmpdir): new_sim.set_sim_path(new_sim_path) new_sim.write_simulation() new_sim.run_simulation() - original_node_map = mfsplit._node_map + original_node_map = mfsplit._node_map_arr mfsplit.save_node_mapping(hdf_file) new_sim2 = MFSimulation.load(sim_ws=new_sim_path) mfsplit2 = Mf6Splitter.load_node_mapping(hdf_file) - saved_node_map = mfsplit2._node_map + saved_node_map = mfsplit2._node_map_arr - for k, v1 in original_node_map.items(): - v2 = saved_node_map[k] - if not v1 == v2: - raise AssertionError("Node map read/write not returning proper values") + np.testing.assert_allclose( + original_node_map, + saved_node_map, + err_msg="Node map read/write not returning proper values", + ) array_dict = {} for model in range(nparts): @@ -267,6 +435,7 @@ def test_save_load_node_mapping_structured(function_tmpdir): new_heads = mfsplit2.reconstruct_array(array_dict) err_msg = "Heads from original and split models do not match" + original_heads[sim.get_model().modelgrid.idomain == 0] = 0 np.testing.assert_allclose(new_heads, original_heads, err_msg=err_msg) @@ -375,11 +544,6 @@ def test_save_node_mapping_with_boundnames(function_tmpdir): mfsplit = Mf6Splitter(sim) mfsplit.split_model(array) - non_int_keys = [ - k for k in mfsplit._node_map if not isinstance(k, (int, np.integer)) - ] - assert not non_int_keys, f"boundnames leaked into _node_map: {non_int_keys}" - hdf_file = function_tmpdir / "node_map.hdf5" mfsplit.save_node_mapping(hdf_file) assert hdf_file.exists() @@ -1772,3 +1936,364 @@ def test_ats(function_tmpdir): new_sim.write_simulation() success, _ = new_sim.run_simulation() assert success + + +@requires_exe("mf6") +def test_reconstruct_recarray(function_tmpdir): + sim_path = get_example_data_path() / "mf6-freyberg" + split_path = function_tmpdir / "split_model" + + sim = MFSimulation.load(sim_ws=sim_path) + sim.set_sim_path(function_tmpdir) + sim.write_simulation() + sim.run_simulation() + + gwf = sim.get_model() + modelgrid = gwf.modelgrid + + array = np.ones((modelgrid.nrow, modelgrid.ncol), dtype=int) + ncol = 1 + for row in range(modelgrid.nrow): + if row != 0 and row % 2 == 0: + ncol += 1 + array[row, ncol:] = 2 + + mfsplit = Mf6Splitter(sim) + new_sim = mfsplit.split_model(array) + + new_sim.set_sim_path(split_path) + new_sim.write_simulation() + new_sim.run_simulation() + + ml0 = new_sim.get_model("freyberg_1") + ml1 = new_sim.get_model("freyberg_2") + + pkgs = ["wel", "riv", "chd"] + d = {} + + for pkg in pkgs: + vpak = gwf.get_package(pkg) + vrecarray = vpak.stress_period_data.data[0] + vrecarray.sort(axis=0) + rarrays = {} + for ix, model in enumerate([ml0, ml1]): + pak = model.get_package(pkg) + try: + rarrays[ix + 1] = pak.stress_period_data.data[0] + except (TypeError, AttributeError): + pass + recarray = mfsplit.reconstruct_recarray(rarrays) + recarray.sort(axis=0) + + for ix, rec in enumerate(recarray): + vrec = vrecarray[ix] + for name in recarray.dtype.names: + assert rec[name] == vrec[name], "Recarray reconstruction failed" + + +@requires_exe("mf6") +def test_fjf_obs_structured(function_tmpdir): + sim_ws = function_tmpdir + split_ws = function_tmpdir / "split_model" + + nlay = 1 + nrow = 10 + ncol = 10 + delc = np.full((nrow,), 10) + delr = np.full((ncol,), 10) + top = np.full((nrow, ncol), 50) + botm = np.full((nlay, nrow, ncol), 0) + idomain = np.ones(botm.shape, dtype=int) + + sim = flopy.mf6.MFSimulation(sim_ws=sim_ws) + ims = flopy.mf6.ModflowIms(sim, complexity="SIMPLE") + tdis = flopy.mf6.ModflowTdis(sim) + + gwf = flopy.mf6.ModflowGwf(sim) + + dis = flopy.mf6.ModflowGwfdis( + gwf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delc=delc, + delr=delr, + top=top, + botm=botm, + idomain=idomain, + ) + npf = flopy.mf6.ModflowGwfnpf(gwf, k=5, k33=1) + ic = flopy.mf6.ModflowGwfic(gwf, strt=top - 5) + + chd_rec = [(0, i, 0, 45) for i in range(nrow)] + ghb_rec = [(0, i, 9, 35, 25) for i in range(nrow)] + chd = flopy.mf6.ModflowGwfchd( + gwf, stress_period_data={0: chd_rec}, pname="chd_left" + ) + ghb = flopy.mf6.ModflowGwfghb( + gwf, stress_period_data={0: ghb_rec}, pname="ghb_right" + ) + + obs = flopy.mf6.ModflowUtlobs( + gwf, + continuous={ + "obs.csv": [ + ("faceflow1", "flow-ja-face", (0, 3, 3), (0, 3, 4)), + ("h1", "head", (0, 3, 4)), + ("faceflow2", "flow-ja-face", (0, 6, 6), (0, 6, 7)), + ("h2", "head", (0, 6, 7)), + ] + }, + ) + + sim.write_simulation() + sim.run_simulation() + + array = np.zeros(top.shape, dtype=int) + array[:, (ncol // 2) :] = 1 + mfs = flopy.mf6.utils.Mf6Splitter(sim) + new_sim = mfs.split_model(array, sim_ws=split_ws) + new_sim.write_simulation() + success, _ = new_sim.run_simulation() + assert success, "split model run failed, obs remapping may have failed" + + for mdl in range(2): + obs_ra = new_sim.get_model(f"{gwf.name}_{mdl}").obs.continuous.data[ + f"obs_{mdl}.csv" + ] + assert len(obs_ra) == 2, "number remapped of observation records incorrect" + id2 = obs_ra[obs_ra.obsname == f"faceflow{mdl + 1}"].id2[0] + assert isinstance(id2, tuple), "obs id2 datatype is incorrect" + + if mdl == 0: + v_ra = obs.continuous.data["obs.csv"] + v_id = v_ra[v_ra.obsname == f"faceflow{mdl + 1}"].id2[0] + + assert id2 == v_id, "id2 cellid not properly set" + + +@requires_exe("mf6") +def test_fjf_obs_vertex(function_tmpdir): + sim_ws = get_example_data_path() / "mf6" / "test003_gwfs_disv" + split_ws = function_tmpdir / "split_model" + + sim = flopy.mf6.MFSimulation.load(sim_ws=sim_ws) + sim.set_sim_path(function_tmpdir) + + gwf = sim.get_model() + modelgrid = gwf.modelgrid + ncpl = modelgrid.ncpl + + obs = flopy.mf6.ModflowUtlobs( + gwf, + continuous={ + "obs.csv": [ + ("faceflow1", "flow-ja-face", (0, 24), (0, 34)), + ("h1", "head", (0, 34)), + ("faceflow2", "flow-ja-face", (0, 64), (0, 74)), + ("h2", "head", (0, 74)), + ] + }, + ) + + sim.write_simulation() + sim.run_simulation() + + array = np.zeros((ncpl,), dtype=int) + array[(ncpl // 2) :] = 1 + + mfs = flopy.mf6.utils.Mf6Splitter(sim) + new_sim = mfs.split_model(array, sim_ws=split_ws) + new_sim.write_simulation() + success, _ = new_sim.run_simulation() + assert success, "split model run failed, obs remapping may have failed" + + for mdl in range(2): + obs_ra = new_sim.get_model(f"{gwf.name}_{mdl}").obs.continuous.data[ + f"obs_{mdl}.csv" + ] + assert len(obs_ra) == 2, "number remapped of observation records incorrect" + id2 = obs_ra[obs_ra.obsname == f"faceflow{mdl + 1}"].id2[0] + assert isinstance(id2, tuple), "obs id2 datatype is incorrect" + + if mdl == 0: + v_ra = obs.continuous.data["obs.csv"] + v_id = v_ra[v_ra.obsname == f"faceflow{mdl + 1}"].id2[0] + + assert id2 == v_id, "id2 cellid not properly set" + + +def test_sfr_none_cells(function_tmpdir): + sim_ws = function_tmpdir / "sfr_none_test" + split_ws = sim_ws / "split_model" + + # build the model + sim = flopy.mf6.MFSimulation(sim_ws=sim_ws) + tdis = flopy.mf6.ModflowTdis(sim, perioddata=[(365.0, 365, 1.0)]) + ims = flopy.mf6.ModflowIms(sim, complexity="SIMPLE") + gwf = flopy.mf6.ModflowGwf(sim) + + nlay, nrow, ncol = (1, 10, 10) + delc = np.full((nrow,), 100) + delr = np.full((ncol,), 100) + top = np.ones((nrow, ncol)) * np.linspace(100, 97, 10) + botm = np.zeros((nlay, nrow, ncol)) + idomain = np.ones(botm.shape, dtype=int) + + dis = flopy.mf6.ModflowGwfdis( + gwf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + ) + + npf = flopy.mf6.ModflowGwfnpf(gwf, icelltype=1, k=5.0, k33=1.0) + + sto = flopy.mf6.ModflowGwfsto( + gwf, + iconvert=1, + ss=1e-06, + sy=0.12, + steady_state=[False], + transient=[ + True, + ], + ) + + ic = flopy.mf6.ModflowGwfic(gwf, strt=top - 10) + + recs = [(0, i, 0, 90.0) for i in range(nrow)] + chd_l = flopy.mf6.ModflowGwfchd(gwf, stress_period_data={0: recs}, pname="chd_l") + + recs = [(0, i, 9, 85.0, 100 * 100 * 0.01) for i in range(nrow)] + ghb_l = flopy.mf6.ModflowGwfghb(gwf, stress_period_data={0: recs}, pname="ghb_r") + + sfr_con = [ + (0, -1), + (1, 0, -2), + (2, 1, -3), + (3, 2, 10, -4), + (4, 3, -5), + (5, 4, -6), + (6, 5, -7), + (7, 6, 12, -8), + (8, 7, -9), + (9, 8), + (10, 11, -3), + (11, -10), + (12, 13, -7), + (13, 14, -12), + (14, -13), + ] + + packagedata = [] + rgrd = np.round((top[0, 0] - top[0, 1]) / 100.0, 4) + for c, cons in enumerate(sfr_con): + ncon = len(cons) - 1 + if c < 10: + rec = ( + c, + (0, 4, c), + 100, + 5, + rgrd, + top[4, c] - 4, + 1.0, + 0.005, + 0.025, + ncon, + 1, + 0, + ) + elif c < 12: + rec = ( + c, + (-1, -1, -1), + 100, + 5, + rgrd, + top[4, 3], + 1.0, + 0.005, + 0.025, + ncon, + 1, + 0, + ) + else: + rec = ( + c, + (-1, -1, -1), + 100, + 5, + rgrd, + top[4, 7], + 1.0, + 0.005, + 0.025, + ncon, + 1, + 0, + ) + packagedata.append(rec) + + nreaches = len(packagedata) + + perioddata = [(0, "inflow", 100), (11, "inflow", 50), (14, "inflow", 75)] + + sfr = flopy.mf6.ModflowGwfsfr( + gwf, + nreaches=nreaches, + packagedata=packagedata, + connectiondata=sfr_con, + perioddata={0: perioddata}, + ) + + sim.write_simulation() + sim.run_simulation() + + # split the model so each section has a connected segment + # (multiple reaches) of SFR NONE cells + arr = np.zeros(top.shape, dtype=int) + arr[:, (ncol // 2) :] = 1 + + mfs = Mf6Splitter(sim) + new_sim = mfs.split_model(arr, sim_ws=split_ws) + new_sim.write_simulation() + new_sim.run_simulation() + + valid_nreach = [7, 8] + sfr_none_cells = [2, 3] + for ix in range(2): + nreach = valid_nreach[ix] + none_cells = sfr_none_cells[ix] + recarray = new_sim.get_model(f"model_{ix}").sfr.packagedata.array + assert len(recarray) == nreach, "Wrong number of SFR reaches assigned to model" + + none_cnt = 0 + for cid in recarray.cellid: + if cid == (-1, -1, -1): + none_cnt += 1 + assert none_cnt == none_cells, "Splitter not correctly assigning SFR None cells" + + +@requires_pkg("pymetis") +def test_optimal_mask_force_lak_rebalancing(): + sim_path = get_example_data_path() / "mf6" / "test045_lake2tr" + + sim = MFSimulation.load(sim_ws=sim_path) + mfsplit = Mf6Splitter(sim) + array = mfsplit.optimize_splitting_mask(nparts=15) + + counts = [np.count_nonzero(array == i) for i in np.unique(array)] + ratio = np.max(counts) / np.mean(counts) + if ratio > 1.75: + raise AssertionError( + "load balancing ratio is higher than expected, lak remapping " + "rebalancing adjustments should be checked" + ) diff --git a/autotest/test_postprocessing.py b/autotest/test_postprocessing.py index fcebe5fbf2..5aebc0f21f 100644 --- a/autotest/test_postprocessing.py +++ b/autotest/test_postprocessing.py @@ -149,6 +149,16 @@ def test_get_structured_faceflows(function_tmpdir, nlay, nrow, ncol): assert np.any(fff) == (nrow > 1) assert np.any(flf) == (nlay > 1) + if nlay * nrow * ncol == 3: + # The 1-D cases have unit K and cell dimensions, with all heads + # prescribed as 11, 12, 13. Darcy flow is -1 toward increasing + # column, row, or layer number; the final face has no neighbor. + expected = np.array([-1.0, -1.0, 0.0]).reshape(nlay, nrow, ncol) + for flow, size in zip((frf, fff, flf), (ncol, nrow, nlay)): + if size == 3: + np.testing.assert_allclose(flow, expected, rtol=0.0, atol=1e-8) + assert flow.flat[-1] == 0.0 + @pytest.mark.mf6 @requires_exe("mf6") diff --git a/autotest/test_sfr.py b/autotest/test_sfr.py index 999342f618..77022567d1 100644 --- a/autotest/test_sfr.py +++ b/autotest/test_sfr.py @@ -935,3 +935,26 @@ def test_mf2005(function_tmpdir, namfile): np.array_equal(str2.segment_data[0][name], m.str.segment_data[0][name]) is True ) + + +def test_single_reach_sfr_pkg(function_tmpdir): + """Test for a single segment/reach SFR package""" + sfrfiletxt = ( + "1 1 0 0 86400.00000000 0.00010000 0 0\n" + "1 5 1 1 1 100.0\n" + "1 0 0\n" + "1 1 0 0 100.0 0 0 0.3 0.025\n" + "1.0 1.0 87.5 10.0\n" + "1.0 1.0 86.5 10.0\n" + ) + sfrfile = io.StringIO(sfrfiletxt) + m = Modflow("junk", model_ws=function_tmpdir) + sfr = ModflowSfr2.load(sfrfile, model=m) + assert len(sfr.segment_data[0]) == 1 + assert len(sfr.reach_data) == 1 + + sfrfile2 = function_tmpdir / "junk.sfr" + sfr.write_file() + sfr = ModflowSfr2.load(sfrfile2, model=m) + assert len(sfr.segment_data[0]) == 1 + assert len(sfr.reach_data) == 1 diff --git a/docs/PyPI_release.md b/docs/PyPI_release.md index f79ad3f768..9650a647d7 100644 --- a/docs/PyPI_release.md +++ b/docs/PyPI_release.md @@ -30,4 +30,4 @@ How to Cite *Software/Code citation for FloPy:* -[Bakker, Mark, Post, Vincent, Hughes, J. D., Langevin, C. D., White, J. T., Leaf, A. T., Paulinski, S. R., Bellino, J. C., Morway, E. D., Toews, M. W., Larsen, J. D., Fienen, M. N., Starn, J. J., Brakenhoff, D. A., and Bonelli, W. P., 2026, FloPy v3.11.0: U.S. Geological Survey Software Release, 03 September 2026, https://doi.org/10.5066/F7BK19FH](https://doi.org/10.5066/F7BK19FH) +[Bakker, Mark, Post, Vincent, Hughes, J. D., Langevin, C. D., White, J. T., Leaf, A. T., Paulinski, S. R., Bellino, J. C., Morway, E. D., Toews, M. W., Larsen, J. D., Fienen, M. N., Starn, J. J., Brakenhoff, D. A., and Bonelli, W. P., 2026, FloPy v3.12.0: U.S. Geological Survey Software Release, 31 October 2026, https://doi.org/10.5066/F7BK19FH](https://doi.org/10.5066/F7BK19FH) diff --git a/docs/make_release.md b/docs/make_release.md index a17c8506d0..b88512a9e1 100644 --- a/docs/make_release.md +++ b/docs/make_release.md @@ -72,6 +72,8 @@ Pushing the release branch to the repository triggers the workflow. If the branch name ends with `rc`, it's a dry run and the workflow stops here. If the branch name does not end with `rc` the workflow creates a draft PR from the release branch into the `master` branch. +**Note:** the changelog is generated from commit messages with git-cliff. Development PRs are squash merged, so the PR title becomes that message, and a title that is not a [conventional commit](https://www.conventionalcommits.org/) header is dropped from the notes without warning. [`.github/workflows/pull_request.yml`](../.github/workflows/pull_request.yml) rejects such titles, but it cannot tell whether the type is the right one: a user facing change titled `chore:` still passes the check and is still dropped. Review the generated changelog on the release PR and add anything missing to the section for the version being cut. + ### Merge release branch to master Review the PR and merging if it passes inspection. @@ -86,28 +88,17 @@ Review the release and publish it. Publishing the release triggers a final job t The release workflow assumes [trusted publishing](https://docs.pypi.org/trusted-publishers/) has been configured in the PyPI admin interface. A GitHub environment called `release` is required (however it needs no secrets or environment variables). -For the Conda distribution, there [is a bot](https://github.com/regro-cf-autotick-bot) which will [automatically detect new package versions uploaded to PyPI and create a PR](https://github.com/conda-forge/flopy-feedstock/pull/50) to update the `conda-forge/flopy-feedstock` repository. This PR can be reviewed, updated if needed, and merged to update the package on the `conda-forge` channel. If it becomes necessary to manually publish an update to conda forge, see below. - -### Reset the develop branch - -Make a new branch from `master`: - -```shell -git checkout master -git switch -c post-x.y.z-release-reset -``` +**Note:** PyPI matches a trusted publisher on the organization name, the repository name, the workflow filename and the environment name. Renaming any of them silently invalidates the publisher, and nothing reports it until the next release fails with `invalid-publisher` — possibly long after the rename. After any such rename, update the publisher at to match. -Update the version number for the next development cycle: +For the Conda distribution, there [is a bot](https://github.com/regro-cf-autotick-bot) which will [automatically detect new package versions uploaded to PyPI and create a PR](https://github.com/conda-forge/flopy-feedstock/pull/50) to update the `conda-forge/flopy-feedstock` repository. This PR can be reviewed, updated if needed, and merged to update the package on the `conda-forge` channel. If it becomes necessary to manually publish an update to conda forge, see below. -```shell -python scripts/update_version.py -v x.y.z.dev0 -``` +**Note:** the bot updates only the version number and the checksum. If the release changed FloPy's dependencies or minimum versions, update the recipe's `host` and `run` requirements to match the `Requires-Dist` metadata of the sdist on PyPI before merging. A maintainer can push the correction to the bot's branch. -The version number must comply with [PEP 440](https://peps.python.org/pep-0440/). +### Reset the develop branch -Lint and format Python files: `ruff check .` and `ruff format .` from the project root. +Publishing the release triggers a final job to draft a pull request resetting `develop` from `master`. The job sets the development version for the next cycle with `scripts/update_version.py --post-release`: the next anticipated minor version, with a `.devN` segment where `N` is the patch number just released (e.g. `3.10.0.dev2` after `3.9.2`, `3.12.0.dev0` after `3.11.0`). The version number complies with [PEP 440](https://peps.python.org/pep-0440/). -Create and merge (don't squash) a pull request from this branch into `develop`. +Review the draft PR and merge it — don't squash. Squashing removes the commit history from `develop` and causes it to diverge from `master`. ## Conda diff --git a/etc/environment.yml b/etc/environment.yml index 36284adfca..0ee6058033 100644 --- a/etc/environment.yml +++ b/etc/environment.yml @@ -6,10 +6,10 @@ dependencies: - pip # required - - python>=3.11 - - numpy>=1.20.3 + - python>=3.12 + - numpy>=2.2.0 - matplotlib>=1.4.0 - - pandas>=2.0.0,<3 + - pandas>=2.2.0,<3 # codegen - boltons>=1.0 diff --git a/flopy/mf6/data/mfdataarray.py b/flopy/mf6/data/mfdataarray.py index 0f192263ed..a7b596fe16 100644 --- a/flopy/mf6/data/mfdataarray.py +++ b/flopy/mf6/data/mfdataarray.py @@ -823,6 +823,7 @@ def _get_data(self, layer=None, apply_mult=False, **kwargs): and kwargs["array"] and isinstance(self, MFTransientArray) and data is not [] # noqa: F632 + and data is not None ): data = np.expand_dims(data, 0) return data @@ -1889,7 +1890,10 @@ def _get_array(self, num_sp, apply_mult, **kwargs): if sp in self._data_storage: self.get_data_prep(sp) data = super().get_data(apply_mult=apply_mult, **kwargs) - data = np.expand_dims(data, 0) + if data is not None: + data = np.expand_dims(data, 0) + else: + data = output else: # if there is no previous data provide array of # zeros, otherwise provide last array of data found @@ -2057,6 +2061,13 @@ def to_geodataframe(self, gdf=None, kper=0, full_grid=True, shorten_attr=False, name = f"{self.path[1]}_{self.name}" data = self.get_data(key=kper, apply_mult=True) + if data is None: + per_with_data = np.array([i for i, v in self.empty_keys.items() if not v]) + per_with_data = per_with_data[per_with_data < kper] + if len(per_with_data) == 0: + return gdf + data = self.get_data(key=per_with_data[-1], apply_mult=True) + if data.size == ncpl: name = f"{name}_{kper}" gdf[name] = data.ravel() diff --git a/flopy/mf6/data/mfdatalist.py b/flopy/mf6/data/mfdatalist.py index 86ce4251eb..c821b34f90 100644 --- a/flopy/mf6/data/mfdatalist.py +++ b/flopy/mf6/data/mfdatalist.py @@ -1697,9 +1697,17 @@ def to_geodataframe(self, gdf=None, kper=0, full_grid=True, shorten_attr=False, if gdf is None: gdf = modelgrid.to_geodataframe() + if self.data is None: + return gdf + data = self.to_array(kper=kper, mask=True) if data is None: - return gdf + # get data from the last stress period where data was specified + per_with_data = np.array([i for i, v in self.data.items() if v is not None]) + per_with_data = per_with_data[per_with_data < kper] + if len(per_with_data) == 0: + return gdf + data = self.to_array(kper=per_with_data[-1], mask=True) col_names = [] for name, array3d in data.items(): diff --git a/flopy/mf6/data/mfdataplist.py b/flopy/mf6/data/mfdataplist.py index 669b9af669..46e5d7331a 100644 --- a/flopy/mf6/data/mfdataplist.py +++ b/flopy/mf6/data/mfdataplist.py @@ -5,6 +5,7 @@ import sys import warnings from collections.abc import Iterable +from pathlib import PureWindowsPath import numpy as np import pandas @@ -1480,11 +1481,10 @@ def _resolve_ext_file_path(self, data_storage): # include model relative path in external file path # only if model relative path is not already in external # file path i.e. when reading! - fp_rp_l = fp_relative.split(os.path.sep) - rp_l_r = rel_path.split(os.path.sep)[::-1] - for i, rp in enumerate(rp_l_r): - if rp != fp_rp_l[len(rp_l_r) - i - 1]: - fp_relative = os.path.join(rp, fp_relative) + fp_parts = PureWindowsPath(fp_relative).parts + rp_parts = PureWindowsPath(rel_path).parts + if fp_parts[: len(rp_parts)] != rp_parts: + fp_relative = os.path.join(rel_path, fp_relative) fp = self._simulation_data.mfpath.resolve_path(fp_relative, model_name) else: if fp_relative is not None: @@ -1794,11 +1794,10 @@ def _get_file_path(self): # include model relative path in external file path # only if model relative path is not already in external # file path i.e. when reading! - fp_rp_l = fp_relative.split(os.path.sep) - rp_l_r = rel_path.split(os.path.sep)[::-1] - for i, rp in enumerate(rp_l_r): - if rp != fp_rp_l[len(rp_l_r) - i - 1]: - fp_relative = os.path.join(rp, fp_relative) + fp_parts = PureWindowsPath(fp_relative).parts + rp_parts = PureWindowsPath(rel_path).parts + if fp_parts[: len(rp_parts)] != rp_parts: + fp_relative = os.path.join(rel_path, fp_relative) return self._simulation_data.mfpath.resolve_path( fp_relative, self._model_or_sim.name ) @@ -1968,7 +1967,17 @@ def to_geodataframe( if gdf is None: gdf = modelgrid.to_geodataframe() + if self.data is None: + return gdf + data = self.to_array(kper=kper, mask=True) + if data is None: + # get data from the last stress period where data was specified + per_with_data = np.array([i for i, v in self.data.items() if v is not None]) + per_with_data = per_with_data[per_with_data < kper] + if len(per_with_data) == 0: + return gdf + data = self.to_array(kper=per_with_data[-1], mask=True) col_names = [] for name, array3d in data.items(): diff --git a/flopy/mf6/data/mfdatastorage.py b/flopy/mf6/data/mfdatastorage.py index afcf2596ae..307b98f2c7 100644 --- a/flopy/mf6/data/mfdatastorage.py +++ b/flopy/mf6/data/mfdatastorage.py @@ -3,6 +3,7 @@ import sys from copy import deepcopy from enum import Enum +from pathlib import PureWindowsPath from shutil import copyfile import numpy as np @@ -1710,11 +1711,10 @@ def store_external( # include model relative path in external file path # only if model relative path is not already in external # file path i.e. when reading! - fp_rp_l = fp_relative.split(os.path.sep) - rp_l_r = rel_path.split(os.path.sep)[::-1] - for i, rp in enumerate(rp_l_r): - if rp != fp_rp_l[len(rp_l_r) - i - 1]: - fp_relative = os.path.join(rp, fp_relative) + fp_parts = PureWindowsPath(fp_relative).parts + rp_parts = PureWindowsPath(rel_path).parts + if fp_parts[: len(rp_parts)] != rp_parts: + fp_relative = os.path.join(rel_path, fp_relative) fp = self._simulation_data.mfpath.resolve_path( fp_relative, model_name ) diff --git a/flopy/mf6/mfmodel.py b/flopy/mf6/mfmodel.py index ceeb59c85a..c35ac2587f 100644 --- a/flopy/mf6/mfmodel.py +++ b/flopy/mf6/mfmodel.py @@ -2006,7 +2006,6 @@ def register_package( return path, self.structure.nam_spec - package_extension = package.package_type if set_package_name: # produce a default package name if ( @@ -2021,16 +2020,6 @@ def register_package( not in self._package_container.package_name_dict ): package.package_name = package_name - suffix = package_name.split("_") - if ( - len(suffix) > 1 - and datautil.DatumUtil.is_int(suffix[-1]) - and suffix[-1] != "0" - ): - # update file extension to make unique - package_extension = ( - f"{package_extension}_{suffix[-1]}" - ) break else: package.package_name = package.package_type diff --git a/flopy/mf6/modflow/mfgweest.py b/flopy/mf6/modflow/mfgweest.py index b7e021a94c..82917569bc 100644 --- a/flopy/mf6/modflow/mfgweest.py +++ b/flopy/mf6/modflow/mfgweest.py @@ -159,6 +159,7 @@ class ModflowGweest(MFPackage): "shape (nodes)", "reader readarray", "layered true", + "netcdf true", ], [ "block griddata", @@ -167,6 +168,7 @@ class ModflowGweest(MFPackage): "shape (nodes)", "reader readarray", "layered true", + "netcdf true", "optional true", ], [ @@ -176,6 +178,7 @@ class ModflowGweest(MFPackage): "shape (nodes)", "reader readarray", "layered true", + "netcdf true", "optional true", ], [ @@ -185,6 +188,7 @@ class ModflowGweest(MFPackage): "shape (nodes)", "reader readarray", "layered true", + "netcdf true", "mf6internal cps", ], [ @@ -194,6 +198,7 @@ class ModflowGweest(MFPackage): "shape (nodes)", "reader readarray", "layered true", + "netcdf true", "mf6internal rhos", ], ] diff --git a/flopy/mf6/modflow/mfgwelke.py b/flopy/mf6/modflow/mfgwelke.py index 691058ba25..1305cdb8d0 100644 --- a/flopy/mf6/modflow/mfgwelke.py +++ b/flopy/mf6/modflow/mfgwelke.py @@ -131,7 +131,7 @@ class ModflowGwelke(MFPackage): package supports a 'withdrawal' flow term. if this withdrawal term is active, then water will be withdrawn from the lake at the calculated temperature of the lake. - * temperature : string + * temperature : double precision real or character value that defines the temperature for the lake. the specified temperature is only applied if the lake is a constant temperature lake. if the options block includes a timeseriesfile entry (see the 'time- @@ -144,23 +144,23 @@ class ModflowGwelke(MFPackage): fluxes into or out of the lake and the inactive value will be written for the lake temperature. if a lake is constant, then the temperature for the lake will be fixed at the user specified value. - * rainfall : string + * rainfall : double precision real or character value that defines the rainfall temperature for the lake. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * evaporation : string + * evaporation : double precision use of the evaporation keyword is allowed in the lke package; however, the specified value is not currently used in lke calculations. instead, the latent heat of evaporation is multiplied by the simulated evaporation rate for determining the thermal energy lost from a stream reach. - * runoff : string + * runoff : double precision real or character value that defines the temperature of runoff for the lake. users are free to use whatever temperature scale they want, which might include negative temperatures. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * ext_inflow : string + * ext_inflow : double precision real or character value that defines the temperature of external inflow for the lake. users are free to use whatever temperature scale they want, which might include negative temperatures. if the options block includes a timeseriesfile @@ -218,6 +218,7 @@ class ModflowGwelke(MFPackage): "shape", "reader urword", "optional true", + "mf6internal flow_pkg_name", ], [ "block options", @@ -234,6 +235,7 @@ class ModflowGwelke(MFPackage): "shape", "reader urword", "optional true", + "mf6internal fp_aux_name", ], [ "block options", @@ -249,6 +251,7 @@ class ModflowGwelke(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -256,6 +259,7 @@ class ModflowGwelke(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprconc", ], [ "block options", @@ -263,6 +267,7 @@ class ModflowGwelke(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -270,6 +275,7 @@ class ModflowGwelke(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -279,6 +285,7 @@ class ModflowGwelke(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal temp_filerec", ], [ "block options", @@ -309,6 +316,7 @@ class ModflowGwelke(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -349,6 +357,7 @@ class ModflowGwelke(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -451,12 +460,13 @@ class ModflowGwelke(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal dev_nonexpanding", ], [ "block packagedata", "name packagedata", "type recarray lakeno strt ktf rbthcnd aux boundname", - "shape (maxbound)", + "shape (nlakes)", "reader urword", ], [ @@ -468,6 +478,7 @@ class ModflowGwelke(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -539,6 +550,7 @@ class ModflowGwelke(MFPackage): [ "block period", "name lakeno", + "mf6internal ifno", "type integer", "shape", "tagged false", @@ -567,7 +579,7 @@ class ModflowGwelke(MFPackage): [ "block period", "name temperature", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -577,7 +589,7 @@ class ModflowGwelke(MFPackage): [ "block period", "name rainfall", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -587,7 +599,7 @@ class ModflowGwelke(MFPackage): [ "block period", "name evaporation", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -597,7 +609,7 @@ class ModflowGwelke(MFPackage): [ "block period", "name runoff", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -607,7 +619,7 @@ class ModflowGwelke(MFPackage): [ "block period", "name ext-inflow", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -630,6 +642,7 @@ class ModflowGwelke(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfgwemwe.py b/flopy/mf6/modflow/mfgwemwe.py index ceddcec313..a7693cc6b1 100644 --- a/flopy/mf6/modflow/mfgwemwe.py +++ b/flopy/mf6/modflow/mfgwemwe.py @@ -131,7 +131,7 @@ class ModflowGwemwe(MFPackage): aquifer well package supports a 'withdrawal' flow term. if this withdrawal term is active, then water will be withdrawn from the well at the calculated temperature of the well. - * temperature : string + * temperature : double precision real or character value that defines the temperature for the well. the specified temperature is only applied if the well is a constant temperature well. if the options block includes a timeseriesfile entry (see the 'time- @@ -144,7 +144,7 @@ class ModflowGwemwe(MFPackage): mass fluxes into or out of the well and the inactive value will be written for the well temperature. if a well is constant, then the temperature for the well will be fixed at the user specified value. - * rate : string + * rate : double precision real or character value that defines the injection temperature :math:`(e.g.,:^{circ}c:or:^{circ}f)` for the well. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values @@ -202,6 +202,7 @@ class ModflowGwemwe(MFPackage): "shape", "reader urword", "optional true", + "mf6internal flow_pkg_name", ], [ "block options", @@ -218,6 +219,7 @@ class ModflowGwemwe(MFPackage): "shape", "reader urword", "optional true", + "mf6internal fp_aux_name", ], [ "block options", @@ -233,6 +235,7 @@ class ModflowGwemwe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -240,6 +243,7 @@ class ModflowGwemwe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprconc", ], [ "block options", @@ -247,6 +251,7 @@ class ModflowGwemwe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -254,6 +259,7 @@ class ModflowGwemwe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -263,6 +269,7 @@ class ModflowGwemwe(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal temp_filerec", ], [ "block options", @@ -293,6 +300,7 @@ class ModflowGwemwe(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -333,6 +341,7 @@ class ModflowGwemwe(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -435,12 +444,13 @@ class ModflowGwemwe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal dev_nonexpanding", ], [ "block packagedata", "name packagedata", "type recarray mawno strt ktf fthk aux boundname", - "shape (maxbound)", + "shape (nmawwells)", "reader urword", ], [ @@ -452,6 +462,7 @@ class ModflowGwemwe(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -523,6 +534,7 @@ class ModflowGwemwe(MFPackage): [ "block period", "name mawno", + "mf6internal ifno", "type integer", "shape", "tagged false", @@ -551,7 +563,7 @@ class ModflowGwemwe(MFPackage): [ "block period", "name temperature", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -561,7 +573,7 @@ class ModflowGwemwe(MFPackage): [ "block period", "name rate", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -584,6 +596,7 @@ class ModflowGwemwe(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfgwesfe.py b/flopy/mf6/modflow/mfgwesfe.py index 8323883877..c41da75877 100644 --- a/flopy/mf6/modflow/mfgwesfe.py +++ b/flopy/mf6/modflow/mfgwesfe.py @@ -131,7 +131,7 @@ class ModflowGwesfe(MFPackage): temperatures cannot be specified for all flow terms. for example, the streamflow package supports a 'diversion' flow term. diversion water will be routed using the calculated temperature of the reach. - * temperature : string + * temperature : double precision real or character value that defines the temperature for the reach. the specified temperature is only applied if the reach is a constant temperature reach. if the options block includes a timeseriesfile entry (see the 'time- @@ -144,25 +144,25 @@ class ModflowGwesfe(MFPackage): fluxes into or out of the reach and the inactive value will be written for the reach temperature. if a reach is constant, then the temperature for the reach will be fixed at the user specified value. - * rainfall : string + * rainfall : double precision real or character value that defines the rainfall temperature :math:`(e.g.,:^{circ}c:or:^{circ}f)` for the reach. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * evaporation : string + * evaporation : double precision use of the evaporation keyword is allowed in the sfe package; however, the specified value is not currently used in sfe calculations. instead, the latent heat of evaporation is multiplied by the simulated evaporation rate for determining the thermal energy lost from a stream reach. - * runoff : string + * runoff : double precision real or character value that defines the temperature of runoff :math:`(e.g.,:^{circ}c:or:^{circ}f)` for the reach. users are free to use whatever temperature scale they want, which might include negative temperatures. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * inflow : string + * inflow : double precision real or character value that defines the temperature of inflow :math:`(e.g.,:^{circ}c:or:^{circ}f)` for the reach. users are free to use whatever temperature scale they want, which might include negative @@ -223,6 +223,7 @@ class ModflowGwesfe(MFPackage): "shape", "reader urword", "optional true", + "mf6internal flow_pkg_name", ], [ "block options", @@ -239,6 +240,7 @@ class ModflowGwesfe(MFPackage): "shape", "reader urword", "optional true", + "mf6internal fp_aux_name", ], [ "block options", @@ -254,6 +256,7 @@ class ModflowGwesfe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -261,6 +264,7 @@ class ModflowGwesfe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprconc", ], [ "block options", @@ -268,6 +272,7 @@ class ModflowGwesfe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -275,6 +280,7 @@ class ModflowGwesfe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -284,6 +290,7 @@ class ModflowGwesfe(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal temp_filerec", ], [ "block options", @@ -314,6 +321,7 @@ class ModflowGwesfe(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -354,6 +362,7 @@ class ModflowGwesfe(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -456,12 +465,13 @@ class ModflowGwesfe(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal dev_nonexpanding", ], [ "block packagedata", "name packagedata", "type recarray rno strt ktf rbthcnd aux boundname", - "shape (maxbound)", + "shape (nreaches)", "reader urword", ], [ @@ -473,6 +483,7 @@ class ModflowGwesfe(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -544,6 +555,7 @@ class ModflowGwesfe(MFPackage): [ "block period", "name rno", + "mf6internal ifno", "type integer", "shape", "tagged false", @@ -572,7 +584,7 @@ class ModflowGwesfe(MFPackage): [ "block period", "name temperature", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -582,7 +594,7 @@ class ModflowGwesfe(MFPackage): [ "block period", "name rainfall", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -592,7 +604,7 @@ class ModflowGwesfe(MFPackage): [ "block period", "name evaporation", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -602,7 +614,7 @@ class ModflowGwesfe(MFPackage): [ "block period", "name runoff", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -612,7 +624,7 @@ class ModflowGwesfe(MFPackage): [ "block period", "name inflow", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -635,6 +647,7 @@ class ModflowGwesfe(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfgweuze.py b/flopy/mf6/modflow/mfgweuze.py index bf61bdc0e5..d851afc852 100644 --- a/flopy/mf6/modflow/mfgweuze.py +++ b/flopy/mf6/modflow/mfgweuze.py @@ -124,7 +124,7 @@ class ModflowGweuze(MFPackage): infiltration, uzet, and auxiliary. these settings are used to assign the temperature associated with the corresponding flow terms. temperatures cannot be specified for all flow terms. - * temperature : string + * temperature : double precision real or character value that defines the temperature for the unsaturated zone flow cell. the specified temperature is only applied if the unsaturated zone flow cell is a constant temperature cell. if the options block includes a @@ -138,13 +138,13 @@ class ModflowGweuze(MFPackage): energy fluxes into or out of the uzf cell and the inactive value will be written for the uzf cell temperature. if a uzf cell is constant, then the temperature for the uzf cell will be fixed at the user specified value. - * infiltration : string + * infiltration : double precision real or character value that defines the temperature of the infiltration :math:`(e.g.,:^{circ}c:or:^{circ}f)` for the uzf cell. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * uzet : string + * uzet : double precision real or character value that states what fraction of the simulated unsaturated zone evapotranspiration is associated with evaporation. the evaporative losses from the unsaturated zone moisture content will have an evaporative cooling @@ -205,6 +205,7 @@ class ModflowGweuze(MFPackage): "shape", "reader urword", "optional true", + "mf6internal flow_pkg_name", ], [ "block options", @@ -221,6 +222,7 @@ class ModflowGweuze(MFPackage): "shape", "reader urword", "optional true", + "mf6internal fp_aux_name", ], [ "block options", @@ -236,6 +238,7 @@ class ModflowGweuze(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -243,6 +246,7 @@ class ModflowGweuze(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprconc", ], [ "block options", @@ -250,6 +254,7 @@ class ModflowGweuze(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -257,6 +262,7 @@ class ModflowGweuze(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -266,6 +272,7 @@ class ModflowGweuze(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal temp_filerec", ], [ "block options", @@ -296,6 +303,7 @@ class ModflowGweuze(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -336,6 +344,7 @@ class ModflowGweuze(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -438,12 +447,13 @@ class ModflowGweuze(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal dev_nonexpanding", ], [ "block packagedata", "name packagedata", "type recarray uzfno strt aux boundname", - "shape (maxbound)", + "shape (nuzfcells)", "reader urword", ], [ @@ -455,6 +465,7 @@ class ModflowGweuze(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -508,6 +519,7 @@ class ModflowGweuze(MFPackage): [ "block period", "name uzfno", + "mf6internal ifno", "type integer", "shape", "tagged false", @@ -536,7 +548,7 @@ class ModflowGweuze(MFPackage): [ "block period", "name temperature", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -546,7 +558,7 @@ class ModflowGweuze(MFPackage): [ "block period", "name infiltration", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -556,7 +568,7 @@ class ModflowGweuze(MFPackage): [ "block period", "name uzet", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -579,6 +591,7 @@ class ModflowGweuze(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfgwfdisu.py b/flopy/mf6/modflow/mfgwfdisu.py index 8de18ce067..9801d73f67 100644 --- a/flopy/mf6/modflow/mfgwfdisu.py +++ b/flopy/mf6/modflow/mfgwfdisu.py @@ -143,10 +143,28 @@ class ModflowGwfdisu(MFPackage): only needed if horizontal anisotropy is specified in the npf package, if the xt3d option is used in the npf package, or if the save_specific_discharge option is specified in the npf package. angldegx does not need to be specified - if these conditions are not met. angldegx is of size nja; values specified for - vertical connections and for the diagonal position are not used. note that - angldegx is read in degrees, which is different from modflow-usg, which reads a - similar variable (anglex) in radians. + otherwise. angldegx is of size nja; values specified for vertical connections + and for the diagonal position are not used. note that angldegx is read in + degrees, which is different from modflow-usg, which reads a similar variable + (anglex) in radians. only the value specified for the connection from the + lower to the higher cell number is used, but the values for both directions of + every horizontal connection between active cells are checked. because angldegx + is the outward normal of the shared face, the value for the connection from + cell n to cell m must differ by 180 degrees (within 0.01 degrees) from the + value for the connection from cell m to cell n. if vertices and cell2d are + specified, angldegx must be expressed in the same coordinate system as the + vertex coordinates; the centers of two connected cells must lie on opposite + sides of their shared face, so angldegx must be within 90 degrees of the + direction from the cell center to the center of the connected cell, and, for + cells that share exactly two vertices, angldegx must agree within 1 degree with + the outward normal of the face computed from those vertices. a warning is + issued for each of these three conditions that is not met, and the simulation + terminates with an error if any of them is not met and the npf package requires + angldegx to be specified (xt3d, k22, or save_specific_discharge). a warning is + also issued if angldegx deviates by more than 45 degrees from the direction + between the centers of the connected cells; this is not an error, but such + connections depart from the assumptions of the control-volume finite-difference + method and may warrant a closer look at the grid. vertices : [(iv, xv, yv)] * iv : integer is the vertex number. Records in the VERTICES block must be listed in diff --git a/flopy/mf6/modflow/mfgwflak.py b/flopy/mf6/modflow/mfgwflak.py index fab618f1e8..a6cd4e6599 100644 --- a/flopy/mf6/modflow/mfgwflak.py +++ b/flopy/mf6/modflow/mfgwflak.py @@ -84,11 +84,17 @@ class ModflowGwflak(MFPackage): water mover (mvr) package. when the mover option is specified, additional memory is allocated within the package to store the available, provided, and received water. - dev_force_fallback : keyword - keyword that solves every active lake with the substitution fallback under the - implicit formulation, instead of only those lakes that are too poorly - conditioned to solve as a matrix unknown. this development option is not - supported. + dev_force_legacy : keyword + keyword that solves every active lake with the default (legacy) substitution + solver under the implicit formulation, instead of only those lakes that are too + poorly conditioned to solve as a matrix unknown. cannot be specified with + dev_force_legacy_lake. this development option exists to test that path and is + not supported. + dev_force_legacy_lake : integer + integer lake number that is solved with the default (legacy) substitution + solver under the implicit formulation, while the other lakes are solved as + matrix unknowns. cannot be specified with dev_force_legacy. this development + option exists to test that path and is not supported. dev_groundwater_head_conductance : keyword keyword that calculates the conductance for horizontal connections using the groundwater head instead of the lake stage. this development option is not @@ -114,13 +120,16 @@ class ModflowGwflak(MFPackage): the implicit option is active because the implicit formulation solves the lake stage through its lakebed seepage; the program will terminate with an error if a lake with no groundwater connections is specified with the implicit option. - if a lake stage becomes too poorly conditioned to solve as a matrix unknown, - which can happen for a weakly connected or disconnected lake, that lake is - solved automatically by the default substitution solver instead, so the - implicit formulation remains as robust as the default formulation. the newton - formulation is recommended for lakes that may become perched (disconnected from - the aquifer), because results obtained with the standard rewetting (rewet) - capability can otherwise differ between the two approaches. + the implicit formulation does not yet include the density terms of the buoyancy + (buy) package, and the program will terminate with an error if the implicit + option is specified and the buy package is active. if a lake stage becomes too + poorly conditioned to solve as a matrix unknown, which can happen for a weakly + connected or disconnected lake, that lake is solved automatically by the + default substitution solver instead, so the implicit formulation remains as + robust as the default formulation. the newton formulation is recommended for + lakes that may become perched (disconnected from the aquifer), because results + obtained with the standard rewetting (rewet) capability can otherwise differ + between the two approaches. maximum_iterations : integer integer value that defines the maximum number of newton-raphson iterations allowed for a lake. by default, maximum_iterations is equal to 100. @@ -154,6 +163,9 @@ class ModflowGwflak(MFPackage): and 100.0 when using length units (length_units) of feet, meters, or centimeters in the simulation, respectively. length_conversion does not need to be specified if no lake outlets are specified or length_units are meters. + dev_no_final_check : keyword + flag disabling the final convergence check of the change in lake stages. by + default this check is enabled. nlakes : integer value specifying the number of lakes that will be simulated for all stress periods. @@ -297,26 +309,26 @@ class ModflowGwflak(MFPackage): is defined as a specified flow. MANNING--character keyword to indicate the outlet is defined using Manning's equation. WEIR--character keyword to indicate the outlet is defined using a sharp weir equation. - * invert : string + * invert : double precision real or character value that defines the invert elevation for the lake outlet. A specified INVERT value is only used for active lakes if COUTTYPE for lake outlet OUTLETNO is not SPECIFIED. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * width : string + * width : double precision real or character value that defines the width of the lake outlet. A specified WIDTH value is only used for active lakes if COUTTYPE for lake outlet OUTLETNO is not SPECIFIED. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * rough : string + * rough : double precision real value that defines the roughness coefficient for the lake outlet. Any value can be specified if COUTTYPE is not MANNING. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * slope : string + * slope : double precision real or character value that defines the bed slope for the lake outlet. A specified SLOPE value is only used for active lakes if COUTTYPE for lake outlet OUTLETNO is MANNING. If the Options block includes a TIMESERIESFILE entry (see @@ -336,32 +348,32 @@ class ModflowGwflak(MFPackage): status, stage, rainfall, evaporation, runoff, inflow, withdrawal, and auxiliary. keywords for outlet settings include rate, invert, width, slope, and rough. - * stage : string + * stage : double precision real or character value that defines the stage for the lake. the specified stage is only applied if the lake is a constant stage lake. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * invert : string + * invert : double precision real or character value that defines the invert elevation for the lake outlet. a specified invert value is only used for active lakes if couttype for lake outlet outletno is not specified. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * width : string + * width : double precision real or character value that defines the width of the lake outlet. a specified width value is only used for active lakes if couttype for lake outlet outletno is not specified. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * rough : string + * rough : double precision real value that defines the roughness coefficient for the lake outlet. any value can be specified if couttype is not manning. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * slope : string + * slope : double precision real or character value that defines the bed slope for the lake outlet. a specified slope value is only used for active lakes if couttype for lake outlet outletno is manning. if the options block includes a timeseriesfile entry (see @@ -370,38 +382,38 @@ class ModflowGwflak(MFPackage): * status : string keyword option to define lake status. status can be active, inactive, or constant. by default, status is active. - * rainfall : string + * rainfall : double precision real or character value that defines the rainfall rate :math:`(lt^{-1})` for the lake. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * evaporation : string + * evaporation : double precision real or character value that defines the maximum evaporation rate :math:`(lt^{-1})` for the lake. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * runoff : string + * runoff : double precision real or character value that defines the runoff rate :math:`(l^3 t^{-1})` for the lake. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * inflow : string + * inflow : double precision real or character value that defines the volumetric inflow rate :math:`(l^3 t^{-1})` for the lake. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. by default, inflow rates are zero for each lake. - * withdrawal : string + * withdrawal : double precision real or character value that defines the maximum withdrawal rate :math:`(l^3 t^{-1})` for the lake. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * rate : string + * rate : double precision real or character value that defines the extraction rate for the lake outflow. a positive value indicates inflow and a negative value indicates outflow from the lake. rate only applies to outlets associated with active lakes (status is @@ -485,6 +497,7 @@ class ModflowGwflak(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -499,6 +512,7 @@ class ModflowGwflak(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -506,6 +520,7 @@ class ModflowGwflak(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -519,6 +534,7 @@ class ModflowGwflak(MFPackage): [ "block options", "name stage", + "mf6internal options_stage", "type keyword", "shape", "in_record true", @@ -540,6 +556,7 @@ class ModflowGwflak(MFPackage): [ "block options", "name budget_filerecord", + "mf6internal budfilerec", "type record budget fileout budgetfile", "shape", "reader urword", @@ -580,6 +597,7 @@ class ModflowGwflak(MFPackage): [ "block options", "name budgetcsv_filerecord", + "mf6internal budcsvfilerec", "type record budgetcsv fileout budgetcsvfile", "shape", "reader urword", @@ -610,6 +628,7 @@ class ModflowGwflak(MFPackage): [ "block options", "name package_convergence_filerecord", + "mf6internal pkgconvfilerec", "type record package_convergence fileout package_convergence_filename", "shape", "reader urword", @@ -625,6 +644,7 @@ class ModflowGwflak(MFPackage): "reader urword", "tagged true", "optional false", + "mf6internal pkgconv", ], [ "block options", @@ -635,6 +655,7 @@ class ModflowGwflak(MFPackage): "reader urword", "tagged false", "optional false", + "mf6internal pkgconvfname", ], [ "block options", @@ -661,6 +682,7 @@ class ModflowGwflak(MFPackage): [ "block options", "name filein", + "mf6internal options_filein", "type keyword", "shape", "in_record true", @@ -720,14 +742,24 @@ class ModflowGwflak(MFPackage): ], [ "block options", - "name dev_force_fallback", + "name dev_force_legacy", + "mf6internal iforceleg", "type keyword", "reader urword", "optional true", ], + [ + "block options", + "name dev_force_legacy_lake", + "mf6internal iforceleglak", + "type integer", + "reader urword", + "optional true", + ], [ "block options", "name dev_groundwater_head_conductance", + "mf6internal igwhcopt", "type keyword", "reader urword", "optional true", @@ -735,6 +767,7 @@ class ModflowGwflak(MFPackage): [ "block options", "name dev_maximum_outlet_depth", + "mf6internal outdmax", "type double precision", "reader urword", "optional true", @@ -745,10 +778,12 @@ class ModflowGwflak(MFPackage): "type double precision", "reader urword", "optional true", + "mf6internal surfdep", ], [ "block options", "name implicit", + "mf6internal iimplicit", "type keyword", "reader urword", "optional true", @@ -759,6 +794,7 @@ class ModflowGwflak(MFPackage): "type integer", "reader urword", "optional true", + "mf6internal maxlakit", ], [ "block options", @@ -766,6 +802,7 @@ class ModflowGwflak(MFPackage): "type double precision", "reader urword", "optional true", + "mf6internal dmaxchg", ], [ "block options", @@ -773,6 +810,7 @@ class ModflowGwflak(MFPackage): "type double precision", "reader urword", "optional true", + "mf6internal convtime", ], [ "block options", @@ -780,6 +818,15 @@ class ModflowGwflak(MFPackage): "type double precision", "reader urword", "optional true", + "mf6internal convlength", + ], + [ + "block options", + "name dev_no_final_check", + "type keyword", + "reader urword", + "optional true", + "mf6internal iconvchk", ], [ "block dimensions", @@ -806,7 +853,7 @@ class ModflowGwflak(MFPackage): "block packagedata", "name packagedata", "type recarray ifno strt nlakeconn aux boundname", - "shape (maxbound)", + "shape (nlakes)", "reader urword", ], [ @@ -818,6 +865,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -874,6 +922,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal conndata_ifno", ], [ "block connectiondata", @@ -954,6 +1003,7 @@ class ModflowGwflak(MFPackage): "type recarray ifno tab6 filein tab6_filename", "shape (ntables)", "reader urword", + "optional true", ], [ "block tables", @@ -964,6 +1014,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal tables_ifno", ], [ "block tables", @@ -978,6 +1029,7 @@ class ModflowGwflak(MFPackage): [ "block tables", "name filein", + "mf6internal tables_filein", "type keyword", "shape", "in_record true", @@ -988,6 +1040,7 @@ class ModflowGwflak(MFPackage): [ "block tables", "name tab6_filename", + "mf6internal laktab6_filename", "type string", "preserve_case true", "in_record true", @@ -1001,6 +1054,7 @@ class ModflowGwflak(MFPackage): "type recarray outletno lakein lakeout couttype invert width rough slope", "shape (noutlets)", "reader urword", + "optional true", ], [ "block outlets", @@ -1011,6 +1065,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal outlets_outletno", ], [ "block outlets", @@ -1050,6 +1105,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "time_series true", + "mf6internal outlets_invert", ], [ "block outlets", @@ -1060,6 +1116,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "time_series true", + "mf6internal outlets_width", ], [ "block outlets", @@ -1070,6 +1127,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "time_series true", + "mf6internal outlets_rough", ], [ "block outlets", @@ -1080,6 +1138,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "time_series true", + "mf6internal outlets_slope", ], [ "block period", @@ -1109,6 +1168,7 @@ class ModflowGwflak(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal ifno", ], [ "block period", @@ -1131,8 +1191,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name stage", - "type string", + "type double precision", "shape", + "mf6dimension (nlakes)", "tagged true", "in_record true", "time_series true", @@ -1141,8 +1202,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name rainfall", - "type string", + "type double precision", "shape", + "mf6dimension (nlakes)", "tagged true", "in_record true", "reader urword", @@ -1151,8 +1213,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name evaporation", - "type string", + "type double precision", "shape", + "mf6dimension (nlakes)", "tagged true", "in_record true", "reader urword", @@ -1161,8 +1224,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name runoff", - "type string", + "type double precision", "shape", + "mf6dimension (nlakes)", "tagged true", "in_record true", "reader urword", @@ -1171,8 +1235,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name inflow", - "type string", + "type double precision", "shape", + "mf6dimension (nlakes)", "tagged true", "in_record true", "reader urword", @@ -1181,8 +1246,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name withdrawal", - "type string", + "type double precision", "shape", + "mf6dimension (nlakes)", "tagged true", "in_record true", "reader urword", @@ -1191,8 +1257,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name rate", - "type string", + "type double precision", "shape", + "mf6dimension (noutlets)", "tagged true", "in_record true", "reader urword", @@ -1201,8 +1268,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name invert", - "type string", + "type double precision", "shape", + "mf6dimension (noutlets)", "tagged true", "in_record true", "reader urword", @@ -1211,8 +1279,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name rough", - "type string", + "type double precision", "shape", + "mf6dimension (noutlets)", "tagged true", "in_record true", "reader urword", @@ -1221,8 +1290,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name width", - "type string", + "type double precision", "shape", + "mf6dimension (noutlets)", "tagged true", "in_record true", "reader urword", @@ -1231,8 +1301,9 @@ class ModflowGwflak(MFPackage): [ "block period", "name slope", - "type string", + "type double precision", "shape", + "mf6dimension (noutlets)", "tagged true", "in_record true", "reader urword", @@ -1241,6 +1312,7 @@ class ModflowGwflak(MFPackage): [ "block period", "name auxiliaryrecord", + "mf6internal auxrecord", "type record auxiliary auxname auxval", "shape", "tagged", @@ -1254,6 +1326,7 @@ class ModflowGwflak(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", @@ -1293,7 +1366,8 @@ def __init__( timeseries=None, observations=None, mover=None, - dev_force_fallback=None, + dev_force_legacy=None, + dev_force_legacy_lake=None, dev_groundwater_head_conductance=None, dev_maximum_outlet_depth=None, surfdep=None, @@ -1302,6 +1376,7 @@ def __init__( maximum_stage_change=None, time_conversion=None, length_conversion=None, + dev_no_final_check=None, nlakes=None, noutlets=None, ntables=None, @@ -1349,8 +1424,9 @@ def __init__( "obs", observations, "continuous", self._obs_filerecord ) self.mover = self.build_mfdata("mover", mover) - self.dev_force_fallback = self.build_mfdata( - "dev_force_fallback", dev_force_fallback + self.dev_force_legacy = self.build_mfdata("dev_force_legacy", dev_force_legacy) + self.dev_force_legacy_lake = self.build_mfdata( + "dev_force_legacy_lake", dev_force_legacy_lake ) self.dev_groundwater_head_conductance = self.build_mfdata( "dev_groundwater_head_conductance", dev_groundwater_head_conductance @@ -1370,6 +1446,9 @@ def __init__( self.length_conversion = self.build_mfdata( "length_conversion", length_conversion ) + self.dev_no_final_check = self.build_mfdata( + "dev_no_final_check", dev_no_final_check + ) self.nlakes = self.build_mfdata("nlakes", nlakes) self.noutlets = self.build_mfdata("noutlets", noutlets) self.ntables = self.build_mfdata("ntables", ntables) diff --git a/flopy/mf6/modflow/mfgwfmaw.py b/flopy/mf6/modflow/mfgwfmaw.py index 7d3aa08915..49e7473c59 100644 --- a/flopy/mf6/modflow/mfgwfmaw.py +++ b/flopy/mf6/modflow/mfgwfmaw.py @@ -473,6 +473,7 @@ class ModflowGwfmaw(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -487,6 +488,7 @@ class ModflowGwfmaw(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -494,6 +496,7 @@ class ModflowGwfmaw(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -528,6 +531,7 @@ class ModflowGwfmaw(MFPackage): [ "block options", "name budget_filerecord", + "mf6internal budfilerec", "type record budget fileout budgetfile", "shape", "reader urword", @@ -568,6 +572,7 @@ class ModflowGwfmaw(MFPackage): [ "block options", "name budgetcsv_filerecord", + "mf6internal budcsvfilerec", "type record budgetcsv fileout budgetcsvfile", "shape", "reader urword", @@ -615,6 +620,7 @@ class ModflowGwfmaw(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal inonvert", ], [ "block options", @@ -640,6 +646,7 @@ class ModflowGwfmaw(MFPackage): [ "block options", "name mfrcsv_filerecord", + "mf6internal mfrcsvfilerec", "type record maw_flow_reduce_csv fileout mfrcsvfile", "shape", "reader urword", @@ -649,6 +656,7 @@ class ModflowGwfmaw(MFPackage): [ "block options", "name maw_flow_reduce_csv", + "mf6internal maw_flw_red_csv", "type keyword", "shape", "in_record true", @@ -755,11 +763,13 @@ class ModflowGwfmaw(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ieffradopt", ], [ "block dimensions", "name nmawwells", "type integer", + "mf6internal maxbound", "reader urword", "optional false", ], @@ -779,6 +789,7 @@ class ModflowGwfmaw(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -805,6 +816,7 @@ class ModflowGwfmaw(MFPackage): "shape", "tagged false", "in_record true", + "time_series true", "reader urword", ], [ @@ -861,6 +873,7 @@ class ModflowGwfmaw(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal conndata_ifno", ], [ "block connectiondata", @@ -932,6 +945,7 @@ class ModflowGwfmaw(MFPackage): "tagged false", "in_record true", "reader urword", + "mf6internal angledata_ifno", "numeric_index true", ], [ @@ -942,6 +956,7 @@ class ModflowGwfmaw(MFPackage): "tagged false", "in_record true", "reader urword", + "mf6internal angledata_icon", "numeric_index true", ], [ @@ -963,6 +978,7 @@ class ModflowGwfmaw(MFPackage): "in_record true", "reader urword", "optional true", + "mf6internal connlen", ], [ "block period", @@ -1014,6 +1030,7 @@ class ModflowGwfmaw(MFPackage): [ "block period", "name flowing_wellrecord", + "mf6internal flwwellrecord", "type record flowing_well fwelev fwcond fwrlen", "shape", "tagged", @@ -1122,6 +1139,7 @@ class ModflowGwfmaw(MFPackage): [ "block period", "name rate_scalingrecord", + "mf6internal ratescalingrec", "type record rate_scaling pump_elevation scaling_length", "shape", "tagged", @@ -1170,6 +1188,7 @@ class ModflowGwfmaw(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfgwfmvr.py b/flopy/mf6/modflow/mfgwfmvr.py index 9955dfdc15..1ea1af7e2d 100644 --- a/flopy/mf6/modflow/mfgwfmvr.py +++ b/flopy/mf6/modflow/mfgwfmvr.py @@ -60,7 +60,7 @@ class ModflowGwfmvr(MFPackage): provided by the user, then packages are assigned a default value, which is the package acronym followed by a hyphen and the package number. For example, the first Drain Package is named DRN-1. The second Drain Package is named DRN-2, - and so forth. + and so forth. Each package may be listed only once in the PACKAGES block. perioddata : [(mname1, pname1, id1, mname2, pname2, id2, mvrtype, value)] * mname1 : string diff --git a/flopy/mf6/modflow/mfgwfsfr.py b/flopy/mf6/modflow/mfgwfsfr.py index 753582615b..e224c6b062 100644 --- a/flopy/mf6/modflow/mfgwfsfr.py +++ b/flopy/mf6/modflow/mfgwfsfr.py @@ -144,6 +144,9 @@ class ModflowGwfsfr(MFPackage): keyword that deactivates the checks of reach geometry relative to the model grid and of reach parameters for reasonable values. this development option is not supported. + dev_no_final_check : keyword + keyword that deactivates the final convergence check of the change in stream + flow routing stages and flows. this development option is not supported. nreaches : integer integer value specifying the number of stream reaches. there must be nreaches entries in the packagedata block. @@ -184,7 +187,7 @@ class ModflowGwfsfr(MFPackage): block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * man : string + * man : double precision real or character value that defines the Manning's roughness coefficient for the reach. MAN must be greater than zero. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be @@ -307,7 +310,7 @@ class ModflowGwfsfr(MFPackage): that can be used to start the sfrsetting string include: status, bedk, manning, stage, inflow, rainfall, evaporation, runoff, diversion, upstream_fraction, and auxiliary. - * stage : string + * stage : double precision real or character value that defines the stage for the reach. the specified stage is only applied if the reach uses the simple routing option. if stage is not specified for reaches that use the simple routing option, the specified @@ -328,33 +331,33 @@ class ModflowGwfsfr(MFPackage): and inactive downstream reach should be changed to 1.0 and 0.0, respectively, to ensure that the active reach receives all of the downstream outflow from the upstream reach. by default, status is active. - * bedk : string + * bedk : double precision real or character value that defines the hydraulic conductivity of the reach streambed. bedk can be any positive value if the reach is not connected to an underlying gwf cell. otherwise, bedk must be greater than zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * manning : string + * manning : double precision real or character value that defines the manning's roughness coefficient for the reach. manning must be greater than zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * inflow : string + * inflow : double precision real or character value that defines the volumetric inflow rate for the streamflow routing reach. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. by default, inflow rates are zero for each reach. - * rainfall : string + * rainfall : double precision real or character value that defines the volumetric rate per unit area of water added by precipitation directly on the streamflow routing reach. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. by default, rainfall rates are zero for each reach. - * evaporation : string + * evaporation : double precision real or character value that defines the volumetric rate per unit area of water subtracted by evaporation from the streamflow routing reach. a positive evaporation rate should be provided. if the options block includes a @@ -365,7 +368,7 @@ class ModflowGwfsfr(MFPackage): runoff but excluding groundwater leakage into the reach) the volumetric evaporation rate is limited to the sources of water to the reach. by default, evaporation rates are zero for each reach. - * runoff : string + * runoff : double precision real or character value that defines the volumetric rate of diffuse overland runoff that enters the streamflow routing reach. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be @@ -502,6 +505,7 @@ class ModflowGwfsfr(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -516,6 +520,7 @@ class ModflowGwfsfr(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -523,6 +528,7 @@ class ModflowGwfsfr(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -536,6 +542,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name stage", + "mf6internal options_stage", "type keyword", "shape", "in_record true", @@ -557,6 +564,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name budget_filerecord", + "mf6internal budfilerec", "type record budget fileout budgetfile", "shape", "reader urword", @@ -597,6 +605,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name budgetcsv_filerecord", + "mf6internal budcsvfilerec", "type record budgetcsv fileout budgetcsvfile", "shape", "reader urword", @@ -627,6 +636,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name package_convergence_filerecord", + "mf6internal pkgconvfilerec", "type record package_convergence fileout package_convergence_filename", "shape", "reader urword", @@ -636,6 +646,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name package_convergence", + "mf6internal pkgconv", "type keyword", "shape", "in_record true", @@ -646,6 +657,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name package_convergence_filename", + "mf6internal pkgconvfname", "type string", "shape", "in_record true", @@ -678,6 +690,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name filein", + "mf6internal options_filein", "type keyword", "shape", "in_record true", @@ -738,6 +751,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name maximum_picard_iterations", + "mf6internal maxsfrpicard", "type integer", "reader urword", "optional true", @@ -745,6 +759,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name maximum_iterations", + "mf6internal maxsfrit", "type integer", "reader urword", "optional true", @@ -752,6 +767,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name maximum_depth_change", + "mf6internal dmaxchg", "type double precision", "reader urword", "optional true", @@ -767,6 +783,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name length_conversion", + "mf6internal lengthconv", "type double precision", "reader urword", "optional true", @@ -781,6 +798,7 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name dev_storage_weight", + "mf6internal storage_weight", "type double precision", "reader urword", "optional true", @@ -788,6 +806,15 @@ class ModflowGwfsfr(MFPackage): [ "block options", "name dev_no_check", + "mf6internal icheck", + "type keyword", + "reader urword", + "optional true", + ], + [ + "block options", + "name dev_no_final_check", + "mf6internal iconvchk", "type keyword", "reader urword", "optional true", @@ -796,6 +823,7 @@ class ModflowGwfsfr(MFPackage): "block dimensions", "name nreaches", "type integer", + "mf6internal maxbound", "reader urword", "optional false", ], @@ -803,12 +831,13 @@ class ModflowGwfsfr(MFPackage): "block packagedata", "name packagedata", "type recarray ifno cellid rlen rwid rgrd rtp rbth rhk man ncon ustrf ndv aux boundname", - "shape (maxbound)", + "shape (nreaches)", "reader urword", ], [ "block packagedata", "name ifno", + "mf6internal packagedata_ifno", "type integer", "shape", "tagged false", @@ -883,7 +912,7 @@ class ModflowGwfsfr(MFPackage): [ "block packagedata", "name man", - "type string", + "type double precision", "shape", "tagged false", "in_record true", @@ -945,12 +974,13 @@ class ModflowGwfsfr(MFPackage): "type recarray ifno tab6 filein tab6_filename", "shape", "valid", - "optional false", + "optional true", "reader urword", ], [ "block crosssections", "name ifno", + "mf6internal crosssect_ifno", "type integer", "shape", "tagged false", @@ -961,6 +991,7 @@ class ModflowGwfsfr(MFPackage): [ "block crosssections", "name tab6", + "mf6internal xs_tab6", "type keyword", "shape", "in_record true", @@ -971,6 +1002,7 @@ class ModflowGwfsfr(MFPackage): [ "block crosssections", "name filein", + "mf6internal xs_filein", "type keyword", "shape", "in_record true", @@ -981,6 +1013,7 @@ class ModflowGwfsfr(MFPackage): [ "block crosssections", "name tab6_filename", + "mf6internal sfrtab6_filename", "type string", "preserve_case true", "in_record true", @@ -992,12 +1025,13 @@ class ModflowGwfsfr(MFPackage): "block connectiondata", "name connectiondata", "type recarray ifno ic", - "shape (maxbound)", + "shape (nreaches)", "reader urword", ], [ "block connectiondata", "name ifno", + "mf6internal conndata_ifno", "type integer", "shape", "tagged false", @@ -1021,12 +1055,14 @@ class ModflowGwfsfr(MFPackage): "block diversions", "name diversions", "type recarray ifno idv iconr cprior", - "shape (maxbound)", + "shape", + "optional true", "reader urword", ], [ "block diversions", "name ifno", + "mf6internal diversions_ifno", "type integer", "shape", "tagged false", @@ -1037,6 +1073,7 @@ class ModflowGwfsfr(MFPackage): [ "block diversions", "name idv", + "mf6internal diversions_idv", "type integer", "shape", "tagged false", @@ -1067,14 +1104,15 @@ class ModflowGwfsfr(MFPackage): "block initialstages", "name initialstages", "type recarray ifno initialstage", - "shape (maxbound)", + "shape (nreaches)", "valid", - "optional false", + "optional true", "reader urword", ], [ "block initialstages", "name ifno", + "mf6internal initstage_ifno", "type integer", "shape", "tagged false", @@ -1143,7 +1181,7 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name bedk", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -1153,7 +1191,7 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name manning", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -1163,7 +1201,7 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name stage", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -1173,7 +1211,7 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name inflow", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -1183,7 +1221,7 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name rainfall", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -1193,7 +1231,7 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name evaporation", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -1203,7 +1241,7 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name runoff", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -1250,15 +1288,18 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name upstream_fraction", + "mf6internal upstream_frac", "type double precision", "shape", "tagged true", "in_record true", "reader urword", + "time_series true", ], [ "block period", "name cross_sectionrecord", + "mf6internal xsectionrecord", "type record cross_section tab6 filein tab6_filename", "shape", "tagged", @@ -1317,6 +1358,7 @@ class ModflowGwfsfr(MFPackage): [ "block period", "name auxiliary", + "mf6internal period_auxiliary", "type keyword", "shape", "in_record true", @@ -1370,6 +1412,7 @@ def __init__( time_conversion=None, dev_storage_weight=None, dev_no_check=None, + dev_no_final_check=None, nreaches=None, packagedata=None, crosssections=None, @@ -1436,6 +1479,9 @@ def __init__( "dev_storage_weight", dev_storage_weight ) self.dev_no_check = self.build_mfdata("dev_no_check", dev_no_check) + self.dev_no_final_check = self.build_mfdata( + "dev_no_final_check", dev_no_final_check + ) self.nreaches = self.build_mfdata("nreaches", nreaches) self.packagedata = self.build_mfdata("packagedata", packagedata) self.crosssections = self.build_mfdata("crosssections", crosssections) diff --git a/flopy/mf6/modflow/mfgwfuzf.py b/flopy/mf6/modflow/mfgwfuzf.py index 89358247bd..9374ca6d99 100644 --- a/flopy/mf6/modflow/mfgwfuzf.py +++ b/flopy/mf6/modflow/mfgwfuzf.py @@ -178,12 +178,12 @@ class ModflowGwfuzf(MFPackage): * ifno : integer integer value that defines the feature (UZF object) number associated with the specified PERIOD data on the line. - * finf : string + * finf : double precision real or character value that defines the applied infiltration rate of the UZF cell (:math:`LT^{-1}`). If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * pet : string + * pet : double precision real or character value that defines the potential evapotranspiration rate of the UZF cell and specified GWF cell. Evapotranspiration is first removed from the unsaturated zone and any remaining potential evapotranspiration is applied @@ -193,7 +193,7 @@ class ModflowGwfuzf(MFPackage): specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * extdp : string + * extdp : double precision real or character value that defines the evapotranspiration extinction depth of the UZF cell. If IVERTCON is greater than zero and EXTDP extends below the GWF cell bottom then remaining potential evapotranspiration is applied to the @@ -202,7 +202,7 @@ class ModflowGwfuzf(MFPackage): TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * extwc : string + * extwc : double precision real or character value that defines the evapotranspiration extinction water content of the UZF cell. EXTWC is always specified, but is only used if SIMULATE_ET and UNSAT_ETWC are specified in the OPTIONS block. The @@ -213,20 +213,20 @@ class ModflowGwfuzf(MFPackage): If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * ha : string + * ha : double precision real or character value that defines the air entry potential (head) of the UZF cell. HA is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * hroot : string + * hroot : double precision real or character value that defines the root potential (head) of the UZF cell. HROOT is always specified, but is only used if SIMULATE_ET and UNSAT_ETAE are specified in the OPTIONS block. If the Options block includes a TIMESERIESFILE entry (see the 'Time-Variable Input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * rootact : string + * rootact : double precision real or character value that defines the root activity function of the UZF cell. ROOTACT is the length of roots in a given volume of soil divided by that volume. Values range from 0 to about 3 :math:`cm^{-2}`, depending on the plant @@ -303,6 +303,7 @@ class ModflowGwfuzf(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -317,6 +318,7 @@ class ModflowGwfuzf(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -356,6 +358,7 @@ class ModflowGwfuzf(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -396,6 +399,7 @@ class ModflowGwfuzf(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -426,6 +430,7 @@ class ModflowGwfuzf(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal pkgconvfilerec", ], [ "block options", @@ -436,6 +441,7 @@ class ModflowGwfuzf(MFPackage): "reader urword", "tagged true", "optional false", + "mf6internal pkgconv", ], [ "block options", @@ -446,6 +452,7 @@ class ModflowGwfuzf(MFPackage): "reader urword", "tagged false", "optional false", + "mf6internal pkgconvfname", ], [ "block options", @@ -532,6 +539,7 @@ class ModflowGwfuzf(MFPackage): [ "block options", "name dev_no_final_check", + "mf6internal iconvchk", "type keyword", "reader urword", "optional true", @@ -591,6 +599,7 @@ class ModflowGwfuzf(MFPackage): "type integer", "reader urword", "optional false", + "mf6internal maxbound", ], [ "block dimensions", @@ -618,6 +627,7 @@ class ModflowGwfuzf(MFPackage): [ "block packagedata", "name ifno", + "mf6internal packagedata_ifno", "type integer", "shape", "tagged false", @@ -749,7 +759,7 @@ class ModflowGwfuzf(MFPackage): [ "block period", "name finf", - "type string", + "type double precision", "shape", "tagged false", "in_record true", @@ -759,7 +769,7 @@ class ModflowGwfuzf(MFPackage): [ "block period", "name pet", - "type string", + "type double precision", "shape", "tagged false", "in_record true", @@ -769,7 +779,7 @@ class ModflowGwfuzf(MFPackage): [ "block period", "name extdp", - "type string", + "type double precision", "shape", "tagged false", "in_record true", @@ -779,7 +789,7 @@ class ModflowGwfuzf(MFPackage): [ "block period", "name extwc", - "type string", + "type double precision", "shape", "tagged false", "in_record true", @@ -789,7 +799,7 @@ class ModflowGwfuzf(MFPackage): [ "block period", "name ha", - "type string", + "type double precision", "shape", "tagged false", "in_record true", @@ -799,7 +809,7 @@ class ModflowGwfuzf(MFPackage): [ "block period", "name hroot", - "type string", + "type double precision", "shape", "tagged false", "in_record true", @@ -809,7 +819,7 @@ class ModflowGwfuzf(MFPackage): [ "block period", "name rootact", - "type string", + "type double precision", "shape", "tagged false", "in_record true", diff --git a/flopy/mf6/modflow/mfgwtfmi.py b/flopy/mf6/modflow/mfgwtfmi.py index 15e4130f53..349eead71c 100644 --- a/flopy/mf6/modflow/mfgwtfmi.py +++ b/flopy/mf6/modflow/mfgwtfmi.py @@ -33,12 +33,12 @@ class ModflowGwtfmi(MFPackage): convergence tolerances. packagedata : [(flowtype, filein, fname)] * flowtype : string - is the word GWFBUDGET, GWFHEAD, GWFMOVER or the name of an advanced GWF stress - package. If GWFBUDGET is specified, then the corresponding file must be a - budget file. If GWFHEAD is specified, the file must be a head file. If GWFGRID - is specified, the file must be a binary grid file. If an advanced GWF stress - package name appears then the corresponding file must be the budget file saved - by a LAK, SFR, MAW or UZF Package. + is the word GWFBUDGET, GWFHEAD, GWFGRID, GWFMOVER or the name of an advanced + GWF stress package. If GWFBUDGET is specified, then the corresponding file + must be a budget file. If GWFHEAD is specified, the file must be a head file. + If GWFGRID is specified, the file must be a binary grid file. If an advanced + GWF stress package name appears then the corresponding file must be the budget + file saved by a LAK, SFR, MAW or UZF Package. * filein : keyword keyword to specify that an input filename is expected next. * fname : string diff --git a/flopy/mf6/modflow/mfgwtlkt.py b/flopy/mf6/modflow/mfgwtlkt.py index 38ee2e07c5..9bfb5faef7 100644 --- a/flopy/mf6/modflow/mfgwtlkt.py +++ b/flopy/mf6/modflow/mfgwtlkt.py @@ -125,7 +125,7 @@ class ModflowGwtlkt(MFPackage): the lake package supports a 'withdrawal' flow term. if this withdrawal term is active, then water will be withdrawn from the lake at the calculated concentration of the lake. - * concentration : string + * concentration : double precision real or character value that defines the concentration for the lake. the specified concentration is only applied if the lake is a constant concentration lake. if the options block includes a timeseriesfile entry (see the 'time- @@ -138,12 +138,12 @@ class ModflowGwtlkt(MFPackage): mass fluxes into or out of the lake and the inactive value will be written for the lake concentration. if a lake is constant, then the concentration for the lake will be fixed at the user specified value. - * rainfall : string + * rainfall : double precision real or character value that defines the rainfall solute concentration :math:`(ml^{-3})` for the lake. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * evaporation : string + * evaporation : double precision real or character value that defines the concentration of evaporated water :math:`(ml^{-3})` for the lake. if this concentration value is larger than the simulated concentration in the lake, then the evaporated water will be removed @@ -151,13 +151,13 @@ class ModflowGwtlkt(MFPackage): timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * runoff : string + * runoff : double precision real or character value that defines the concentration of runoff :math:`(ml^{-3})` for the lake. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * ext_inflow : string + * ext_inflow : double precision real or character value that defines the concentration of external inflow :math:`(ml^{-3})` for the lake. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable @@ -215,6 +215,7 @@ class ModflowGwtlkt(MFPackage): "shape", "reader urword", "optional true", + "mf6internal flow_pkg_name", ], [ "block options", @@ -231,6 +232,7 @@ class ModflowGwtlkt(MFPackage): "shape", "reader urword", "optional true", + "mf6internal fp_aux_name", ], [ "block options", @@ -246,6 +248,7 @@ class ModflowGwtlkt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -253,6 +256,7 @@ class ModflowGwtlkt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprconc", ], [ "block options", @@ -260,6 +264,7 @@ class ModflowGwtlkt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -267,6 +272,7 @@ class ModflowGwtlkt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -276,6 +282,7 @@ class ModflowGwtlkt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal conc_filerec", ], [ "block options", @@ -306,6 +313,7 @@ class ModflowGwtlkt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -346,6 +354,7 @@ class ModflowGwtlkt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -448,12 +457,13 @@ class ModflowGwtlkt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal dev_nonexpanding", ], [ "block packagedata", "name packagedata", "type recarray ifno strt aux boundname", - "shape (maxbound)", + "shape (nlakes)", "reader urword", ], [ @@ -465,6 +475,7 @@ class ModflowGwtlkt(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -546,7 +557,7 @@ class ModflowGwtlkt(MFPackage): [ "block period", "name concentration", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -556,7 +567,7 @@ class ModflowGwtlkt(MFPackage): [ "block period", "name rainfall", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -566,7 +577,7 @@ class ModflowGwtlkt(MFPackage): [ "block period", "name evaporation", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -576,7 +587,7 @@ class ModflowGwtlkt(MFPackage): [ "block period", "name runoff", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -586,7 +597,7 @@ class ModflowGwtlkt(MFPackage): [ "block period", "name ext-inflow", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -609,6 +620,7 @@ class ModflowGwtlkt(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfgwtmwt.py b/flopy/mf6/modflow/mfgwtmwt.py index 2cdcd4f487..91dee8056d 100644 --- a/flopy/mf6/modflow/mfgwtmwt.py +++ b/flopy/mf6/modflow/mfgwtmwt.py @@ -125,7 +125,7 @@ class ModflowGwtmwt(MFPackage): supports a 'withdrawal' flow term. if this withdrawal term is active, then water will be withdrawn from the well at the calculated concentration of the well. - * concentration : string + * concentration : double precision real or character value that defines the concentration for the well. the specified concentration is only applied if the well is a constant concentration well. if the options block includes a timeseriesfile entry (see the 'time- @@ -138,7 +138,7 @@ class ModflowGwtmwt(MFPackage): mass fluxes into or out of the well and the inactive value will be written for the well concentration. if a well is constant, then the concentration for the well will be fixed at the user specified value. - * rate : string + * rate : double precision real or character value that defines the injection solute concentration :math:`(ml^{-3})` for the well. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a @@ -195,6 +195,7 @@ class ModflowGwtmwt(MFPackage): "shape", "reader urword", "optional true", + "mf6internal flow_pkg_name", ], [ "block options", @@ -211,6 +212,7 @@ class ModflowGwtmwt(MFPackage): "shape", "reader urword", "optional true", + "mf6internal fp_aux_name", ], [ "block options", @@ -226,6 +228,7 @@ class ModflowGwtmwt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -233,6 +236,7 @@ class ModflowGwtmwt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprconc", ], [ "block options", @@ -240,6 +244,7 @@ class ModflowGwtmwt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -247,6 +252,7 @@ class ModflowGwtmwt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -256,6 +262,7 @@ class ModflowGwtmwt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal conc_filerec", ], [ "block options", @@ -286,6 +293,7 @@ class ModflowGwtmwt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -326,6 +334,7 @@ class ModflowGwtmwt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -428,12 +437,13 @@ class ModflowGwtmwt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal dev_nonexpanding", ], [ "block packagedata", "name packagedata", "type recarray ifno strt aux boundname", - "shape (maxbound)", + "shape (nmawwells)", "reader urword", ], [ @@ -445,6 +455,7 @@ class ModflowGwtmwt(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -526,7 +537,7 @@ class ModflowGwtmwt(MFPackage): [ "block period", "name concentration", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -536,7 +547,7 @@ class ModflowGwtmwt(MFPackage): [ "block period", "name rate", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -559,6 +570,7 @@ class ModflowGwtmwt(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfgwtsft.py b/flopy/mf6/modflow/mfgwtsft.py index 33b7d56511..4c28adf83c 100644 --- a/flopy/mf6/modflow/mfgwtsft.py +++ b/flopy/mf6/modflow/mfgwtsft.py @@ -125,7 +125,7 @@ class ModflowGwtsft(MFPackage): terms. concentrations cannot be specified for all flow terms. for example, the streamflow package supports a 'diversion' flow term. diversion water will be routed using the calculated concentration of the reach. - * concentration : string + * concentration : double precision real or character value that defines the concentration for the reach. the specified concentration is only applied if the reach is a constant concentration reach. if the options block includes a timeseriesfile entry (see @@ -138,12 +138,12 @@ class ModflowGwtsft(MFPackage): mass fluxes into or out of the reach and the inactive value will be written for the reach concentration. if a reach is constant, then the concentration for the reach will be fixed at the user specified value. - * rainfall : string + * rainfall : double precision real or character value that defines the rainfall solute concentration :math:`(ml^{-3})` for the reach. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * evaporation : string + * evaporation : double precision real or character value that defines the concentration of evaporated water :math:`(ml^{-3})` for the reach. if this concentration value is larger than the simulated concentration in the reach, then the evaporated water will be removed @@ -151,13 +151,13 @@ class ModflowGwtsft(MFPackage): timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * runoff : string + * runoff : double precision real or character value that defines the concentration of runoff :math:`(ml^{-3})` for the reach. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * inflow : string + * inflow : double precision real or character value that defines the concentration of inflow :math:`(ml^{-3})` for the reach. value must be greater than or equal to zero. if the options block includes a timeseriesfile entry (see the 'time-variable @@ -217,6 +217,7 @@ class ModflowGwtsft(MFPackage): "shape", "reader urword", "optional true", + "mf6internal flow_pkg_name", ], [ "block options", @@ -233,6 +234,7 @@ class ModflowGwtsft(MFPackage): "shape", "reader urword", "optional true", + "mf6internal fp_aux_name", ], [ "block options", @@ -248,6 +250,7 @@ class ModflowGwtsft(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -255,6 +258,7 @@ class ModflowGwtsft(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprconc", ], [ "block options", @@ -262,6 +266,7 @@ class ModflowGwtsft(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -269,6 +274,7 @@ class ModflowGwtsft(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -278,6 +284,7 @@ class ModflowGwtsft(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal conc_filerec", ], [ "block options", @@ -308,6 +315,7 @@ class ModflowGwtsft(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -348,6 +356,7 @@ class ModflowGwtsft(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -450,12 +459,13 @@ class ModflowGwtsft(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal dev_nonexpanding", ], [ "block packagedata", "name packagedata", "type recarray ifno strt aux boundname", - "shape (maxbound)", + "shape (nreaches)", "reader urword", ], [ @@ -467,6 +477,7 @@ class ModflowGwtsft(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -548,7 +559,7 @@ class ModflowGwtsft(MFPackage): [ "block period", "name concentration", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -558,7 +569,7 @@ class ModflowGwtsft(MFPackage): [ "block period", "name rainfall", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -568,7 +579,7 @@ class ModflowGwtsft(MFPackage): [ "block period", "name evaporation", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -578,7 +589,7 @@ class ModflowGwtsft(MFPackage): [ "block period", "name runoff", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -588,7 +599,7 @@ class ModflowGwtsft(MFPackage): [ "block period", "name inflow", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -611,6 +622,7 @@ class ModflowGwtsft(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfgwtuzt.py b/flopy/mf6/modflow/mfgwtuzt.py index f4be064b9e..dd7e0b15ec 100644 --- a/flopy/mf6/modflow/mfgwtuzt.py +++ b/flopy/mf6/modflow/mfgwtuzt.py @@ -125,7 +125,7 @@ class ModflowGwtuzt(MFPackage): infiltration, uzet, and auxiliary. these settings are used to assign the concentration of associated with the corresponding flow terms. concentrations cannot be specified for all flow terms. - * concentration : string + * concentration : double precision real or character value that defines the concentration for the unsaturated zone flow cell. the specified concentration is only applied if the unsaturated zone flow cell is a constant concentration cell. if the options block includes a @@ -139,13 +139,13 @@ class ModflowGwtuzt(MFPackage): solute mass fluxes into or out of the uzf cell and the inactive value will be written for the uzf cell concentration. if a uzf cell is constant, then the concentration for the uzf cell will be fixed at the user specified value. - * infiltration : string + * infiltration : double precision real or character value that defines the infiltration solute concentration :math:`(ml^{-3})` for the uzf cell. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * uzet : string + * uzet : double precision real or character value that defines the concentration of unsaturated zone evapotranspiration water :math:`(ml^{-3})` for the uzf cell. if this concentration value is larger than the simulated concentration in the uzf cell, @@ -205,6 +205,7 @@ class ModflowGwtuzt(MFPackage): "shape", "reader urword", "optional true", + "mf6internal flow_pkg_name", ], [ "block options", @@ -221,6 +222,7 @@ class ModflowGwtuzt(MFPackage): "shape", "reader urword", "optional true", + "mf6internal fp_aux_name", ], [ "block options", @@ -236,6 +238,7 @@ class ModflowGwtuzt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprpak", ], [ "block options", @@ -243,6 +246,7 @@ class ModflowGwtuzt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprconc", ], [ "block options", @@ -250,6 +254,7 @@ class ModflowGwtuzt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal iprflow", ], [ "block options", @@ -257,6 +262,7 @@ class ModflowGwtuzt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal ipakcb", ], [ "block options", @@ -266,6 +272,7 @@ class ModflowGwtuzt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal conc_filerec", ], [ "block options", @@ -296,6 +303,7 @@ class ModflowGwtuzt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budfilerec", ], [ "block options", @@ -336,6 +344,7 @@ class ModflowGwtuzt(MFPackage): "reader urword", "tagged true", "optional true", + "mf6internal budcsvfilerec", ], [ "block options", @@ -438,12 +447,13 @@ class ModflowGwtuzt(MFPackage): "type keyword", "reader urword", "optional true", + "mf6internal dev_nonexpanding", ], [ "block packagedata", "name packagedata", "type recarray ifno strt aux boundname", - "shape (maxbound)", + "shape (nuzfcells)", "reader urword", ], [ @@ -455,6 +465,7 @@ class ModflowGwtuzt(MFPackage): "in_record true", "reader urword", "numeric_index true", + "mf6internal packagedata_ifno", ], [ "block packagedata", @@ -536,7 +547,7 @@ class ModflowGwtuzt(MFPackage): [ "block period", "name concentration", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -546,7 +557,7 @@ class ModflowGwtuzt(MFPackage): [ "block period", "name infiltration", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -556,7 +567,7 @@ class ModflowGwtuzt(MFPackage): [ "block period", "name uzet", - "type string", + "type double precision", "shape", "tagged true", "in_record true", @@ -579,6 +590,7 @@ class ModflowGwtuzt(MFPackage): "shape", "in_record true", "reader urword", + "mf6internal period_auxiliary", ], [ "block period", diff --git a/flopy/mf6/modflow/mfmvr.py b/flopy/mf6/modflow/mfmvr.py index adb12a30c9..0e68ed857a 100644 --- a/flopy/mf6/modflow/mfmvr.py +++ b/flopy/mf6/modflow/mfmvr.py @@ -52,7 +52,7 @@ class ModflowMvr(MFPackage): provided by the user, then packages are assigned a default value, which is the package acronym followed by a hyphen and the package number. For example, the first Drain Package is named DRN-1. The second Drain Package is named DRN-2, - and so forth. + and so forth. Each package may be listed only once in the PACKAGES block. perioddata : [(mname1, pname1, id1, mname2, pname2, id2, mvrtype, value)] * mname1 : string diff --git a/flopy/mf6/modflow/mfnam.py b/flopy/mf6/modflow/mfnam.py index debd1e89ff..77e839de24 100644 --- a/flopy/mf6/modflow/mfnam.py +++ b/flopy/mf6/modflow/mfnam.py @@ -201,7 +201,6 @@ class ModflowNam(MFPackage): "preserve_case true", "type string", "reader urword", - "optional", ], [ "block models", diff --git a/flopy/mf6/modflow/mfprtprp.py b/flopy/mf6/modflow/mfprtprp.py index cd8bd64553..cb43fa1f17 100644 --- a/flopy/mf6/modflow/mfprtprp.py +++ b/flopy/mf6/modflow/mfprtprp.py @@ -181,10 +181,14 @@ class ModflowPrtprp(MFPackage): releasetimes block or configured via release_time_frequency, only one particle is released at that time. coincidence is evaluated up to the tolerance specified in release_time_tolerance, or :math:`epsilon times 10^{11}` by - default, where :math:`epsilon` is machine precision. if no release times are - configured via this setting, the releasetimes block, or the - release_time_frequency option, a single release time is configured at the - beginning of the first time step of the simulation's first stress period. + default, where :math:`epsilon` is machine precision. when such a coincidence + occurs, the release is reported under the earlier of the two time steps, i.e. + whichever time step the coincident release time falls in per the usual + convention that a time exactly on a time step boundary belongs to the earlier + time step, not the later one. if no release times are configured via this + setting, the releasetimes block, or the release_time_frequency option, a single + release time is configured at the beginning of the first time step of the + simulation's first stress period. * all : keyword keyword to indicate release at the start of all time steps in the period. * first : keyword diff --git a/flopy/mf6/modflow/mfutlncf.py b/flopy/mf6/modflow/mfutlncf.py index b3689207f6..da2f7fa665 100644 --- a/flopy/mf6/modflow/mfutlncf.py +++ b/flopy/mf6/modflow/mfutlncf.py @@ -21,7 +21,12 @@ class ModflowUtlncf(MFPackage): processing purposes only. wkt : [string] is the coordinate reference system (crs) well-known text string in wkt1 format - (ogc 01-009). written as the 'wkt' attribute on the projection variable. + (ogc 01-009). written as the 'wkt' attribute on the projection variable. for + supported projected crs types, individual cf-standard numeric grid_mapping + parameters are also written to the projection variable, for tools that read + these directly rather than parsing 'wkt'/'crs_wkt' text. for a + netcdf_structured export with a rotated grid (angrot not 0.0), wkt1 cannot + represent the rotation; provide 'crs_wkt' instead for correct grid positioning. ignored when latitude and longitude griddata arrays have been provided. to also embed the crs in the binary grid file, provide the same string as the 'crs' option in the discretization input; the discretization 'crs' option @@ -29,13 +34,23 @@ class ModflowUtlncf(MFPackage): (e.g. 'epsg:26918'). crs_wkt : [string] is the coordinate reference system (crs) well-known text string in wkt2 format - (iso 19162:2019), as required by cf-1.11. written as the 'crs_wkt' attribute on + (iso 19162:2019), as required by cf-1.13. written as the 'crs_wkt' attribute on the projection variable. when omitted, the 'wkt' value is written to 'crs_wkt' as a compatibility alias. ignored when latitude and longitude griddata arrays have been provided. to also embed the crs in the binary grid file, provide the same string as the 'crs' option in the discretization input; the discretization 'crs' option additionally accepts other pyproj-compatible identifiers such as - an epsg code (e.g. 'epsg:26918'). + an epsg code (e.g. 'epsg:26918'). for a netcdf_structured export with a rotated + grid (angrot not 0.0), a valid wkt2 projcrs string is required here to + correctly position the grid: it is wrapped in a derived crs that encodes the + rotation, resolved by any crs-aware consumer through ordinary reprojection; + without it the 'x'/'y' coordinate values remain grid-local rather than real- + world. for grid mappings transverse_mercator, lambert_conformal_conic (two- + standard-parallel variant only), and albers_conical_equal_area, individual cf- + standard numeric grid_mapping parameters (e.g. 'false_easting', + 'scale_factor_at_central_meridian') are also written to the projection + variable, for tools -- such as arcgis's classic netcdf connector -- that read + these directly rather than parsing 'wkt'/'crs_wkt' text. deflate : integer is the variable deflate level (0=min, 9=max) in the netcdf file. defining this parameter activates per-variable compression at the level specified. use with @@ -52,9 +67,8 @@ class ModflowUtlncf(MFPackage): parameter (netcdf_mesh2d) or the chunk_z, chunk_y, and chunk_x parameter set (netcdf_structured) to have an effect. chunk_face : integer - is the keyword used to provide a data chunk size for the face dimension in a - netcdf_mesh2d output file. must be used in combination with the the chunk_time - parameter to have an effect. + is the chunk size for the face dimension in a netcdf_mesh2d output file. must + be used in combination with the chunk_time parameter to have an effect. chunk_z : integer is the chunk size for the layer (vertical) dimension in a netcdf_structured output file. must be used in combination with the chunk_time, chunk_x, and @@ -74,11 +88,14 @@ class ModflowUtlncf(MFPackage): ncpl : integer is the number of cells in a projected plane layer. latitude : [double precision] - cell center latitude array. only supported for netcdf_structured export type. - providing latitude and longitude arrays enables geographic-coordinate output as - a special case for external tooling that requires it; the preferred approach is - to use the 'wkt' and 'crs_wkt' options instead. when latitude and longitude - arrays are provided, any 'wkt' or 'crs_wkt' option values are ignored. + cell center latitude array. only supported for netcdf_structured export type; + ignored, with a warning, for netcdf_mesh2d. providing latitude and longitude + arrays enables geographic-coordinate output for external tooling that requires + it, and is the more reliable option for a grid with non-uniform row or column + spacing, or a rotated grid, since some tools cannot otherwise correctly + position such a grid from 'wkt'/'crs_wkt' alone. for other grids, + 'wkt'/'crs_wkt' is the simpler choice. when latitude and longitude arrays are + provided, any 'wkt' or 'crs_wkt' option values are ignored. longitude : [double precision] cell center longitude array. only supported for netcdf_structured export type. see the latitude description for usage notes. diff --git a/flopy/mf6/modflow/mfutlspc.py b/flopy/mf6/modflow/mfutlspc.py index 1299b52e9a..cf7dfa9393 100644 --- a/flopy/mf6/modflow/mfutlspc.py +++ b/flopy/mf6/modflow/mfutlspc.py @@ -39,12 +39,12 @@ class ModflowUtlspc(MFPackage): line of information that is parsed into a keyword and values. keyword values that can be used to start the spcsetting string include: concentration and temperature. - * concentration : string + * concentration : double precision is the boundary concentration. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. by default, the concentration for each boundary feature is zero. - * temperature : string + * temperature : double precision is the user-supplied boundary temperature. if the options block includes a timeseriesfile entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a @@ -165,24 +165,22 @@ class ModflowUtlspc(MFPackage): [ "block period", "name concentration", - "type string", + "type double precision", "shape", "tagged true", "in_record true", "reader urword", "time_series true", - "mf6internal concentration_in", ], [ "block period", "name temperature", - "type string", + "type double precision", "shape", "tagged true", "in_record true", "reader urword", "time_series true", - "mf6internal temperature_in", ], ] diff --git a/flopy/mf6/modflow/mfutltas.py b/flopy/mf6/modflow/mfutltas.py index 1cd78d7559..22e5f0b761 100644 --- a/flopy/mf6/modflow/mfutltas.py +++ b/flopy/mf6/modflow/mfutltas.py @@ -118,7 +118,7 @@ class ModflowUtltas(MFPackage): "block attributes", "name interpolation_method", "type string", - "valid stepwise linear linearend", + "valid stepwise linear", "shape", "tagged false", "reader urword", diff --git a/flopy/mf6/modflow/mfutlts.py b/flopy/mf6/modflow/mfutlts.py index 6cc63f5ff5..8323df251b 100644 --- a/flopy/mf6/modflow/mfutlts.py +++ b/flopy/mf6/modflow/mfutlts.py @@ -22,7 +22,8 @@ class ModflowUtlts(MFPackage): time_series_namerecord : (names, time_series_names) xxx * names : keyword - xxx + keyword to specify the names of the time series. The 'NAME' keyword can be used + as a substitute for 'NAMES'. * time_series_names : [string] Name by which a package references a particular time-array series. The name must be unique among all time-array series used in a package. @@ -34,12 +35,13 @@ class ModflowUtlts(MFPackage): * interpolation_method : [string] Interpolation method, which is either STEPWISE or LINEAR. - interpolation_methodrecord_single : record + interpolation_methodrecord_single : (method, interpolation_method_single) xxx - method : keyword - xxx - interpolation_method_single : string - interpolation method, which is either stepwise or linear. + * method : keyword + xxx + * interpolation_method_single : string + Interpolation method, which is either STEPWISE or LINEAR. + sfacrecord : (sfacs, sfacval) xxx * sfacs : keyword @@ -48,14 +50,14 @@ class ModflowUtlts(MFPackage): Scale factor, which will multiply all array values in time series. SFAC is an optional attribute; if omitted, SFAC = 1.0. - sfacrecord_single : (sfacval) + sfacrecord_single : (sfac, sfacval) xxx + * sfac : keyword + xxx * sfacval : [double precision] Scale factor, which will multiply all array values in time series. SFAC is an optional attribute; if omitted, SFAC = 1.0. - sfac : keyword - xxx timeseries : [(ts_time, ts_array)] xxx * ts_time : double precision @@ -166,6 +168,7 @@ class ModflowUtlts(MFPackage): "shape", "reader urword", "optional false", + "in_record true", ], [ "block attributes", @@ -176,6 +179,7 @@ class ModflowUtlts(MFPackage): "tagged false", "reader urword", "optional false", + "in_record true", ], [ "block attributes", @@ -219,9 +223,9 @@ class ModflowUtlts(MFPackage): "name sfac", "type keyword", "shape", - "tagged false", "reader urword", "optional false", + "in_record true", ], [ "block timeseries", diff --git a/flopy/mf6/modflow/mfutltvk.py b/flopy/mf6/modflow/mfutltvk.py index 02df213e87..ba7ed0a138 100644 --- a/flopy/mf6/modflow/mfutltvk.py +++ b/flopy/mf6/modflow/mfutltvk.py @@ -38,13 +38,13 @@ class ModflowUtltvk(MFPackage): line of information that is parsed into a property name keyword and values. property name keywords that can be used to start the tvksetting string include: k, k22, and k33. - * k : string + * k : double precision is the new value to be assigned as the cell's hydraulic conductivity from the start of the specified stress period, as per k in the npf package. if the options block includes a ts6 entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * k22 : string + * k22 : double precision is the new value to be assigned as the cell's hydraulic conductivity of the second ellipsoid axis (or the ratio of k22/k if the k22overk npf package option is specified) from the start of the specified stress period, as per k22 in the @@ -52,7 +52,7 @@ class ModflowUtltvk(MFPackage): direction. if the options block includes a ts6 entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * k33 : string + * k33 : double precision is the new value to be assigned as the cell's hydraulic conductivity of the third ellipsoid axis (or the ratio of k33/k if the k33overk npf package option is specified) from the start of the specified stress period, as per k33 in the @@ -168,35 +168,32 @@ class ModflowUtltvk(MFPackage): [ "block period", "name k", - "type string", + "type double precision", "shape", "tagged true", "in_record true", "reader urword", "time_series true", - "mf6internal k_in", ], [ "block period", "name k22", - "type string", + "type double precision", "shape", "tagged true", "in_record true", "reader urword", "time_series true", - "mf6internal k22_in", ], [ "block period", "name k33", - "type string", + "type double precision", "shape", "tagged true", "in_record true", "reader urword", "time_series true", - "mf6internal k33_in", ], ] diff --git a/flopy/mf6/modflow/mfutltvs.py b/flopy/mf6/modflow/mfutltvs.py index 2a167c4249..c852412de3 100644 --- a/flopy/mf6/modflow/mfutltvs.py +++ b/flopy/mf6/modflow/mfutltvs.py @@ -44,7 +44,7 @@ class ModflowUtltvs(MFPackage): line of information that is parsed into a property name keyword and values. property name keywords that can be used to start the tvssetting string include: ss and sy. - * ss : string + * ss : double precision is the new value to be assigned as the cell's specific storage (or storage coefficient if the storagecoefficient sto package option is specified) from the start of the specified stress period, as per ss in the sto package. specific @@ -52,7 +52,7 @@ class ModflowUtltvs(MFPackage): includes a ts6 entry (see the 'time-variable input' section), values can be obtained from a time series by entering the time-series name in place of a numeric value. - * sy : string + * sy : double precision is the new value to be assigned as the cell's specific yield from the start of the specified stress period, as per sy in the sto package. specific yield values must be greater than or equal to 0. if the options block includes a ts6 @@ -174,24 +174,22 @@ class ModflowUtltvs(MFPackage): [ "block period", "name ss", - "type string", + "type double precision", "shape", "tagged true", "in_record true", "reader urword", "time_series true", - "mf6internal ss_in", ], [ "block period", "name sy", - "type string", + "type double precision", "shape", "tagged true", "in_record true", "reader urword", "time_series true", - "mf6internal sy_in", ], ] diff --git a/flopy/mf6/utils/model_splitter.py b/flopy/mf6/utils/model_splitter.py index 8b333cb5ed..19df3656eb 100644 --- a/flopy/mf6/utils/model_splitter.py +++ b/flopy/mf6/utils/model_splitter.py @@ -123,6 +123,12 @@ } +# cost of cutting a graph edge for special conditions, +# relative to the cost of one for every other graph edge +HFB_EDGE_WEIGHT = 1000 +LAK_EDGE_WEIGHT = 1000 + + class Mf6Splitter: """ A class for splitting a single model into a multi-model simulation @@ -132,9 +138,12 @@ class Mf6Splitter: sim : flopy.mf6.MFSimulation modelname : str, None name of model to split + no_remap_key : int + integer value to tell model splitter not to remap cells. Useful for ignoring + inactive cells and applied when the `active_only=True` in `optimize_splitting_mask()` """ - def __init__(self, sim, modelname=None): + def __init__(self, sim, modelname=None, no_remap_key=-9): self._sim = sim self._model = self._sim.get_model(modelname) if modelname is None: @@ -153,8 +162,9 @@ def __init__(self, sim, modelname=None): self._ncpl = self._modelgrid.nnodes self._shape = self._modelgrid.shape self._grid_type = self._modelgrid.grid_type - self._node_map = {} - self._node_map_r = {} + self._no_remap_key = int(no_remap_key) + self._node_map_arr = None + self._model_map_arr = None self._new_connections = None self._new_ncpl = None self._grid_info = None @@ -223,8 +233,8 @@ def switch_models(self, modelname, remap_nodes=False): if remap_nodes: self._modelgrid = self._model.modelgrid - self._node_map = {} - self._node_map_r = {} + self._node_map_arr = None + self._model_map_arr = None self._new_connections = None self._new_ncpl = None self._grid_info = None @@ -242,18 +252,6 @@ def switch_models(self, modelname, remap_nodes=False): self._sim_mover_data = {} self._offsets = {} - @property - def reversed_node_map(self): - """ - Returns a lookup table of {model number : {model node: original node}} - - """ - if not self._node_map_r: - self._node_map_r = {mkey: {} for mkey in self._model_dict.keys()} - for onode, (mkey, nnode) in self._node_map.items(): - self._node_map_r[mkey][nnode] = onode - return self._node_map_r - @property def original_modelgrid(self): """ @@ -398,10 +396,6 @@ def save_node_mapping(self, filename): ------- None """ - node_map = { - int(k): (int(v[0]), int(v[1])) for k, v in self._node_map.items() - } - h5py = import_optional_dependency("h5py") # import h5py f = h5py.File(filename, "w") @@ -432,11 +426,9 @@ def save_node_mapping(self, filename): # finally, think about the node_map arrangement.... onode: (model, nnode) # maybe put it in a group "node_map/original_node", etc... nm_grp = f.create_group("node_map") - onode, nmodel, nnode = [], [], [] - for k, (m, n) in node_map.items(): - onode.append(k) - nmodel.append(m) - nnode.append(n) + onode = list(range(self._node_map_arr.size)) + nmodel = [int(i) for i in self._model_map_arr] + nnode = [int(i) for i in self._node_map_arr] onode_ds = nm_grp.create_dataset( "original_node", (len(onode),), dtype=int, compression="gzip" @@ -551,12 +543,8 @@ def construct_modelgrid(f, name, grid_type): ) # construct the node map - node_map = {} - onode = f["node_map/original_node"][:] - mnum = f["node_map/new_model"][:] - nnode = f["node_map/new_node"][:] - for ix, node in enumerate(onode): - node_map[node] = (mnum[ix], nnode[ix]) + model_map = f["node_map/new_model"][:] + node_map = f["node_map/new_node"][:] # construct representation of original model geometry modelname = f["modelname"][0].decode("utf8") @@ -583,25 +571,18 @@ def construct_modelgrid(f, name, grid_type): f.close() - # create additional splitting data efficient reconstruction - split_array = np.zeros((mfs._ncpl,), dtype=int) - model_array = np.zeros((mfs._ncpl,), dtype=int) - for k, v in node_map.items(): - k = int(k) - model_array[k] = v[0] - split_array[k] = v[1] - grid_info = {} for mkey in mkeys: ncpl = mfs._new_ncpl[mkey] array = np.full((ncpl,), -1, dtype=int) - onode = np.asarray(model_array == mkey).nonzero()[0] - nnode = split_array[onode] + onode = np.asarray(model_map == mkey).nonzero()[0] + nnode = node_map[onode] array[nnode] = onode grid_info[mkey] = (array,) mfs._grid_info = grid_info - mfs._node_map = node_map + mfs._node_map_arr = node_map + mfs._model_map_arr = model_map mfs._allow_splitting = False return mfs @@ -617,20 +598,20 @@ def optimize_splitting_mask(self, nparts, active_only=False, options=None, verbo number of parts to split the model in to active_only : bool only consider active cells when building adjacency graph. Default is False + no_remap_key : int + for use with active_only=True. Cells that are inactive are tagged with an int value that can be + passed to split model which reduces the number of cells remapped. Default is -9. options : None or pymetis.Options optional pymetis.Options class that gets passed through to the pymetis.part_graph() function. Example `options=pymetis.Options(seed=42, contig=1)` + verbose : bool Default False. Prints progress statements if True Returns ------- np.ndarray """ - if active_only: - import_optional_dependency("sklearn") - from sklearn.neighbors import NearestNeighbors - pymetis = import_optional_dependency( "pymetis", "please install pymetis using: " @@ -707,20 +688,88 @@ def optimize_splitting_mask(self, nparts, active_only=False, options=None, verbo # for k in inactive: [neighbors.pop(k) for k in inactive] node_map = {i: ix for ix, i in enumerate(np.where(iact > 0)[0])} + # todo: this might need be better as node_map[k]: [existing logic] for k, v in neighbors.items() neighbors = { - k : [node_map[i] for i in v if i not in inactive] for k, v in neighbors.items() + node_map[k] : [node_map[i] for i in v if i not in inactive] for k, v in neighbors.items() } + neighbors = dict(sorted(neighbors.items())) + else: + neighbors = dict(sorted(neighbors.items())) + node_map = {i: ix for ix, i in enumerate(neighbors.keys())} if verbose: print("Creating graph and weights") - neighbors = dict(sorted(neighbors.items())) - weights = [np.count_nonzero(idomain[:, nn]) + adv_pkg_weights[nn] for nn in neighbors.keys()] + weights = [np.count_nonzero(idomain[:, nn]) + adv_pkg_weights[nn] for nn in node_map.keys()] graph = [np.array(neigh, dtype=int) for neigh in neighbors.values()] + hfb_faces = [] + if hfbs: + if verbose: + print("Weighting horizontal flow barrier faces") + # metis cannot be told that a barrier must not be cut, but making + # the faces it crosses expensive to cut steers the partition around + # it. Barriers that are still cut are moved after the partition is + # built. + for hfb in hfbs: + for recarray in hfb.stress_period_data.data.values(): + _, nodes1 = self._cellid_to_layer_node(recarray.cellid1) + _, nodes2 = self._cellid_to_layer_node(recarray.cellid2) + nodes1 = [node_map[i] if i in node_map else -1 for i in nodes1] + nodes2 = [node_map[i] if i in node_map else -1 for i in nodes2] + hfb_nodes = np.array([nodes1, nodes2]) + + # filter out barriers that touch inactive cells + vidx = np.where(np.min(hfb_nodes, axis=0) > -1)[0] + hfb_nodes = hfb_nodes[:, vidx] + + # filter out vertical flow barriers + not_vfb = np.where((hfb_nodes[0] - hfb_nodes[1]) != 0)[0] + hfb_nodes = hfb_nodes[:, not_vfb] + + hfb_faces.extend(zip(hfb_nodes[0], hfb_nodes[1])) + hfb_faces.extend(zip(hfb_nodes[1], hfb_nodes[0])) + + hfb_faces = set(hfb_faces) + + lak_faces = [] + if laks: + if verbose: + print("Weighting lak faces") + for lakeno in laks: + lak_nodes = np.where(lak_array == lakeno)[0] + lak_nodes = [node_map[i] for i in lak_nodes] + nodes1, nodes2 = [], [] + for lak_node in lak_nodes: + neighs = neighbors[lak_node] + for nn in neighs: + if nn in lak_nodes: + nodes1.append(lak_node) + nodes2.append(nn) + + lak_faces.extend(zip(nodes1, nodes2)) + + lak_faces = set(lak_faces) + + eweights = None + if hfbs or laks: + eweights = [] + for node, conns in enumerate(graph): + for conn in conns: + if (node, int(conn)) in hfb_faces: + eweights.append(HFB_EDGE_WEIGHT) + elif (node, int(conn)) in lak_faces: + eweights.append(LAK_EDGE_WEIGHT) + else: + eweights.append(1) + if verbose: print("Running Metis") n_cuts, membership = pymetis.part_graph( - nparts, adjacency=graph, vweights=weights, options=options + nparts, + adjacency=graph, + vweights=weights, + eweights=eweights, + options=options, ) membership = np.array(membership, dtype=int) @@ -729,28 +778,25 @@ def optimize_splitting_mask(self, nparts, active_only=False, options=None, verbo if verbose: print("Remapping inactive to model domains") if len(inactive) > 0: - xc = self._modelgrid.xcellcenters.ravel() - yc = self._modelgrid.ycellcenters.ravel() - axc = xc[list(node_map.keys())] - ayc = yc[list(node_map.keys())] - fit_points = np.array(list(zip(axc, ayc))) - ixc = xc[list(inactive.keys())] - iyc = yc[list(inactive.keys())] - pred_points = np.array(list(zip(ixc, iyc))) - - nn = NearestNeighbors(n_neighbors=1, algorithm="auto") - nn.fit(fit_points) - ind = nn.kneighbors(pred_points, return_distance=False).ravel() - data = membership[ind] - member_array = np.full((ncpl,), -1) + member_array = np.full((ncpl,), self._no_remap_key) member_array[list(node_map.keys())] = membership[list(node_map.values())] - member_array[list(inactive.keys())] = data membership = member_array if laks: for lak in laks: idx = np.asarray(lak_array == lak).nonzero()[0] - mnum = np.unique(membership[idx])[0] + mnums = np.unique(membership[idx]) #[0] + mnum = mnums[0] + + if len(mnums) > 1: + # if the lake is in multiple parts of the membership array, + # reset it so it's in the part of the array that has the + # greatest coverage of the lake. + lak_membership = membership[idx] + counts = [np.count_nonzero(lak_membership == i) for i in mnums] + ix = np.argmax(counts) + mnum = mnums[ix] + membership[idx] = mnum if hfbs: @@ -761,6 +807,7 @@ def optimize_splitting_mask(self, nparts, active_only=False, options=None, verbo _, nodes1 = self._cellid_to_layer_node(cellids1) _, nodes2 = self._cellid_to_layer_node(cellids2) cnt = 0 + moved = 0 while cnt < len(nodes1): mnums1 = membership[nodes1] mnums2 = membership[nodes2] @@ -771,14 +818,21 @@ def optimize_splitting_mask(self, nparts, active_only=False, options=None, verbo mnum_to = mnums1[idx] adj_nodes = np.array(nodes2)[idx] membership[adj_nodes] = mnum_to + moved += len(adj_nodes) cnt += 1 if cnt == len(nodes1): raise AssertionError( - "Cannot uniquely spilt around HFB boundaries, try another " + "Cannot uniquely split around HFB boundaries, try another " "value for nparts" ) + if verbose and moved: + print( + f"Moved {moved} cell(s) to keep horizontal flow " + f"barriers within a model" + ) + return membership.reshape(shape) def reconstruct_array(self, arrays): @@ -864,7 +918,11 @@ def reconstruct_recarray(self, recarrays): new_recarray = np.recarray((rlen,), dtype=dtype) idx = 0 for mkey, recarray in recarrays.items(): - remapper = self.reversed_node_map[mkey] + onodes = np.where(self._model_map_arr == mkey) + nnodes = self._node_map_arr[onodes] + remapper = np.zeros((self._new_ncpl[mkey],), dtype=int) + remapper[nnodes] = onodes + orec = recarray.copy() modelgrid = self._model_dict[mkey].modelgrid if self._grid_type in ("structured", "vertex"): @@ -877,7 +935,7 @@ def reconstruct_recarray(self, recarrays): else: node = [i[0] for i in orec.cellid] - new_node = [remapper[i] for i in node if i in remapper] + new_node = [remapper[i] for i in node] if modelgrid.grid_type == "structured": if self._modelgrid is None: @@ -989,7 +1047,7 @@ def _remap_nodes(self, array): array = np.ravel(array) idomain = self._modelgrid.idomain.reshape((-1, self._ncpl)) - mkeys = [int(i) for i in np.unique(array)] + mkeys = [int(i) for i in np.unique(array) if i != self._no_remap_key] bad_keys = [] for mkey in mkeys: count = 0 @@ -1008,14 +1066,20 @@ def _remap_nodes(self, array): if self._modelgrid.grid_type == "unstructured": self._map_iac_ja_connections() else: - self._connection = self._modelgrid.neighbors( + neighbors = self._modelgrid.neighbors( reset=True, method="rook", fast=self._fast_neighbors ) + iac, ja = [], [] + for k in list(neighbors.keys()): + rec = [k,] + neighbors.pop(k) + ja.extend(rec) + iac.append(len(rec)) + self._connection = (np.array(iac, dtype=int), np.array(ja, dtype=int)) grid_info = {} if self._modelgrid.grid_type == "structured": a = array.reshape(self._modelgrid.nrow, self._modelgrid.ncol) - for m in np.unique(a): + for m in mkeys: cells = np.asarray(a == m).nonzero() rmin, rmax = np.min(cells[0]), np.max(cells[0]) cmin, cmax = np.min(cells[1]), np.max(cells[1]) @@ -1028,7 +1092,7 @@ def _remap_nodes(self, array): # get new nrow and ncol information nrow = (rmax - rmin) + 1 ncol = (cmax - cmin) + 1 - mapping = np.ones((nrow, ncol), dtype=int) * -1 + mapping = np.full((nrow, ncol), -1, dtype=int) nodes = self._modelgrid.get_node(cellids) mapping[cells[0] - rmin, cells[1] - cmin] = nodes grid_info[m] = [ @@ -1056,6 +1120,8 @@ def _remap_nodes(self, array): for i in grid_info[m][0]: new_ncpl[m] *= i + node_map = np.full((self._ncpl,), self._no_remap_key, dtype=int) + model_map = np.full((self._ncpl,), self._no_remap_key, dtype=int) for mdl in mkeys: mnodes = np.asarray(array == mdl).nonzero()[0] mg_info = grid_info[mdl] @@ -1064,24 +1130,43 @@ def _remap_nodes(self, array): new_nodes = np.asarray(mapping != -1).nonzero()[0] old_nodes = mapping[new_nodes] for ix, nnode in enumerate(new_nodes): - self._node_map[old_nodes[ix]] = (mdl, nnode) + node_map[old_nodes[ix]] = nnode + model_map[old_nodes[ix]] = mdl else: for nnode, onode in enumerate(mnodes): - self._node_map[onode] = (mdl, nnode) + node_map[onode] = nnode + model_map[onode] = mdl + self._node_map_arr = node_map + self._model_map_arr = model_map new_connections = { i: {"internal": {}, "external": {}} for i in mkeys } exchange_meta = {i: {} for i in mkeys} usg_meta = {i: {} for i in mkeys} - for node, conn in self._connection.items(): - mdl, nnode = self._node_map[node] + # todo: rework the conn stuff + + iac, ja = self._connection + idx0 = 0 + for ia in iac: + idx1 = idx0 + ia + node = ja[idx0] + conn = ja[idx0 + 1 : idx1] + mdl = self._model_map_arr[node] + nnode = self._node_map_arr[node] + # advance the ja indexing + idx0 = idx1 + if mdl == self._no_remap_key: + continue for ix, cnode in enumerate(conn): - cmdl, cnnode = self._node_map[cnode] + cmdl = self._model_map_arr[cnode] + cnnode = self._node_map_arr[cnode] + if cmdl == self._no_remap_key: + continue if cmdl == mdl: if nnode in new_connections[mdl]["internal"]: - new_connections[mdl]["internal"][nnode].append(cnnode) if self._uconnection is not None: + new_connections[mdl]["internal"][nnode].append(cnnode) usg_meta[mdl][nnode]["ihc"].append( int(self._uconnection[node]["ihc"][ix + 1]) ) @@ -1097,8 +1182,8 @@ def _remap_nodes(self, array): ) else: - new_connections[mdl]["internal"][nnode] = [cnnode] if self._uconnection is not None: + new_connections[mdl]["internal"][nnode] = [cnnode] usg_meta[mdl][nnode] = { "ihc": [ self._uconnection[node]["ihc"][0], @@ -1178,7 +1263,6 @@ def _map_iac_ja_connections(self): vertex information has been supplied """ - conn = {} uconn = {} iac = self._modelgrid.iac ja = self._modelgrid.ja @@ -1191,8 +1275,6 @@ def _map_iac_ja_connections(self): idx0 = 0 for ia in iac: idx1 = idx0 + ia - cn = ja[idx0 + 1 : idx1] - conn[ja[idx0]] = list(cn) uconn[ja[idx0]] = { "cl12": list(cl12[idx0:idx1]), "ihc": list(ihc[idx0:idx1]), @@ -1204,7 +1286,7 @@ def _map_iac_ja_connections(self): idx0 = idx1 - self._connection = conn + self._connection = (iac, ja) self._uconnection = uconn def _map_verts_iverts(self, array): @@ -1231,8 +1313,10 @@ def _map_verts_iverts(self, array): tmp_vert_dict = {} for node, ivert in enumerate(iverts): tiverts = [] - mk, nnode = self._node_map[node] - if mk == mkey: + mk = self._model_map_arr[node] + if mk == self._no_remap_key: + continue + elif mk == mkey: for iv in ivert: vert = tuple(verts[iv].tolist()) if vert in tmp_vert_dict: @@ -1311,7 +1395,7 @@ def _remap_cell2d(self, item, cell2d, mapped_data): for mkey in self._model_dict.keys(): idx = [] - for node, (nmkey, nnode) in self._node_map.items(): + for node, nmkey in enumerate(self._model_map_arr): if nmkey == mkey: idx.append(node) @@ -1833,7 +1917,6 @@ def _remap_mvr(self, package, mapped_data): ------- dict """ - # self._mvr_remaps = {} if isinstance(package, (modflow.ModflowGwtmvt, modflow.ModflowGwemve)): return mapped_data @@ -2069,10 +2152,25 @@ def _remap_sfr(self, package, mapped_data): messy_rch = packagedata.ifno[messy_idx] # store these for later rcids = [] for rch in messy_rch: - con = np.abs(connectiondata[connectiondata.ifno == rch].ic_0)[0] - cid = packagedata[packagedata.ifno == con].cellid[0] - # use absolute value in case this is connected to cell(s) that are - # also not connected to the model + rch_cons = tuple(connectiondata[connectiondata.ifno == rch][0])[1:] + stack = [np.abs(i) for i in rch_cons if not np.isnan(i)] + visited = [rch,] + while stack: + con = stack.pop(0) + cid = packagedata[packagedata.ifno == con].cellid[0] + if cid in (messy_val, "None", None): + rch_cons = tuple(connectiondata[connectiondata.ifno == con][0])[1:] + for nc in rch_cons: + nc = np.abs(nc) + if np.isnan(nc) or nc in visited: + continue + stack.append(nc) + visited.append(con) + else: + break + + # use absolute value in case there are no connected cells + # that exchange with the GWF system rcids.append(tuple(np.abs(cid))) cellids[messy_idx] = rcids @@ -2535,10 +2633,13 @@ def _set_boundname_remaps(self, recarray, obs_map, variables, mkey): ------- dict : obs_map """ + # todo: may need to patch changes for boundnames in obs_map["cellid"] if "boundname" in recarray.dtype.names: for bname in recarray.boundname: for variable in variables: - if bname in obs_map[variable]: + if obs_map[variable] is None: + continue + elif bname in obs_map[variable]: if not isinstance(obs_map[variable][bname], list): obs_map[variable][bname] = [ obs_map[variable][bname] @@ -2724,13 +2825,8 @@ def _remap_obs(self, package, mapped_data, remapper, pkg_type=None): for ofile, recarray in continuous_data.items(): if pkg_type is None: layers1, node1 = self._cellid_to_layer_node(recarray.id) - new_node1 = np.array( - [remapper[i][-1] for i in node1], dtype=int - ) - new_model1 = np.array( - [remapper[i][0] for i in node1], dtype=int - ) - + new_node1 = self._node_map_arr[node1] + new_model1 = self._model_map_arr[node1] new_cellid1 = np.full( ( len( @@ -2806,6 +2902,7 @@ def _remap_obs(self, package, mapped_data, remapper, pkg_type=None): ) else: + # use the cellid remapper new_node1 = np.full( (len(recarray),), None, dtype=object ) @@ -2827,8 +2924,9 @@ def _remap_obs(self, package, mapped_data, remapper, pkg_type=None): layers1, node1 = self._cellid_to_layer_node( recarray.id[idx] ) - new_node1[idx] = [remapper[i][-1] for i in node1] - new_model1[idx] = [remapper[i][0] for i in node1] + + new_node1[idx] = self._node_map_arr[node1] + new_model1[idx] = self._model_map_arr[node1] new_node1[bidx] = [i for i in recarray.id[bidx]] new_model1[bidx] = [ @@ -2889,10 +2987,10 @@ def _remap_obs(self, package, mapped_data, remapper, pkg_type=None): "structured", "vertex", ): - layers2 = [ + layers2 = np.array([ cid[0] if cid is not None else None for cid in cellid2 - ] + ], dtype=object) if self._modelgrid.grid_type == "structured": cellid2 = [ ( @@ -2909,7 +3007,7 @@ def _remap_obs(self, package, mapped_data, remapper, pkg_type=None): ] node2 = self._modelgrid.get_node( - list(cellid2[conv_idx]) + [cellid2[cv_ix] for cv_ix in conv_idx] ) new_node2 = np.full( (len(recarray),), None, dtype=object @@ -2918,16 +3016,16 @@ def _remap_obs(self, package, mapped_data, remapper, pkg_type=None): (len(recarray),), None, dtype=object ) - new_node2[conv_idx] = [remapper[i][-1] for i in node2] - new_model2[conv_idx] = [remapper[i][0] for i in node2] + new_node2[conv_idx] = self._node_map_arr[node2] + new_model2[conv_idx] = self._model_map_arr[node2] for ix in range(len(recarray)): if ix in conv_idx: continue else: - new_node2.append(None) - new_model2.append(new_model1[ix]) + new_node2[ix] = None + new_model2[ix] = int(new_model1[ix]) - if not np.allclose(new_model1, new_model2): + if not np.allclose(new_model1, new_model2.astype(int)): raise AssertionError( "One or more observation records cross model boundaries" ) @@ -2938,20 +3036,11 @@ def _remap_obs(self, package, mapped_data, remapper, pkg_type=None): for mkey, model in self._model_dict.items(): idx = np.asarray(new_model2 == mkey).nonzero() tmp_node = new_node2[idx] - cidx = np.asarray(tmp_node != None).nonzero() # noqa: E711 - tmp_cellid = model.modelgrid.get_lrc( - tmp_node[cidx].to_list() + tmp_layers = layers2[idx] + cidx = np.asarray(tmp_node != None).nonzero() # noqa: E711 + tmp_cellid = self._new_node_to_cellid( + model, tmp_node, tmp_layers, cidx ) - if self._modelgrid.grid_type in ( - "structured", - "vertex", - ): - tmp_layers = layers2[cidx] - tmp_cellid = [ - (tmp_layers[ix],) + cid[1:] - for ix, cid in enumerate(tmp_cellid) - ] - tmp_node[cidx] = tmp_cellid new_cellid2[idx] = tmp_node else: @@ -3104,13 +3193,8 @@ def _get_new_model_new_node(self, nodes): tuple (list, list) list of new model numbers and new node numbers """ - new_model = np.zeros((len(nodes),), dtype=int) - new_node = np.zeros((len(nodes),), dtype=int) - for ix, node in enumerate(nodes): - nm, nn = self._node_map[node] - new_model[ix] = nm - new_node[ix] = nn - + new_model = self._model_map_arr[nodes] + new_node = self._node_map_arr[nodes] return new_model, new_node def _new_node_to_cellid(self, model, new_node, layers, idx): @@ -3134,7 +3218,7 @@ def _new_node_to_cellid(self, model, new_node, layers, idx): new_node = new_node[idx].astype(int) if self._modelgrid.grid_type == "structured": - new_node += layers[idx] * model.modelgrid.ncpl + new_node += layers[idx].astype(int) * model.modelgrid.ncpl new_cellids = model.modelgrid.get_lrc(new_node.astype(int)) elif self._modelgrid.grid_type == "vertex": new_cellids = [tuple(cid) for cid in zip(layers[idx], new_node)] @@ -3374,7 +3458,7 @@ def _remap_package(self, package, ismvr=False): elif isinstance(package, modflow.ModflowUtlobs): if package.parent_file is not None: return {} - mapped_data = self._remap_obs(package, mapped_data, self._node_map) + mapped_data = self._remap_obs(package, mapped_data, None) elif isinstance(package, modflow.ModflowGwtfmi): mapped_data = self._remap_fmi(package, mapped_data) @@ -3484,7 +3568,7 @@ def _remap_package(self, package, ismvr=False): pass if hasattr(package, "obs"): - obs_map = {"cellid": dict(self._node_map)} + obs_map = {"cellid": {}} for mkey, mdict in mapped_data.items(): if "stress_period_data" in mdict: for _, ra in mdict["stress_period_data"].items(): diff --git a/flopy/modflow/mfsfr2.py b/flopy/modflow/mfsfr2.py index ecb7351078..628b0b38a4 100644 --- a/flopy/modflow/mfsfr2.py +++ b/flopy/modflow/mfsfr2.py @@ -458,10 +458,13 @@ def __init__( self.reach_data[n] = reach_data[n] # assign node numbers if there are none (structured grid) - if np.diff(self.reach_data.node).max() == 0 and self.parent.has_package("DIS"): - # first make kij list - lrc = np.array(self.reach_data)[["k", "i", "j"]].tolist() - self.reach_data["node"] = self.parent.dis.get_node(lrc) + diff = np.diff(self.reach_data.node) + if len(diff) > 0: + if diff.max() == 0 and self.parent.has_package("DIS"): + # first make kij list + lrc = np.array(self.reach_data)[["k", "i", "j"]].tolist() + self.reach_data["node"] = self.parent.dis.get_node(lrc) + # assign unique ID and outreach columns to each reach self.reach_data.sort(order=["iseg", "ireach"]) new_cols = { @@ -493,27 +496,30 @@ def __init__( for n in segment_data[i].dtype.names: self.segment_data[i][n] = segment_data[i][n] # compute outreaches if nseg and outseg columns have non-default values - if ( - np.diff(self.reach_data.iseg).max() != 0 - and np.max(list(set(self.graph.keys()))) != 0 - and np.max(list(set(self.graph.values()))) != 0 - ): - if len(self.graph) == 1: - self.segment_data[0]["nseg"] = 1 - self.reach_data["iseg"] = 1 - - consistent_seg_numbers = ( - len(set(self.reach_data.iseg).difference(set(self.graph.keys()))) == 0 - ) - if not consistent_seg_numbers: - warnings.warn( - "Inconsistent segment numbers of reach_data and segment_data" + diff = np.diff(self.reach_data.iseg) + if len(diff) > 0: + if ( + diff.max() != 0 + and np.max(list(set(self.graph.keys()))) != 0 + and np.max(list(set(self.graph.values()))) != 0 + ): + if len(self.graph) == 1: + self.segment_data[0]["nseg"] = 1 + self.reach_data["iseg"] = 1 + + consistent_seg_numbers = ( + len(set(self.reach_data.iseg).difference(set(self.graph.keys()))) + == 0 ) + if not consistent_seg_numbers: + warnings.warn( + "Inconsistent segment numbers of reach_data and segment_data" + ) - # first convert any not_a_segment_values to 0 - for v in self.not_a_segment_values: - self.segment_data[0].outseg[self.segment_data[0].outseg == v] = 0 - self.set_outreaches() + # first convert any not_a_segment_values to 0 + for v in self.not_a_segment_values: + self.segment_data[0].outseg[self.segment_data[0].outseg == v] = 0 + self.set_outreaches() self.channel_geometry_data = channel_geometry_data self.channel_flow_data = channel_flow_data diff --git a/flopy/utils/cvfdutil.py b/flopy/utils/cvfdutil.py index 038ab3da51..837ab2eed5 100644 --- a/flopy/utils/cvfdutil.py +++ b/flopy/utils/cvfdutil.py @@ -77,7 +77,7 @@ def shared_face(ivlist1, ivlist2): iv1 = ivlist1[i] iv2 = ivlist1[i + 1] for i2 in range(len(ivlist2) - 1): - if ivlist2[i2 : i2 + 1] == [iv2, iv1]: + if ivlist2[i2 : i2 + 2] == [iv2, iv1]: return True return False diff --git a/flopy/utils/get_modflow.py b/flopy/utils/get_modflow.py index 73e791edf2..5ae47e391c 100755 --- a/flopy/utils/get_modflow.py +++ b/flopy/utils/get_modflow.py @@ -8,12 +8,14 @@ See https://developer.github.com/v3/repos/releases/ for GitHub Releases API. """ +import http.client import json import os import shutil import ssl import sys import tempfile +import time import urllib import urllib.request import warnings @@ -29,7 +31,8 @@ default_owner = "MODFLOW-ORG" default_repo = "executables" -# key is the repo name, value is the renamed file prefix for the download +# key is the repo name, value is the renamed file prefix for the download; +# any other repo uses its name as the prefix renamed_prefix = { "modflow6": "modflow6", "executables": "modflow_executables", @@ -37,6 +40,29 @@ } available_repos = list(renamed_prefix.keys()) max_http_tries = 3 +http_retry_delay = 2.0 # base seconds between retries; grows exponentially +_max_retry_delay = 60.0 # ceiling on any single backoff sleep + +# HTTP status codes worth retrying +_retry_http_codes = {429, 500, 502, 503, 504} + + +def _env(name, default): + """ + Parse an env var as the type of ``default``. + Fall back to ``default`` on any error. + """ + val = os.environ.get(name) + if val is None: + return default + try: + return type(default)(val) + except (TypeError, ValueError): + return default + + +max_http_tries = max(1, _env("GET_MODFLOW_TRIES", max_http_tries)) +http_retry_delay = max(0.0, _env("GET_MODFLOW_RETRY_DELAY", http_retry_delay)) # Check if this is running from flopy within_flopy = False @@ -119,7 +145,77 @@ def urlopen(request, timeout=10, quiet=False): return urllib.request.urlopen(request, timeout=timeout, context=context) -def get_releases(owner=None, repo=None, quiet=False, per_page=None) -> List[str]: +def _is_transient(err) -> bool: + """Whether an exception from urlopen + read is worth retrying.""" + if isinstance(err, urllib.error.HTTPError): + return err.code in _retry_http_codes + if isinstance(err, urllib.error.URLError): + # SSLCertVerificationError is handled inside urlopen(); any other + # transport-level failure (reset, DNS blip, timeout) is retryable. + return not isinstance(err.reason, ssl.SSLCertVerificationError) + return isinstance( + err, (http.client.IncompleteRead, ConnectionResetError, TimeoutError) + ) + + +def _retry_config(tries, delay): + """Fill unset retry knobs from the module defaults and clamp to sane bounds.""" + tries = max_http_tries if tries is None else tries + delay = http_retry_delay if delay is None else delay + return max(1, tries), max(0.0, delay) + + +def _sleep_before_retry(attempt, tries, delay, err, quiet): + """Sleep with exponential backoff, capped at _max_retry_delay.""" + secs = min(delay * (2 ** (attempt - 1)), _max_retry_delay) + retry_after = getattr(err, "headers", None) and err.headers.get("Retry-After") + if retry_after and str(retry_after).isdigit(): + secs = max(secs, int(retry_after)) + if not quiet: + print(f" attempt {attempt}/{tries} failed ({err}); retrying in {secs:.0f}s") + time.sleep(secs) + + +def fetch(request, timeout=10, quiet=False, tries=None, delay=None): + """Send a request, returning (response body, headers). Retries on failure.""" + tries, delay = _retry_config(tries, delay) + for attempt in range(1, tries + 1): + try: + with urlopen(request, timeout=timeout, quiet=quiet) as resp: + return resp.read(), resp.headers + except Exception as err: + if attempt == tries or not _is_transient(err): + raise + _sleep_before_retry(attempt, tries, delay, err, quiet) + + +def download( + request, dest_path, timeout=120, quiet=False, tries=None, delay=None +) -> None: + """Download (stream) a file to dest_path (atomic via a .part file).""" + tries, delay = _retry_config(tries, delay) + dest_path = Path(dest_path) + part_path = dest_path.with_name(dest_path.name + ".part") + for attempt in range(1, tries + 1): + try: + with urlopen(request, timeout=timeout, quiet=quiet) as resp: + with open(part_path, "wb") as f: + shutil.copyfileobj(resp, f) + os.replace(part_path, dest_path) + return + except Exception as err: + try: + part_path.unlink() + except FileNotFoundError: + pass + if attempt == tries or not _is_transient(err): + raise + _sleep_before_retry(attempt, tries, delay, err, quiet) + + +def get_releases( + owner=None, repo=None, quiet=False, per_page=None, tries=None, delay=None +) -> List[str]: """Get list of available releases.""" owner = default_owner if owner is None else owner repo = default_repo if repo is None else repo @@ -132,25 +228,16 @@ def get_releases(owner=None, repo=None, quiet=False, per_page=None) -> List[str] params["per_page"] = per_page request = get_request(req_url, params=params) - num_tries = 0 - while True: - num_tries += 1 - try: - with urlopen(request, timeout=10, quiet=quiet) as resp: - result = resp.read() - break - except urllib.error.HTTPError as err: - if err.code == 401 and os.environ.get("GITHUB_TOKEN"): - raise ValueError("GITHUB_TOKEN env is invalid") from err - elif err.code == 403 and "rate limit exceeded" in err.reason: - raise ValueError( - f"use GITHUB_TOKEN env to bypass rate limit ({err})" - ) from err - elif err.code in {404, 503} and num_tries < max_http_tries: - # GitHub sometimes returns this error for valid URLs, so retry - print(f"URL request {num_tries} did not work ({err})") - continue - raise RuntimeError(f"cannot retrieve data from {req_url}") from err + try: + result, _ = fetch(request, timeout=10, quiet=quiet, tries=tries, delay=delay) + except urllib.error.HTTPError as err: + if err.code == 401 and os.environ.get("GITHUB_TOKEN"): + raise ValueError("GITHUB_TOKEN env is invalid") from err + elif err.code == 403 and "rate limit exceeded" in err.reason: + raise ValueError( + f"use GITHUB_TOKEN env to bypass rate limit ({err})" + ) from err + raise RuntimeError(f"cannot retrieve data from {req_url}") from err releases = json.loads(result.decode()) if not quiet: @@ -161,7 +248,9 @@ def get_releases(owner=None, repo=None, quiet=False, per_page=None) -> List[str] return avail_releases -def get_release(owner=None, repo=None, tag="latest", quiet=False) -> dict: +def get_release( + owner=None, repo=None, tag="latest", quiet=False, tries=None, delay=None +) -> dict: """Get info about a particular release.""" owner = default_owner if owner is None else owner repo = default_repo if repo is None else repo @@ -172,40 +261,30 @@ def get_release(owner=None, repo=None, tag="latest", quiet=False) -> dict: else f"{api_url}/releases/tags/{tag}" ) request = get_request(req_url) - releases = None - num_tries = 0 - while True: - num_tries += 1 - try: - with urlopen(request, timeout=10, quiet=quiet) as resp: - result = resp.read() - remaining = resp.headers.get("x-ratelimit-remaining", None) - if remaining and int(remaining) <= 10: - warnings.warn( - f"Only {remaining} GitHub API requests remaining " - "before rate-limiting" - ) - break - except urllib.error.HTTPError as err: - if err.code == 401 and os.environ.get("GITHUB_TOKEN"): - raise ValueError("GITHUB_TOKEN env is invalid") from err - elif err.code == 403 and "rate limit exceeded" in err.reason: + try: + result, headers = fetch( + request, timeout=10, quiet=quiet, tries=tries, delay=delay + ) + remaining = headers.get("x-ratelimit-remaining", None) + if remaining and int(remaining) <= 10: + warnings.warn( + f"Only {remaining} GitHub API requests remaining before rate-limiting" + ) + except urllib.error.HTTPError as err: + if err.code == 401 and os.environ.get("GITHUB_TOKEN"): + raise ValueError("GITHUB_TOKEN env is invalid") from err + elif err.code == 403 and "rate limit exceeded" in err.reason: + raise ValueError( + f"use GITHUB_TOKEN env to bypass rate limit ({err})" + ) from err + elif err.code == 404: + releases = get_releases(owner, repo, quiet, tries=tries, delay=delay) + if tag not in releases: raise ValueError( - f"use GITHUB_TOKEN env to bypass rate limit ({err})" + f"Release {tag} not found (choose from {', '.join(releases)})" ) from err - elif err.code == 404: - if releases is None: - releases = get_releases(owner, repo, quiet) - if tag not in releases: - raise ValueError( - f"Release {tag} not found (choose from {', '.join(releases)})" - ) - elif err.code == 503 and num_tries < max_http_tries: - # GitHub sometimes returns this error for valid URLs, so retry - warnings.warn(f"URL request {num_tries} did not work ({err})") - continue - raise RuntimeError(f"cannot retrieve data from {req_url}") from err + raise RuntimeError(f"cannot retrieve data from {req_url}") from err release = json.loads(result.decode()) tag_name = release["tag_name"] @@ -314,6 +393,8 @@ def run_main( downloads_dir=None, force=False, quiet=False, + tries=None, + retry_delay=None, _is_cli=False, ): """Run main method to get MODFLOW and related programs. @@ -327,8 +408,10 @@ def run_main( owner : str, default "MODFLOW-ORG" Name of GitHub repository owner (user or organization). repo : str, default "executables" - Name of GitHub repository. Choose one of "executables" (default), - "modflow6", or "modflow6-nightly-build". + Name of GitHub repository, such as "executables" (default), + "modflow6", or "modflow6-nightly-build". Any other repository can be + used if its release assets are named like those of these + repositories, with the operating system tag (e.g. "linux.zip"). release_id : str, default "latest" GitHub release ID. ostag : str, optional @@ -344,6 +427,12 @@ def run_main( previously downloaded in ``downloads_dir``. quiet : bool, default False If True, show fewer messages. + tries : int, optional + Attempts per network request. Defaults to the ``GET_MODFLOW_TRIES`` + environment variable, or 3. + retry_delay : float, optional + Base seconds between retries, growing exponentially. Defaults to the + ``GET_MODFLOW_RETRY_DELAY`` environment variable, or 2.0. _is_cli : bool, default False Control behavior of method if this is run as a command-line interface or as a Python function. @@ -418,12 +507,10 @@ def run_main( elif not os.access(bindir, os.W_OK): raise OSError(f"extraction directory '{bindir}' is not writable") - # make sure repo option is valid - if repo not in available_repos: - raise KeyError(f"repo {repo!r} not supported; choose one of {available_repos}") - # get the selected release - release = get_release(owner, repo, release_id, quiet) + release = get_release( + owner, repo, release_id, quiet, tries=tries, delay=retry_delay + ) assets = release.get("assets", []) for asset in assets: asset_name = asset["name"] @@ -441,7 +528,8 @@ def run_main( dst_fname = "-".join([repo, release["tag_name"], ostag]) + asset_suffix else: # change local download name so it is more unique - dst_fname = "-".join([renamed_prefix[repo], release["tag_name"], asset_name]) + prefix = renamed_prefix.get(repo, repo) + dst_fname = "-".join([prefix, release["tag_name"], asset_name]) tmpdir = None if downloads_dir is None: downloads_dir = Path.home() / "Downloads" @@ -464,11 +552,14 @@ def run_main( else: if not quiet: print(f"downloading '{download_url}' to '{download_pth}'") - with urlopen( - urllib.request.Request(download_url), timeout=120, quiet=quiet - ) as resp: - with open(download_pth, "wb") as f: - shutil.copyfileobj(resp, f) + download( + urllib.request.Request(download_url), + download_pth, + timeout=120, + quiet=quiet, + tries=tries, + delay=retry_delay, + ) if subset: if isinstance(subset, str): @@ -710,9 +801,10 @@ def cli_main(): ) parser.add_argument( "--repo", - choices=available_repos, default=default_repo, - help=f"Name of GitHub repository; default is '{default_repo}'.", + help=f"Name of GitHub repository, such as {', '.join(available_repos)}; " + f"default is '{default_repo}'. Release assets of any other repository " + "must be named like those of these repositories.", ) parser.add_argument( "--release-id", @@ -739,6 +831,20 @@ def cli_main(): "previously downloaded in downloads-dir.", ) parser.add_argument("--quiet", action="store_true", help="Show fewer messages.") + parser.add_argument( + "--tries", + type=int, + default=None, + help="Number of attempts per network request; default is " + f"{max_http_tries} (env: GET_MODFLOW_TRIES). Use 1 to disable retries.", + ) + parser.add_argument( + "--retry-delay", + type=float, + default=None, + help="Base seconds between retries, grows exponentially; default is " + f"{http_retry_delay} (env: GET_MODFLOW_RETRY_DELAY).", + ) args = vars(parser.parse_args()) try: run_main(**args, _is_cli=True) diff --git a/flopy/utils/mflistfile.py b/flopy/utils/mflistfile.py index 18ae848de6..4301c9be90 100644 --- a/flopy/utils/mflistfile.py +++ b/flopy/utils/mflistfile.py @@ -16,6 +16,24 @@ from ..utils.flopy_io import get_ts_sp from ..utils.utils_def import totim_to_datetime +# Fortran E/ES edit descriptors without an exponent width drop the "E" +# when the exponent has three digits, e.g. 8.5159E-100 -> 8.5159-100 +_MISSING_E = re.compile(r"^([+-]?\d*\.?\d+)([+-]\d{3})$") + + +def _parse_float(s): + """Parse a float from a list file, tolerating a missing "E" and NaN.""" + s = s.strip() + try: + return float(s) + except ValueError: + m = _MISSING_E.match(s) + if m: + return float(f"{m.group(1)}E{m.group(2)}") + if "NAN" in s.upper(): + return np.nan + return None + class ListBudget: """ @@ -612,20 +630,23 @@ def _build_index(self, maxentries): def _get_index(self, maxentries): # --parse through the file looking for matches and parsing ts and sp idxs = [] - l_count = 1 + l_count = 0 while True: seekpoint = self.f.tell() line = self.f.readline() + l_count += 1 if line == "": break if self.budgetkey in line: for _ in range(self.tssp_lines): line = self.f.readline() + l_count += 1 try: ts, sp = get_ts_sp(line) - except: + except Exception: print( - "unable to cast ts,sp on line number", l_count, " line: ", line + "could not parse time step and stress period " + f"on line {l_count}: {line.strip()!r}" ) break @@ -666,14 +687,13 @@ def _set_entries(self): raise Exception(f"entries already set:{self.entries}") if not self.idx_map: raise Exception("must call build_index before call set_entries") + ts, sp, seekpoint = self.idx_map[0] try: - incdict, cumdict = self._get_sp( - self.idx_map[0][0], self.idx_map[0][1], self.idx_map[0][2] - ) - except: - raise Exception( - "unable to read budget information from first entry in list file" - ) + incdict, cumdict = self._get_sp(ts, sp, seekpoint) + except Exception as e: + raise ValueError( + f"unable to read first budget in list file {self.file_name}: {e}" + ) from e self.entries = incdict.keys() null_entries = {} incdict = {} @@ -741,17 +761,24 @@ def _load(self, maxentries=None): return + def _sp_failed(self, msg): + # entries are read from the first budget, so until they are set + # there are no null entries to fall back on + if not self.entries: + raise ValueError(msg) + print(msg) + return self.null_entries + def _get_sp(self, ts, sp, seekpoint): self.f.seek(seekpoint) # --read to the start of the "in" budget information while True: line = self.f.readline() if line == "": - print( - "end of file found while seeking budget " - f"information for ts,sp: {ts} {sp}" + return self._sp_failed( + "end of file found while reading budget " + f"at time step {ts}, stress period {sp}" ) - return self.null_entries # --if there are two '=' in this line, then it is a budget line if len(re.findall(r"=", line)) == 2: @@ -763,27 +790,19 @@ def _get_sp(self, ts, sp, seekpoint): entrydict = {} while True: if line == "": - print( - "end of file found while seeking budget " - f"information for ts,sp: {ts} {sp}" + return self._sp_failed( + "end of file found while reading budget " + f"at time step {ts}, stress period {sp}" ) - return self.null_entries if len(re.findall(r"=", line)) == 2: try: entry, flux, cumu = self._parse_budget_line(line) except Exception: - print("error parsing budget line in ts,sp", ts, sp) - return self.null_entries - if flux is None: - print( - "error casting in flux for", entry, " to float in ts,sp", ts, sp + return self._sp_failed( + "could not parse budget line at time step " + f"{ts}, stress period {sp}: {line.strip()!r}" ) - return self.null_entries - if cumu is None: - print( - "error casting in cumu for", entry, " to float in ts,sp", ts, sp - ) - return self.null_entries + label = entry if entry.endswith(tag.upper()): if " - " in entry.upper(): key = entry.replace(" ", "") @@ -800,6 +819,12 @@ def _get_sp(self, ts, sp, seekpoint): else: entrydict[entry] = 0 key = f"{entry}_{tag}" + label = f"{label} ({tag})" + if flux is None or cumu is None: + return self._sp_failed( + f"could not parse value for {label} at time step {ts}, " + f"stress period {sp}: {line.strip()!r}" + ) incdict[key] = flux cumdict[key] = cumu else: @@ -825,17 +850,8 @@ def _parse_budget_line(self, line): idx = line2.index("=") + 1 fx_str = line2[idx:].split()[0].strip() - flux, cumu = None, None - try: - cumu = float(cu_str) - except: - if "NAN" in cu_str.strip().upper(): - cumu = np.nan - try: - flux = float(fx_str) - except: - if "NAN" in fx_str.strip().upper(): - flux = np.nan + cumu = _parse_float(cu_str) + flux = _parse_float(fx_str) return entry, flux, cumu def _get_totim(self, ts, sp, seekpoint): @@ -847,8 +863,8 @@ def _get_totim(self, ts, sp, seekpoint): ihead += 1 if line == "": print( - "end of file found while seeking budget " - f"information for ts,sp: {ts} {sp}" + "end of file found while reading time summary " + f"at time step {ts}, stress period {sp}" ) return np.nan, np.nan, np.nan elif ( @@ -864,28 +880,37 @@ def _get_totim(self, ts, sp, seekpoint): translen = self._parse_time_line(line) line = self.f.readline() if translen is None: - print("error parsing translen for ts,sp", ts, sp) + print( + f"could not parse transport step length at time step {ts}, " + f"stress period {sp}" + ) return np.nan, np.nan, np.nan tslen = self._parse_time_line(line) if tslen is None: - print("error parsing tslen for ts,sp", ts, sp) + print( + f"could not parse time step length at time step {ts}, " + f"stress period {sp}" + ) return np.nan, np.nan, np.nan sptim = self._parse_time_line(self.f.readline()) if sptim is None: - print("error parsing sptim for ts,sp", ts, sp) + print( + f"could not parse stress period time at time step {ts}, " + f"stress period {sp}" + ) return np.nan, np.nan, np.nan totim = self._parse_time_line(self.f.readline()) if totim is None: - print("error parsing totim for ts,sp", ts, sp) + print(f"could not parse total time at time step {ts}, stress period {sp}") return np.nan, np.nan, np.nan return tslen, sptim, totim def _parse_time_line(self, line): if line == "": - print("end of file found while parsing time information") + print("end of file found while reading time summary") return None try: time_str = line[self.time_line_idx :] @@ -901,7 +926,7 @@ def _parse_time_line(self, line): idx = 0 tval = float(raw[idx]) except: - print("error parsing tslen information: ", time_str) + print(f"could not parse time value: {line.strip()!r}") return None return tval diff --git a/flopy/utils/rasters.py b/flopy/utils/rasters.py index caae365f1f..4c31c484ee 100644 --- a/flopy/utils/rasters.py +++ b/flopy/utils/rasters.py @@ -1,5 +1,6 @@ import warnings from os import PathLike +from pathlib import Path from typing import Union import numpy as np @@ -828,21 +829,21 @@ def get_array(self, band, masked=True): return array - def write(self, name): + def write(self, name: Union[str, PathLike]): """ Method to write raster data to a .tif file Parameters ---------- - name : str + name : PathLike output raster .tif file name """ rasterio = import_optional_dependency("rasterio") - if not name.endswith(".tif"): - name += ".tif" + if not str(name).endswith(".tif"): + name = Path(f"{name!s}.tif") with rasterio.open(name, "w", **self._meta) as foo: for band, arr in self.__arr_dict.items(): diff --git a/flopy/version.py b/flopy/version.py index 1fadd91cb1..4f8f6b7a1d 100644 --- a/flopy/version.py +++ b/flopy/version.py @@ -1,4 +1,4 @@ # flopy version file automatically created using -# update_version.py on September 03, 2026 13:56:50 +# update_version.py on October 31, 2026 23:46:14 -__version__ = "3.11.0" +__version__ = "3.12.0" diff --git a/pyproject.toml b/pyproject.toml index 4816231822..7a047a7fca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,17 +18,16 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Hydrology", ] -requires-python = ">=3.11" +requires-python = ">=3.12" dependencies = [ - "numpy>=1.20.3,<3.0", + "numpy>=2.2.0,<3.0", "matplotlib >=1.4.0", - "pandas >=2.0.0", + "pandas >=2.2.0", ] dynamic = ["version", "readme"] diff --git a/scripts/README.md b/scripts/README.md index 64b0536af3..12321ada7c 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -39,16 +39,21 @@ The `update_version.py` script can be used to update FloPy version numbers. Runn The script acquires a file lock before writing to files to make sure only one process edits the files at any given time and prevent desynchronization. -If the script is run with no arguments, the version number is not changed, but updated timestamps are written. To set the version number, use the `--version` (short `-v`) option, e.g.: +If the script is run with no arguments, the version number is not changed, but updated timestamps are written. In all cases the resulting version is printed to stdout. To set the version number, use the `--version` (short `-v`) option, e.g.: ```shell python scripts/update_version.py -v 3.3.6 ``` -To get the current version number, use the `--get` flag (short `-g`): +Two flags derive the version instead of taking it explicitly: + +- `--release` (short `-r`) strips the development segment (e.g. `.dev0`) from the current version, giving the version to release. +- `--post-release` (short `-p`) gives the development version for the next cycle: the next minor version, with a `.devN` segment where `N` is the patch number just released (e.g. `3.10.0.dev2` after `3.9.2`). + +To print the version without writing any changes, add `--dry-run`: ```shell -python scripts/update_version.py -g +python scripts/update_version.py --dry-run ``` -This simply returns the contents of `version.txt` and does not write any changes to the repository's files. +With no other flag this simply returns the contents of `version.txt`. diff --git a/scripts/update_version.py b/scripts/update_version.py index 30f36eef58..a8c9d33c13 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -1,5 +1,6 @@ import argparse import re +import sys import textwrap from datetime import datetime from pathlib import Path @@ -9,12 +10,13 @@ from packaging.version import Version _epilog = """\ -Update version information stored in version.txt in the project root, -as well as several other files in the repository. If --version is not -provided, the version number will not be changed. A file lock is held -to synchronize file access. The version tag must comply with standard -'..' format conventions for semantic versioning. -To show the version without changing anything, use --get (short -g). +Update version information stored in version.txt in the project root, as +well as several other files in the repository, and print the new version. +If none of --version, --release or --post-release is provided, the version +number is not changed. A file lock is held to synchronize file access. The +version tag must comply with standard '..' format +conventions for semantic versioning. To print the version without changing +anything, add --dry-run. """ _project_name = "flopy" _project_root_path = Path(__file__).parent.parent @@ -39,10 +41,27 @@ def split_nonnumeric(s): _current_version = Version(_version_txt_path.read_text().strip()) +def release_version() -> Version: + """Current version with any development segment (e.g. '.dev0') removed.""" + return Version(_current_version.base_version) + + +def post_release_version() -> Version: + """Development version for the next cycle, following a release. + + Targets the next anticipated minor version, with the development segment + set to the micro (patch) number of the version just released: e.g. after + 3.9.2 comes 3.10.0.dev2, and after 3.11.0 comes 3.12.0.dev0. The counter + marks how many releases into the series development has resumed. + """ + v = Version(_current_version.base_version) + return Version(f"{v.major}.{v.minor + 1}.0.dev{v.micro}") + + def update_version_txt(version: Version): with open(_version_txt_path, "w") as f: f.write(str(version)) - print(f"Updated {_version_txt_path} to version {version}") + print(f"Updated {_version_txt_path} to version {version}", file=sys.stderr) def update_version_py(timestamp: datetime, version: Version): @@ -53,7 +72,7 @@ def update_version_py(timestamp: datetime, version: Version): ) f.write(f'__version__ = "{version}"\n') f.close() - print(f"Updated {_version_py_path} to version {version}") + print(f"Updated {_version_py_path} to version {version}", file=sys.stderr) def get_software_citation(timestamp: datetime, version: Version): @@ -131,7 +150,7 @@ def update_readme_markdown(timestamp: datetime, version: Version): f.write(f"{line}\n") - print(f"Updated {fpth} to version {version}") + print(f"Updated {fpth} to version {version}", file=sys.stderr) def update_citation_cff(timestamp: datetime, version: Version): @@ -149,7 +168,7 @@ def update_citation_cff(timestamp: datetime, version: Version): citation, f, allow_unicode=True, default_flow_style=False, sort_keys=False ) - print(f"Updated {fpth} to version {version}") + print(f"Updated {fpth} to version {version}", file=sys.stderr) def update_pypi_release(timestamp: datetime, version: Version): @@ -166,7 +185,7 @@ def update_pypi_release(timestamp: datetime, version: Version): f.write(f"{line}\n") f.close() - print(f"Updated {fpth} to version {version}") + print(f"Updated {fpth} to version {version}", file=sys.stderr) def update_version( @@ -180,7 +199,7 @@ def update_version( version = ( version if version - else Version(previous.major, previous.minor, previous.micro) + else Version(f"{previous.major}.{previous.minor}.{previous.micro}") ) with lock: @@ -209,18 +228,42 @@ def update_version( help="Specify the release version", ) parser.add_argument( - "-g", - "--get", + "-r", + "--release", required=False, action="store_true", - help="Just get the current version number, no updates (defaults false)", + help=( + "Use the current development version with its development segment " + "(e.g. '.dev0') removed" + ), + ) + parser.add_argument( + "-p", + "--post-release", + required=False, + action="store_true", + help=( + "Use the development version for the next cycle: the next minor " + "version, with a development segment set to the released patch number" + ), + ) + parser.add_argument( + "--dry-run", + required=False, + action="store_true", + help="Print the version that would be written, and exit without writing", ) args = parser.parse_args() - if args.get: - print(_current_version) + if args.post_release: + version = post_release_version() + elif args.release: + version = release_version() + elif args.version: + version = Version(args.version) else: - update_version( - timestamp=datetime.now(), - version=(Version(args.version) if args.version else _current_version), - ) + version = _current_version + + if not args.dry_run: + update_version(timestamp=datetime.now(), version=version) + print(version) diff --git a/version.txt b/version.txt index e0af1238f7..87dbaa157f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.11.0 \ No newline at end of file +3.12.0 \ No newline at end of file