diff --git a/docs/gap_roadmap.md b/docs/gap_roadmap.md index 4822bb0..c27901d 100644 --- a/docs/gap_roadmap.md +++ b/docs/gap_roadmap.md @@ -86,7 +86,7 @@ less often. The technical detail follows for whoever picks the item up. | 8 | Give user-facing names a clear meaning | Open | | 9 | Represent a localized casing hole | Experimental; static opening/isolation and one-year pressure-driven transfer verified | | 10 | Generate hole-transfer reporting from the workbook | Done; verified on CIRRUS (Smeaheia, one year) | -| 11 | Align FIPLEG aquifer reporting with the current coarse-grid boundaries | Open; region 5 definition needs review | +| 11 | Align FIPLEG aquifer reporting with the current coarse-grid boundaries | Implemented; CIRRUS INIT check pending | | 12 | Run only new or unfinished workbook cases | Open; batch reruns every case and rewrites the manifest | | 13 | Clean up the CIRRUS template deck | Partial; generated `BRINE` now matches salt tables; CIRRUS comparison and cleanup pending | @@ -560,10 +560,23 @@ invalid locations, and unchanged cases without reporting. ### 11. Align FIPLEG aquifer reporting with the current coarse-grid boundaries -**Status: open; outdated definition reported by the user (2026-10-12).** -FIPLEG 5 was intended to identify aquifer cells with multiplied pore volume. -The aquifer representation has since moved to the outer edges of the coarse -grid; its reporting zonation needs to be checked against that current placement. +**Status: implemented; CIRRUS INIT check pending.** Outdated definition +reported by the user (2026-10-12): FIPLEG 5 was intended to identify aquifer +cells with multiplied pore volume. The aquifer representation moved to the +outer edges of the coarse grid, but region 5 stayed on the bottom +`aquifer_layers` of the reservoir. + +**Decision (2026-10-17):** region 5 is the four outer coarse-grid columns in the +reservoir layers. Water and overburden edge cells keep regions 1 and 2, even +though their pore volume is also multiplied, so ocean and overburden regions +keep their identity. Region 3 is now the whole reservoir minus those edge +columns, including the bottom `aquifer_layers`, which keep `aquifer_permx` but +are no longer labelled 5. The edge boxes for `FIPLEG 5` and the `PORV` +multiplier come from one shared list in `prepare_init_case.py`, so they cannot +drift apart. The wellbore cells (region 6: pipe, open hole and casing-cement boxes) sit in +a central parent cell (10, 10 in the default 20 x 20 grid), away from the edge +columns; other grid sizes were not checked. Cases generated earlier are not +comparable region-for-region. **In practice:** regional inventories and transfers labelled as aquifer quantities should refer to the actual boundary-aquifer cells, not their former diff --git a/runscripts/prepare_init_case.py b/runscripts/prepare_init_case.py index 01bd7ad..d8ef4ae 100644 --- a/runscripts/prepare_init_case.py +++ b/runscripts/prepare_init_case.py @@ -142,6 +142,12 @@ def region(keyword: str, value: object, start: int, end: int) -> str: formatted_value = f"{value:g}" if isinstance(value, (int, float)) else str(value) return f"{keyword} {formatted_value} 1 {nx} 1 {ny} {start} {end} /" + # Outer coarse-grid columns (i0 i1 j0 j1): the aquifer boundary used for both PORV and FIPLEG 5. + edge_columns = ((1, 1, 1, ny), (nx, nx, 1, ny), (2, nx - 1, 1, 1), (2, nx - 1, ny, ny)) + + def edge_region(keyword: str, value: object, start: int, end: int) -> list[str]: + return [f"{keyword} {value:g} {i0} {i1} {j0} {j1} {start} {end} /" for i0, i1, j0, j1 in edge_columns] + lines = [ "EQUALS", "FIPLEG 4 /", @@ -163,14 +169,15 @@ def region(keyword: str, value: object, start: int, end: int) -> str: "", "-- Reservoir and aquifer properties", ] + lines.append(region("FIPLEG", 3, reservoir_start, reservoir_end)) if aquifer_start is None: - lines.extend([region("PERMX", reservoir_permx, reservoir_start, reservoir_end), region("FIPLEG", 3, reservoir_start, reservoir_end)]) + lines.append(region("PERMX", reservoir_permx, reservoir_start, reservoir_end)) else: if aquifer_start > reservoir_start: - lines.extend( - [region("PERMX", reservoir_permx, reservoir_start, aquifer_start - 1), region("FIPLEG", 3, reservoir_start, aquifer_start - 1)] - ) - lines.extend([region("PERMX", aquifer_permx, aquifer_start, reservoir_end), region("FIPLEG", 5, aquifer_start, reservoir_end)]) + lines.append(region("PERMX", reservoir_permx, reservoir_start, aquifer_start - 1)) + lines.append(region("PERMX", aquifer_permx, aquifer_start, reservoir_end)) + lines.append("-- FIPLEG 5: aquifer boundary, the outer coarse-grid columns in reservoir layers") + lines.extend(edge_region("FIPLEG", 5, reservoir_start, reservoir_end)) lines.extend( [ "", @@ -187,10 +194,7 @@ def region(keyword: str, value: object, start: int, end: int) -> str: "", "MULTIPLY", "-- Multiply pore volume on the four outer coarse-grid sides", - f"PORV {porv_multiplier:g} 1 1 1 {ny} 1 {nz} /", - f"PORV {porv_multiplier:g} {nx} {nx} 1 {ny} 1 {nz} /", - f"PORV {porv_multiplier:g} 2 {nx - 1} 1 1 1 {nz} /", - f"PORV {porv_multiplier:g} 2 {nx - 1} {ny} {ny} 1 {nz} /", + *edge_region("PORV", porv_multiplier, 1, nz), "", "-- Do not reduce permeability in the water layer", f"PERMZ {permz_multiplier:g} 1 {nx} 1 {ny} {water_end + 1} {nz} /", diff --git a/src/GaP/README.md b/src/GaP/README.md index 0e2a125..a30f9a5 100644 --- a/src/GaP/README.md +++ b/src/GaP/README.md @@ -264,6 +264,14 @@ uv run --extra visualization python runscripts/plot_region_fluxes.py \ --output-root cases --zonation FIPLEG --list-vectors ``` +Generated coarse grids label `FIPLEG` as: 1 water, 2 overburden, 3 reservoir, +5 aquifer boundary, and 6 the wellbore cells inside the LGR (pipe, open hole +and casing-cement boxes; the rest of the LGR is not labelled 6). Region 5 is the four outer coarse-grid +columns in the reservoir layers, the cells whose pore volume is multiplied +(`porv_multiplier`); the water and overburden edge cells keep 1 and 2. Cases +generated before this definition (region 5 on the bottom `aquifer_layers`) are +not comparable region-for-region. + The list reports the exact keys and the cases exporting each one. If `1-6` exists but `6-1` does not, only `1-6` is selectable; reversing a pair is not inferred. Select a pair with `--vector`, repeat it for multiple panels, or diff --git a/tests/gap/test_prepare_init_case_from_xlsx.py b/tests/gap/test_prepare_init_case_from_xlsx.py index 478d90c..d8fac05 100644 --- a/tests/gap/test_prepare_init_case_from_xlsx.py +++ b/tests/gap/test_prepare_init_case_from_xlsx.py @@ -1,3 +1,4 @@ +import importlib.util from pathlib import Path import subprocess import sys @@ -11,6 +12,12 @@ from src.WellClass.libs.well_pressure.pressure_table import PressureTable +PREPARE_PATH = Path(__file__).parents[2] / "runscripts/prepare_init_case.py" +PREPARE_SPEC = importlib.util.spec_from_file_location("prepare_init_case", PREPARE_PATH) +prepare_init_case = importlib.util.module_from_spec(PREPARE_SPEC) +PREPARE_SPEC.loader.exec_module(prepare_init_case) + + def _write_minimal_workbook(path: Path, *, salinity: float = 0.032) -> None: metadata = pd.DataFrame({"key": ["namespace", "name", "author"], "value": ["screen", "xlsx-test", "pytest"]}) header = pd.DataFrame( @@ -127,13 +134,43 @@ def test_prepare_init_case_from_xlsx_stages_files(tmp_path): assert "PERMX 10000 1 20 1 20 1 3 /" in grdecl_text assert "PERMX 0.001 1 20 1 20 4 18 /" in grdecl_text assert "PORO 1 1 20 1 20 1 3 /" in grdecl_text - assert "FIPLEG 3 1 20 1 20 19 55 /" in grdecl_text - assert "FIPLEG 5 1 20 1 20 56 58 /" in grdecl_text + assert "FIPLEG 3 1 20 1 20 19 58 /" in grdecl_text + assert "FIPLEG 5 1 1 1 20 19 58 /" in grdecl_text assert "PERMX 1000 1 20 1 20 56 58 /" in grdecl_text assert "TRANZ 0 1 20 1 20 19 19 /" in grdecl_text assert "PERMZ 0.1 1 20 1 20 4 58 /" in grdecl_text +def _boxes(text: str, keyword: str) -> list[tuple[str, ...]]: + return [tuple(line.split()[2:]) for line in text.splitlines() if line.startswith(f"{keyword} ")] + + +@pytest.mark.parametrize("aquifer_layers", [0, 3]) +def test_fipleg_aquifer_marks_reservoir_edge_columns_with_multiplied_pore_volume(tmp_path, aquifer_layers): + root = Path(__file__).parents[2] + grdecl = tmp_path / "grid.grdecl" + grdecl.write_text((root / "test_data/examples/wildcat-pflotran/include/TEMP_GRD.grdecl").read_text(encoding="utf-8")) + prepare_init_case.parameterize_grdecl( + grdecl, water_layers=3, overburden_layers=15, reservoir_layers=40, + aquifer_layers=aquifer_layers, aquifer_permx=250.0, + ) + text = grdecl.read_text(encoding="utf-8") + edges = [("1", "1", "1", "20"), ("20", "20", "1", "20"), ("2", "19", "1", "1"), ("2", "19", "20", "20")] + porv = _boxes(text, "PORV") + assert [box[:4] for box in porv] == edges + assert {box[4:] for box in porv} == {("1", "58", "/")} + fipleg5 = [tuple(line.split()[2:]) for line in text.splitlines() if line.startswith("FIPLEG 5 ")] + assert [box[:4] for box in fipleg5] == edges + assert {box[4:] for box in fipleg5} == {("19", "58", "/")} + # Water, overburden and the interior of the reservoir keep their own regions. + assert "FIPLEG 1 1 20 1 20 1 3 /" in text + assert "FIPLEG 2 1 20 1 20 4 18 /" in text + assert "FIPLEG 3 1 20 1 20 19 58 /" in text + assert text.index("FIPLEG 3 1 20 1 20 19 58 /") < text.index("FIPLEG 5 1 1 1 20 19 58 /") + # Physical aquifer properties are unchanged: the bottom layers keep aquifer_permx. + assert ("PERMX 250 1 20 1 20 56 58 /" in text) == bool(aquifer_layers) + + def test_workbook_keeps_simulation_assumptions_outside_well_model(tmp_path): workbook = tmp_path / "well_input.xlsx" _write_minimal_workbook(workbook)