diff --git a/README.md b/README.md index 8460350..01a8086 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ usage: em2ex.py [-h] [-o OUTPUT_FILE] [--filetype {eclipse,leapfrog}] [--pinch-tol PINCH_TOL] [--refine-xy RX RY] [--extract-i I_LO I_HI] [--extract-j J_LO J_HI] [--extract-k K_LO K_HI] [--extra-keywords KEY [KEY ...]] - [--fault-sidesets] + [--fault-sidesets] [--convert-to-m] filename Converts earth model to Exodus II format @@ -146,6 +146,10 @@ options: "fault_secondary" containing the faces on either side of every fault (any internal face where adjacent cells do not share their corner nodes). + --convert-to-m Convert grid coordinates to metres on output, using + the input file's GRIDUNIT keyword as the source unit. + Supported values are METRES (no-op), FEET and CM. + Files without GRIDUNIT are assumed to be in metres. ``` ### Lateral refinement (Eclipse only) @@ -226,6 +230,33 @@ Detection is purely topological: any internal face where the two adjacent cells Without `--fault-sidesets`, the output is unchanged and only the six standard boundary sidesets are written. +### Coordinate units (Eclipse only) + +The Eclipse `GRIDUNIT` keyword declares the length unit of the grid. em2ex recognises three values: + +| `GRIDUNIT` value | Length unit | Factor to metres | +|---|---|---| +| `METRES` (or absent — the Eclipse default) | metres | 1.0 | +| `FEET` | US survey feet | 0.3048 | +| `CM` | centimetres | 0.01 | + +By default, em2ex **preserves the input file's units** — the numbers in `COORD` and `ZCORN` are passed through to the Exodus mesh unchanged. The Exodus format itself has no concept of length units, so it's the modeller's responsibility to remember (or document downstream) what units the mesh is in. + +To convert to metres on output, pass `--convert-to-m`: + +```bash +./em2ex.py --convert-to-m model.grdecl +``` + +This multiplies every coordinate (`COORD` x/y/z and `ZCORN` z) by the appropriate factor and prints a one-line confirmation. **Per-cell property values are never converted** — `--convert-to-m` only affects geometry. + +A few practical notes: + +- **Non-metres files trigger an info note** at the start of the run telling you what unit the file is in and reminding you about `--convert-to-m`. The conversion is opt-in — em2ex never silently rescales your data. +- **Files without `GRIDUNIT`** are treated as metres (Eclipse's documented default). No info note, no conversion needed. +- **Unrecognised `GRIDUNIT` values** print an info note saying conversion is not available; the numbers pass through. Asking for `--convert-to-m` on an unrecognised unit is rejected with a clear error. +- **Property units are entirely the modeller's responsibility.** The `GRIDUNIT` keyword only describes the unit of the grid's coordinates. Per-cell properties like `PERMX`, `HEATCR`, `THCONR`, etc. carry their own unit conventions (Eclipse's `METRIC`, `FIELD`, `LAB`, `PVT-M` unit systems each define their own choices for pressure, flow rate, permeability, density, thermal conductivity, etc.). em2ex does not track those conventions and applies no conversion to property values, even when `--convert-to-m` is rescaling the geometry. If your input file is in `FIELD` units (psi, bbl/day, mD, BTU-based thermal quantities, etc.) and you convert the geometry to metres, the property values stay in `FIELD` units; the resulting mesh is internally inconsistent and will need property conversion downstream before it's physically meaningful. + `em2ex` attempts to guess the reservoir model format from the file extension (see supported formats below). If the reservoir model has a non-standard file extension, the user can force `em2ex` to read the correct format using the `--filetype` commandline option. diff --git a/em2ex.py b/em2ex.py index f5958f7..6a548b3 100755 --- a/em2ex.py +++ b/em2ex.py @@ -64,6 +64,8 @@ def get_parser(): help = 'Additional per-cell property keywords to read from the grdecl file (e.g. PVTNUM EQLNUM FIPNUM). Each must be a per-cell scalar of length NX*NY*NZ. Normalised to uppercase. The reader recognises ACTNUM, SATNUM, PORO, PERMX, PERMY, PERMZ, NTG, HEATCR and THCONR by default.') parser.add_argument('--fault-sidesets', dest = 'fault_sidesets', action = 'store_true', help = 'Emit paired sidesets named "fault_primary" and "fault_secondary" containing the faces on either side of every fault (any internal face where adjacent cells do not share their corner nodes).') + parser.add_argument('--convert-to-m', dest = 'convert_to_m', action = 'store_true', + help = 'Convert grid coordinates to metres on output, using the input file\'s GRIDUNIT keyword as the source unit. Supported values are METRES (no-op), FEET and CM. Files without GRIDUNIT are assumed to be in metres.') return parser def main(): diff --git a/readers/eclipse.py b/readers/eclipse.py index b7a50dc..ab219aa 100644 --- a/readers/eclipse.py +++ b/readers/eclipse.py @@ -133,6 +133,14 @@ def processData(line): 'PERMX', 'PERMY', 'PERMZ', 'NTG', 'HEATCR', 'THCONR') +# Recognised length units for the GRIDUNIT keyword and the factor that +# converts them to metres. Files that omit GRIDUNIT default to METRES. +GRIDUNIT_TO_METRES = { + 'METRES': 1.0, + 'FEET': 0.3048, + 'CM': 0.01, +} + def readEclipse(f, eclipse, extra_keywords=()): ''' Read an Eclipse grdecl file and store the data in an Eclipse object. `extra_keywords` is an iterable of additional uppercase keyword names to @@ -253,6 +261,25 @@ def parseEclipse(f, args): # Notify user that parsing has finished print("Finished parsing Eclipse file") + # Determine the grid's length unit from GRIDUNIT (default METRES if absent). + # Print an info note for any recognised non-METRES unit so the user is aware, + # plus a hint about --convert-to-m if they want the output in SI. + grid_unit = (eclipse.gridunit[0].upper() if eclipse.gridunit else 'METRES') + needs_conversion = grid_unit != 'METRES' + unit_known = grid_unit in GRIDUNIT_TO_METRES + if needs_conversion and unit_known: + print("Note: input grid uses {} (from GRIDUNIT keyword). Output mesh will be in {}.".format(grid_unit, grid_unit)) + print(" Pass --convert-to-m to convert to metres on output.") + elif needs_conversion and not unit_known: + print("Note: input grid declares GRIDUNIT {} which is not recognised.".format(grid_unit)) + print(" Conversion to metres is not available; output mesh will use the input values.") + + # Validate the user's --convert-to-m request up front (before any work). + do_convert = bool(getattr(args, 'convert_to_m', False)) and needs_conversion + if getattr(args, 'convert_to_m', False) and needs_conversion and not unit_known: + print("--convert-to-m: unrecognised GRIDUNIT value {!r}; cannot convert.".format(grid_unit)) + exit() + # Now the data can be reshaped and processed for easy use # The COORD data has six entries for each of the (nx+1)*(ny+1) nodes coord = np.asarray(eclipse.coord).reshape(ny+1, nx+1, 6) @@ -262,6 +289,17 @@ def parseEclipse(f, args): # flip_z), all of which leave the (k, j, i) cell indexing unchanged. zcorn = np.asarray(eclipse.zcorn).reshape(2*nz, 2*ny, 2*nx) + # Apply --convert-to-m if requested: rescale every length-valued array by + # the GRIDUNIT->metres factor. coord stores x/y/z for both pillar + # endpoints (all 6 entries are coordinates); zcorn stores z values only. + # Done here, before extract/refine/flip, so the rest of the pipeline + # operates in metres. + if do_convert: + factor = GRIDUNIT_TO_METRES[grid_unit] + coord = coord * factor + zcorn = zcorn * factor + print("Converted {} -> metres on output (factor {}).".format(grid_unit, factor)) + # Apply --extract-i/-j/-k subsetting if requested. Indices are 1-based # inclusive in file order (matching the cells as they appear in the # grdecl SPECGRID / properties sections), and the slice happens before diff --git a/test/eclipse/gold/simple_cube_feet_converted.e b/test/eclipse/gold/simple_cube_feet_converted.e new file mode 100644 index 0000000..eca236a Binary files /dev/null and b/test/eclipse/gold/simple_cube_feet_converted.e differ diff --git a/test/eclipse/simple_cube_feet.grdecl b/test/eclipse/simple_cube_feet.grdecl new file mode 100644 index 0000000..5158fc3 --- /dev/null +++ b/test/eclipse/simple_cube_feet.grdecl @@ -0,0 +1,99 @@ +SPECGRID +3 3 3 1 F / + +GRIDUNIT + FEET / + +COORD + 0.000 0.000 0.000 0.000 0.000 1.000 + 0.500 0.000 0.000 0.500 0.000 1.000 + 1.000 0.000 0.000 1.000 0.000 1.000 + 1.500 0.000 0.000 1.500 0.000 1.000 + 0.000 0.500 0.000 0.000 0.500 1.000 + 0.500 0.500 0.000 0.500 0.500 1.000 + 1.000 0.500 0.000 1.000 0.500 1.000 + 1.500 0.500 0.000 1.500 0.500 1.000 + 0.000 1.000 0.000 0.000 1.000 1.000 + 0.500 1.000 0.000 0.500 1.000 1.000 + 1.000 1.000 0.000 1.000 1.000 1.000 + 1.500 1.000 0.000 1.500 1.000 1.000 + 0.000 1.500 0.000 0.000 1.500 1.000 + 0.500 1.500 0.000 0.500 1.500 1.000 + 1.000 1.500 0.000 1.000 1.500 1.000 + 1.500 1.500 0.000 1.500 1.500 1.000 +/ + +ZCORN +0.000 0.000 0.000 0.000 0.000 0.000 +0.000 0.000 0.000 0.000 0.000 0.000 +0.000 0.000 0.000 0.000 0.000 0.000 +0.000 0.000 0.000 0.000 0.000 0.000 +0.000 0.000 0.000 0.000 0.000 0.000 +0.000 0.000 0.000 0.000 0.000 0.000 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +0.500 0.500 0.500 0.500 0.500 0.500 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.000 1.000 1.000 1.000 1.000 1.000 +1.500 1.500 1.500 1.500 1.500 1.500 +1.500 1.500 1.500 1.500 1.500 1.500 +1.500 1.500 1.500 1.500 1.500 1.500 +1.500 1.500 1.500 1.500 1.500 1.500 +1.500 1.500 1.500 1.500 1.500 1.500 +1.500 1.500 1.500 1.500 1.500 1.500 +/ + +ACTNUM +1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 1 +/ + +PERMX +1 2 3 4 5 6 7 8 9 +1 2 3 4 5 6 7 8 9 +1 2 3 4 5 6 7 8 9 +/ + +PERMY +1 2 3 4 5 6 7 8 9 +1 2 3 4 5 6 7 8 9 +1 2 3 4 5 6 7 8 9 +/ + +PERMZ +1 2 3 4 5 6 7 8 9 +1 2 3 4 5 6 7 8 9 +1 2 3 4 5 6 7 8 9 +/ + +PORO +0.2 0.2 0.4 0.4 0.6 0.6 0.8 0.8 0.5 +0.2 0.2 0.4 0.4 0.6 0.6 0.8 0.8 0.5 +0.2 0.2 0.4 0.4 0.6 0.6 0.8 0.8 0.5 +/ + +SATNUM +1 1 1 1 1 1 1 1 1 +2 2 2 2 2 2 2 2 2 +3 3 3 3 3 3 3 3 3 +/ diff --git a/test/eclipse/tests b/test/eclipse/tests index 72b0c32..0cd4f7a 100644 --- a/test/eclipse/tests +++ b/test/eclipse/tests @@ -126,6 +126,21 @@ faulted_fault_sidesets: cli_args: --fault-sidesets gold: faulted_fault_sidesets.e +# Same numbers as simple_cube.grdecl but with GRIDUNIT FEET. Without +# --convert-to-m, the numbers are passed through unchanged, so the output +# is geometrically identical to simple_cube.e (just nominally in feet). +simple_cube_feet: + filename: simple_cube_feet.grdecl + type: exodiff + gold: simple_cube.e + +# Same FEET input, now converted: every coord multiplied by 0.3048. +simple_cube_feet_converted: + filename: simple_cube_feet.grdecl + type: exodiff + cli_args: --convert-to-m + gold: simple_cube_feet_converted.e + missing_specgrid: filename: missing_specgrid.grdecl type: exception