Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,59 @@ Similarly, the `test/leapfrog` directory contains a set of example Leapfrog rese
```
for example.

## Configuration files

Once a workflow uses more than a handful of options, putting them in a config file is easier than re-typing the same command line. `em2ex` accepts a YAML config via `--config`:

```bash
./em2ex.py --config my_workflow.yaml model.grdecl
```

The config is a YAML mapping. Each key is **the CLI flag name with the leading `--` stripped and hyphens converted to underscores** — exactly what you'd type after `--` in the shell, normalised for YAML. So `--refine-xy` becomes `refine_xy` (or, if you prefer, `refine-xy` — both work). Values follow the option's natural type: a single string or number for single-value flags, a YAML list for multi-value flags, a boolean for switches:

```yaml
# Geometry transforms
extract_i: [10, 50]
extract_j: [10, 50]
refine_xy: [2, 2]

# Property handling
extra_keywords:
- PVTNUM
- EQLNUM

# Output controls
output: model.e # corresponds to --output
force: true # corresponds to --force
fault_sidesets: true
convert_to_m: true

# Mesh quality
strict_jacobians: true
```

The `filename` can be specified in the config too (`filename: path/to/model.grdecl`), in which case the positional argument can be omitted on the CLI.

**Precedence**: command-line flags always win over config values, and config values win over the parser's own defaults. So a config that defaults `force: false` can still be overridden by passing `-f` at the prompt.

**Unknown keys are rejected** with a list of valid keys, so typos like `refine_xz` (when you meant `refine_xy`) surface immediately rather than being silently ignored.

## Commandline options

A number of optional commandline options are available, and can be seen by passing the `--help` flag:
```bash
$ ./em2ex.py --help

usage: em2ex.py [-h] [-o OUTPUT_FILE] [--filetype {eclipse,leapfrog}]
[--no-nodesets] [--no-sidesets] [-f] [-u] [--flip]
usage: em2ex.py [-h] [--config FILE] [-o OUTPUT_FILE]
[--filetype {eclipse,leapfrog}] [--no-nodesets]
[--no-sidesets] [-f] [-u] [--flip]
[--translate TRANSLATE TRANSLATE] [--mapaxes] [--pinch]
[--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] [--convert-to-m]
[--no-check-jacobians] [--strict-jacobians]
filename
[filename]

Converts earth model to Exodus II format

Expand All @@ -105,6 +143,11 @@ positional arguments:

options:
-h, --help show this help message and exit
--config FILE YAML config file specifying default values for any of
this script's options. Values from the config are
overridden by command-line flags. Use the option's
`dest` name as the key (e.g. refine_xy, extract_i,
extra_keywords).
-o OUTPUT_FILE, --output OUTPUT_FILE
File name for output
--filetype {eclipse,leapfrog}
Expand Down
88 changes: 86 additions & 2 deletions em2ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,75 @@
from exodus_model import ExodusModel
import argparse
import os
import sys

def _load_config(path):
''' Load a YAML config file. The file must contain a mapping from option
`dest` names (e.g. `refine_xy`, `extract_i`, `extra_keywords`) to values.
Lists are written as YAML sequences; booleans as `true` / `false`.
Returns the loaded dict. '''
import yaml
if not os.path.exists(path):
sys.exit("Config file not found: {}".format(path))
with open(path) as f:
cfg = yaml.safe_load(f) or {}
if not isinstance(cfg, dict):
sys.exit("Config file must contain a YAML mapping (got {})".format(type(cfg).__name__))
return cfg

def _build_config_key_mapping(parser):
''' Build a mapping from acceptable config-file key names to canonical
argparse `dest` names. The user can write any of:

- the CLI flag name with leading dashes stripped (e.g. `refine-xy`)
- the CLI flag name with hyphens converted to underscores (`refine_xy`)
- the argparse `dest` itself (also `refine_xy` here, but for options like
`--flip` -> `flip_z` or `--force` -> `force_overwrite` the dest is
different from the flag and both forms are accepted)

Short options (e.g. `-f`) are deliberately excluded to keep the config
keys descriptive. Returns (mapping, valid_canonical_keys).
'''
mapping = {}
canonical = []
for action in parser._actions:
if action.dest == 'help':
continue
canonical.append(action.dest)
# The dest itself is always accepted.
mapping[action.dest] = action.dest
# Each long-option flag is also accepted (after stripping `--` and
# normalising hyphens to underscores).
for opt in action.option_strings:
stripped = opt.lstrip('-')
if len(stripped) < 2:
continue # skip short flags like -f / -o / -u
mapping[stripped.replace('-', '_')] = action.dest
return mapping, sorted(canonical)


def _validate_and_normalize_config(cfg, parser):
''' Resolve every config key to its canonical `dest` name. Accepts the
flexible forms documented in _build_config_key_mapping; rejects anything
that doesn't match any option, listing the valid keys.

Returns a new dict keyed by canonical `dest` names. '''
mapping, canonical = _build_config_key_mapping(parser)
normalized = {}
unknown = []
for key, value in cfg.items():
candidate = key.lstrip('-').replace('-', '_')
if candidate in mapping:
normalized[mapping[candidate]] = value
else:
unknown.append(key)
if unknown:
sys.exit(
"Unknown keys in config file: {}\n"
"Valid keys (or any long CLI flag name with leading dashes\n"
"stripped and hyphens converted to underscores): {}".format(
', '.join(unknown), ', '.join(canonical)))
return normalized

def _positive_int(s):
''' argparse type for strictly positive integers (refinement factors and
Expand Down Expand Up @@ -34,7 +103,10 @@ def get_parser():
''' Read commandline options and filename '''

parser = argparse.ArgumentParser(description='Converts earth model to Exodus II format')
parser.add_argument('filename')
parser.add_argument('filename', nargs='?', default=None,
help='Input reservoir model file. Optional only when provided via --config.')
parser.add_argument('--config', dest='config_file', default=None, metavar='FILE',
help='YAML config file specifying default values for any of this script\'s options. Values from the config are overridden by command-line flags. Use the option\'s `dest` name as the key (e.g. refine_xy, extract_i, extra_keywords).')
parser.add_argument('-o', '--output', default = None, dest = 'output_file', help = 'File name for output')
parser.add_argument('--filetype', default = None, dest = 'filetype',
choices = ['eclipse', 'leapfrog'], help = 'Explicitly state the filetype for unknown extensions')
Expand Down Expand Up @@ -75,10 +147,22 @@ def get_parser():
def main():
''' Parse the Earth model and write out an Exodus II file '''

# Parse commandline options
# Parse commandline options. If --config is given, load the YAML file and
# apply its values as parser defaults; command-line flags then override
# those defaults in the usual way (precedence: CLI > config > parser default).
parser = get_parser()
pre_parser = argparse.ArgumentParser(add_help=False)
pre_parser.add_argument('--config', dest='config_file', default=None)
pre_args, _ = pre_parser.parse_known_args()
if pre_args.config_file:
config = _load_config(pre_args.config_file)
config = _validate_and_normalize_config(config, parser)
parser.set_defaults(**config)
args = parser.parse_args()

if not args.filename:
parser.error('filename is required (provide as a positional argument or as `filename: ...` in --config)')

# If --use-official-api is passed, then import exodus from exodus.py. Note:
# this requires that exodus.py is in the $PYTHONPATH environment variable
if args.use_official_api:
Expand Down
4 changes: 4 additions & 0 deletions test/eclipse/config_bad_key.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Deliberately-bad config: `refine_xz` is not a real option (refine_xy was meant).
# Used by the yaml exception test to verify the config loader rejects typos.

refine_xz: [2, 2]
12 changes: 12 additions & 0 deletions test/eclipse/config_extract_refine.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Example em2ex config file. Drives the same conversion as the yaml entry
# `simple_cube_extract_refine` (extract a sub-region and then refine it).
#
# Keys are CLI flag names with the leading `--` stripped (and hyphens may be
# converted to underscores for YAML cleanliness). Sequences map to multi-value
# flags, booleans to store_true/store_false flags. CLI flags override these
# values when both are given.

extract_i: [2, 3]
extract_j: [1, 2]
extract_k: [1, 1]
refine_xy: [2, 2]
17 changes: 17 additions & 0 deletions test/eclipse/tests
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ strict_jacobians_inverted:
cli_args: --strict-jacobians
expected_error: exiting due to invalid Jacobians

# Driven entirely by a YAML config file (extract + refine settings). The
# output must match the same gold as the CLI-driven simple_cube_extract_refine
# test, proving the config loader applies values correctly.
config_extract_refine:
filename: simple_cube_extract.grdecl
type: exodiff
cli_args: --config test/eclipse/config_extract_refine.yaml
gold: simple_cube_extract_refine.e

# Typo in config key name -- the loader should reject it up front with a
# helpful "Unknown keys" message rather than silently ignore it.
config_bad_key:
filename: simple_cube.grdecl
type: exception
cli_args: --config test/eclipse/config_bad_key.yaml
expected_error: "Unknown keys in config file: refine_xz"

missing_specgrid:
filename: missing_specgrid.grdecl
type: exception
Expand Down
Loading