diff --git a/.github/workflows/ci_pycopm_docker.yml b/.github/workflows/ci_pycopm_docker.yml index f2b177e..6cc6fc6 100644 --- a/.github/workflows/ci_pycopm_docker.yml +++ b/.github/workflows/ci_pycopm_docker.yml @@ -1,9 +1,7 @@ name: Docker on: - push: - branches: - - main + pull_request: jobs: run-pycopm-docker: diff --git a/.gitignore b/.gitignore index 625c101..e943ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -165,25 +165,18 @@ cython_debug/ # Python environment vpycopm/ -# DUNE -dune-common/ -dune-geometry/ -dune-grid/ -dune-istl/ - -# The OPM -opm-common/ -opm-grid/ -opm-simulators/ +# OPM +dune-* +opm-* # pycopm -cssr/ -developing/ -examples/configurations/drogon/drogon_coarser -test_outputs/ -playground/ -presentation/ -prototyping/ -project/ -studies/ +/cssr/ +/developing/ +/examples/configurations/drogon/drogon_coarser +/test_outputs/ +/playground/ +/presentation/ +/prototyping/ +/project/ +/studies/ .vscode/ diff --git a/README.md b/README.md index 40b9904..a9d9d62 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,13 @@ To install the _pycopm_ executable from the development version: pip install git+https://github.com/cssr-tools/pycopm.git ``` -If you are interested in a specific version (e.g., v2026.04) or in modifying the source code, then you can clone the repository and install the Python requirements in a virtual environment with the following commands: +If you are interested in modifying the source code, then you can clone the repository and install the Python requirements in a virtual environment with the following commands: ```bash # Clone the repo git clone https://github.com/cssr-tools/pycopm.git # Get inside the folder cd pycopm -# For a specific version (e.g., v2026.04), or skip this step (i.e., edge version) -git checkout v2026.04 # Create virtual environment (to specific Python, python3.13 -m venv vpycopm) python3 -m venv vpycopm # Activate virtual environment diff --git a/dev-requirements.txt b/dev-requirements.txt index e3002b3..30dedcf 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,8 +1,12 @@ -black<=26.5.1 -mypy<=2.3.1 -pylint<=4.0.7 -pytest-cov<=7.1.0 -pytest-xdist<=3.8.0 -ruff<=0.16.4 -sphinx<=9.1.0 -sphinx-rtd-theme<=3.1.0 +black +numpydoc +mypy +pydata_sphinx_theme +pylint +pytest-cov +pytest-xdist +ruff +sphinx +sphinx_copybutton +sphinx_design +sphinx-rtd-theme diff --git a/docs/Makefile b/docs/Makefile index 85bc206..3d6b173 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,25 +1,26 @@ -# Minimal makefile for Sphinx documentation -# +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = text +BUILDDIR = _build +APIDIR = text/api -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SOURCEDIR = text -BUILDDIR = _build +.PHONY: help clean api html linkcheck github -# Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: help Makefile +clean: + rm -rf "$(BUILDDIR)" -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - sphinx-apidoc --private -e -f -o text ../src/pycopm - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +api: + @mkdir -p "$(APIDIR)" + sphinx-apidoc --private -e -f -o "$(APIDIR)" ../src/pycopm -github: - @make html - cp -a _build/html/. . +html: api + @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +linkcheck: + @$(SPHINXBUILD) -M linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +github: html + cp -a "$(BUILDDIR)/html/." . diff --git a/docs/_modules/index.html b/docs/_modules/index.html new file mode 100644 index 0000000..d784467 --- /dev/null +++ b/docs/_modules/index.html @@ -0,0 +1,590 @@ + + + + + + + + + + Overview: module code — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + + + + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/core/pycopm.html b/docs/_modules/pycopm/core/pycopm.html new file mode 100644 index 0000000..053eeff --- /dev/null +++ b/docs/_modules/pycopm/core/pycopm.html @@ -0,0 +1,1467 @@ + + + + + + + + + + pycopm.core.pycopm — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.core.pycopm

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0912,R0914,R0915
+
+"""Command-line entry point and top-level workflow coordination for pycopm.
+
+pycopm supports two input workflows:
+
+* OPM ``.DATA`` decks can be coarsened, refined, transformed, or reduced to a
+  submodel.
+* TOML configurations generate coarsened Norne or Drogon cases and can
+  optionally run OPM Flow or ERT studies.
+
+This module parses and validates command-line arguments, selects the appropriate
+workflow, and coordinates its major processing steps. The numerical and
+file-generation details are implemented in the utility modules.
+"""
+
+import argparse
+import re
+import shlex
+import shutil
+import subprocess
+import time
+from pathlib import Path
+
+from pycopm.utils.coarsening import coarsen_and_write_properties, create_coarsening_map
+from pycopm.utils.files_writer import write_coarsened_model_files
+from pycopm.utils.generate_decks import create_deck
+from pycopm.utils.input_values import create_deck_config, load_toml_config
+from pycopm.utils.runs_executer import generate_postprocessing_plots, run_simulations
+from pycopm.utils.terminal import (
+    cli_correct_value,
+    cli_error_value,
+    cli_info_value,
+    pycopm_error,
+    pycopm_info,
+    pycopm_success,
+)
+
+
+
+[docs] +def main(argv: list[str] | None = None) -> None: + """Run the deck-based or TOML-based pycopm workflow. + + OPM ``.DATA`` decks can be coarsened, refined, transformed, or reduced to + a submodel. TOML configurations generate coarsened Norne or Drogon cases + and can optionally run OPM Flow or ERT studies. + + Parameters + ---------- + argv + Command-line arguments. If omitted, arguments are read from + ``sys.argv``. + + Other Parameters + ---------------- + -i, --input_deck_path + Input ``.DATA`` deck or TOML configuration file. + -o, --output_directory + Directory for generated decks, include files, and simulation results. + -f, --flow_command + Command or path used to run OPM Flow. + -m, --execution_mode + Deck-processing stages to run: ``prep``, ``deck``, ``dry``, + ``prep_deck``, ``deck_dry``, or ``all``. + -v, --vicinity_specification + Submodel selection based on region values, an xy polygon, or a + well-centred ``box``, ``diamond``, or ``diamondxy`` neighbourhood. + -c, --coarsening + Uniform coarsening factors in the x, y, and z directions. + -x, --x_coarsening + Cell-specific coarsening specification along the x axis. + -y, --y_coarsening + Cell-specific coarsening specification along the y axis. + -z, --z_coarsening + Cell-specific coarsening specification along the z axis. + -g, --refinement + Uniform numbers of additional cells along the x, y, and z axes. + -rx, --x_refinement + Number of additional cells for each original x interval. + -ry, --y_refinement + Number of additional cells for each original y interval. + -rz, --z_refinement + Number of additional cells for each original z interval. + -a, --active_cell_methods + Aggregation method for active-cell values: ``min``, ``max``, or + ``mode``. + -n, --discrete_aggregation_method + Aggregation method for discrete properties: ``min``, ``max``, or + ``mode``. + -s, --continuous_aggregation_method + Aggregation method for continuous properties: ``min``, ``max``, + ``mean``, or pore-volume-weighted mean (``pvmean``). If omitted, + property-specific physical aggregation is used. + -p, --pore_volume_correction + Pore-volume correction method. The available values are ``0`` through + ``4``; supported methods depend on the selected workflow. + -q, --correct_fluid_in_place + Set to ``1`` to adjust pore volume to match the initial oil and gas in + place of the input model. + -t, --transmissibility_coarsening_method + Transmissibility coarsening method: ``0``, ``1``, or ``2``. + -r, --completion_removal_level + Level of COMPDAT data removed after coarsening: ``0``, ``1``, or + ``2``. + -j, --jump_thresholds + Positive depth-jump thresholds used to prevent unwanted connections + between cells grouped during coarsening. + -w, --output_deck_name + Name of the generated OPM deck. + -l, --include_prefix + Prefix added to generated include filenames. + -e, --deck_encoding + Character encoding used to read the input deck: ``ISO-8859-1`` or + ``utf8``. + -ijk, --requested_ijk + One-based input-grid ``i,j,k`` indices to map to the modified grid. + -d, --grid_transformation + Coordinate transformation: ``translate [x,y,z]``, ``scale [x,y,z]``, + or ``rotatexy``, ``rotatexz``, or ``rotateyz`` followed by an angle + in degrees. + -explicit, --write_explicit_solution + Set to ``1`` to write initial solution properties explicitly instead + of retaining EQUIL initialization. + -dual, --dual_porosity_criterion + Static-property criterion used to separate matrix and fracture or + non-net cells during coarsening. + -precision, --significant_digits + Number of significant digits used when writing floating-point values. + Set to ``0`` to preserve machine precision. + """ + start_time = time.monotonic() + cmdargs = _parse_arguments(argv) + _check_cmdargs(cmdargs) + output_folder = Path(cmdargs.output_directory).expanduser().resolve() + input_file = cmdargs.input_deck_path + output_folder.mkdir(parents=True, exist_ok=True) + + # Process a DATA deck by coarsening, refining, extracting, or transforming it + if input_file.endswith(".DATA"): + dck = create_deck_config(cmdargs) + create_deck(dck, cmdargs) + return + + # Process a TOML file by generating a coarsened Norne or Drogon project + + # Load the TOML configuration and derive the reference-model settings + resource_directory = str(Path(__file__).resolve().parent.parent) + cfg = load_toml_config( + input_file, + str(output_folder), + resource_directory, + int(cmdargs.significant_digits), + ) + cfg.flow_command = _check_flow(cmdargs.flow_command, cfg.flow_command, input_file) + pycopm_info( + f"generating the input files for {cli_info_value(cfg.model_name)}, " + "please wait..." + ) + + for folder in ["preprocessing", "parameters", "jobs", "observations"]: + (output_folder / folder).mkdir(parents=True, exist_ok=True) + + # Build the coarse grid and map fine cells to coarse cells + coarsening_map = create_coarsening_map(cfg) + + # Aggregate and write the properties for the coarse grid + number_tables = coarsen_and_write_properties(cfg, coarsening_map) + + # Render the Flow, ERT, observation, parameter, and job files + write_coarsened_model_files(cfg, number_tables) + + # Copy the model-specific INCLUDE files required by the generated deck + include_folder = "include" if cfg.model_name == "drogon" else "INCLUDE" + source_include = ( + Path(cfg.resource_directory) + / "reference_simulation" + / cfg.model_name + / include_folder + ) + destination_include = output_folder / "preprocessing" / include_folder + shutil.copytree(source_include, destination_include, dirs_exist_ok=True) + pycopm_success("input files required by ERT written to ", cfg.output_directory, []) + if cfg.execution_mode in ["single-run", "ert"]: + + pycopm_info("running ERT, please wait...") + # Run OPM Flow or the selected ERT workflow + run_simulations(cfg) + + # Generate the postprocessing plots after the simulations + generate_postprocessing_plots(cfg, time.monotonic() - start_time, number_tables)
+ + + +
+[docs] +def _parse_arguments(argv: list[str] | None = None) -> argparse.Namespace: + """Parse supported command-line arguments. + + Unknown arguments are left unprocessed for compatibility with external + launchers. + + Parameters + ---------- + argv + Command-line arguments. If omitted, arguments are read from ``sys.argv``. + + Returns + ------- + dict[str, str] + Arguments keyed by their destination names.""" + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Tailor a geological model and optionally run simulations " + "using OPM Flow. All options can be used with DATA decks, while only " + "-i, -o, -f, and -precision apply to TOML configuration files. See the " + "online documentation for examples and detailed option descriptions: " + "https://cssr-tools.github.io/pycopm/introduction.html#overview", + ) + parser.add_argument( + "-i", + "--input_deck_path", + type=str.strip, + default="input.toml", + help="The base name of the TOML configuration file or the name of the deck", + ) + parser.add_argument( + "-o", + "--output_directory", + type=str.strip, + default=".", + help="The base name of the output folder", + ) + parser.add_argument( + "-f", + "--flow_command", + type=str.strip, + default="flow", + help="Set path to flow executable", + ) + parser.add_argument( + "-m", + "--execution_mode", + type=str.strip, + choices=["prep", "deck", "dry", "prep_deck", "deck_dry", "all"], + default="prep_deck", + help="Parts of pycopm to run", + ) + parser.add_argument( + "-v", + "--vicinity_specification", + type=str.strip, + default="", + help="The location to extract the sub model which can be assigned by " + "region values (e.g., 'fipnum 2,4'), by a polygon given the xy locations " + "in meters (e.g., 'xypolygon [0,0] [30,0] [30,30] [0,0]'), or by the name " + "of the well and three different options for the neighbourhood: box, " + "diamond, and diamondxy (e.g., 'welln box [-1,1] [-2,2] [0,3]')", + ) + parser.add_argument( + "-c", + "--coarsening", + type=str.strip, + default="", + help="Level of coarsening in the x, y, and z dir", + ) + parser.add_argument( + "-x", + "--x_coarsening", + type=str.strip, + default="", + help="Array of x coarsening", + ) + parser.add_argument( + "-y", + "--y_coarsening", + type=str.strip, + default="", + help="Array of y coarsening", + ) + parser.add_argument( + "-z", + "--z_coarsening", + type=str.strip, + default="", + help="Array of z coarsening", + ) + parser.add_argument( + "-g", + "--refinement", + type=str.strip, + default="", + help="Level of grid refinement in the x, y, and z dir", + ) + parser.add_argument( + "-rx", + "--x_refinement", + type=str.strip, + default="", + help="Array of x refinement", + ) + parser.add_argument( + "-ry", + "--y_refinement", + type=str.strip, + default="", + help="Array of y refinement", + ) + parser.add_argument( + "-rz", + "--z_refinement", + type=str.strip, + default="", + help="Array of z refinement", + ) + parser.add_argument( + "-a", + "--active_cell_methods", + type=str.strip, + default="mode", + help="Select aggregation method (min, max, or mode) for the active " + "cells (for coarsening in the z direction separate by commas to " + "specify an approach per layer)", + ) + parser.add_argument( + "-n", + "--discrete_aggregation_method", + type=str.strip, + default="mode", + help="Select aggregation method for the discrete variables (min, max, mode)", + ) + parser.add_argument( + "-s", + "--continuous_aggregation_method", + type=str.strip, + default="", + help="Select aggregation method for the continuous variables (min, max, mean, " + "pvmean; by default these are property/direction dependent, e.g., harmonic " + "average for permeability", + ) + parser.add_argument( + "-p", + "--pore_volume_correction", + type=str.strip, + choices=["0", "1", "2", "3", "4"], + default="0", + help="Select pore volume correction approach", + ) + parser.add_argument( + "-q", + "--correct_fluid_in_place", + type=str.strip, + choices=["0", "1"], + default="0", + help="Adjust the pv to the initial FGIP and FOIP", + ) + parser.add_argument( + "-t", + "--transmissibility_coarsening_method", + type=str.strip, + choices=["0", "1", "2"], + default="0", + help="Select coarsening method for transmissibilities", + ) + parser.add_argument( + "-r", + "--completion_removal_level", + type=str.strip, + choices=["0", "1", "2"], + default="2", + help="Select COMPDAT entries to remove after coarsening", + ) + parser.add_argument( + "-j", + "--jump_thresholds", + type=str.strip, + default="", + help="Parameter to avoid creation of neighbouring connections after coarsening", + ) + parser.add_argument( + "-w", + "--output_deck_name", + type=str.strip, + default="", + help="Name of the generated deck", + ) + parser.add_argument( + "-l", + "--include_prefix", + type=str.strip, + default="PYCOPM_", + help="Added text before each generated .INC", + ) + parser.add_argument( + "-e", + "--deck_encoding", + type=str.strip, + choices=["ISO-8859-1", "utf8"], + default="ISO-8859-1", + help="Encoding to read the deck", + ) + parser.add_argument( + "-ijk", + "--requested_ijk", + type=str.strip, + default="", + help="Returns the modified indices given as entry the 'i,j,k' indices", + ) + parser.add_argument( + "-d", + "--grid_transformation", + type=str.strip, + default="", + help="Select transformation method (e.g, 'translate [10,-5,4]', " + "'scale [1,2,3]', or 'rotatexy 45')", + ) + parser.add_argument( + "-explicit", + "--write_explicit_solution", + type=str.strip, + choices=["0", "1"], + default="0", + help="Set to 1 to explicitly write the cell values in the SOLUTION section", + ) + parser.add_argument( + "-dual", + "--dual_porosity_criterion", + type=str.strip, + default="", + help="Set the criterium to differentiate net and non-net in coarsening using a static " + "variable. To remove the vertical transfer function (FT) between net and not-net cells, " + "add to the command ', vertical TF = 0', e.g., 'poro <= 0.1' (which includes vertical TF) " + "or 'poro <= 0.1, vertical TF = 0'", + ) + parser.add_argument( + "-precision", + "--significant_digits", + type=str.strip, + choices=[str(value) for value in range(16)], + default="7", + help="Set the number of significant digits used when writing floating-point values, or 0 " + "to use machine precision", + ) + return parser.parse_args(argv)
+ + + +
+[docs] +def _check_cmdargs(cmdargs: argparse.Namespace) -> None: + """Validate command-line arguments and incompatible operations. + + The checks cover input type, Flow availability, coarsening and refinement + syntax, aggregation methods, vicinity selections, transformations, and + options restricted to particular workflows. + + Parameters + ---------- + cmdargs + Parsed arguments returned by :func:`_parse_arguments`. + + Raises + ------ + SystemExit + If an argument is invalid or an incompatible combination is requested.""" + input_file = cmdargs.input_deck_path + # Select the workflow from the input filename extension + if not input_file.endswith((".DATA", ".toml")): + pycopm_error( + f"invalid extension {cli_error_value(f'-i {input_file}')}, valid extensions " + f"are {cli_correct_value('.DATA')} or {cli_correct_value('.toml')}." + ) + if not cmdargs.output_directory: + pycopm_error( + f"invalid value {cli_error_value('-o')}, the output folder cannot be empty." + ) + # Only -i, -o, -f, and -precision apply to TOML configuration files + if input_file.endswith(".toml"): + data_options = { + "-m": ("execution_mode", "prep_deck"), + "-v": ("vicinity_specification", ""), + "-c": ("coarsening", ""), + "-x": ("x_coarsening", ""), + "-y": ("y_coarsening", ""), + "-z": ("z_coarsening", ""), + "-g": ("refinement", ""), + "-rx": ("x_refinement", ""), + "-ry": ("y_refinement", ""), + "-rz": ("z_refinement", ""), + "-a": ("active_cell_methods", "mode"), + "-n": ("discrete_aggregation_method", "mode"), + "-s": ("continuous_aggregation_method", ""), + "-p": ("pore_volume_correction", "0"), + "-q": ("correct_fluid_in_place", "0"), + "-t": ("transmissibility_coarsening_method", "0"), + "-r": ("completion_removal_level", "2"), + "-j": ("jump_thresholds", ""), + "-w": ("output_deck_name", ""), + "-l": ("include_prefix", "PYCOPM_"), + "-e": ("deck_encoding", "ISO-8859-1"), + "-ijk": ("requested_ijk", ""), + "-d": ("grid_transformation", ""), + "-explicit": ("write_explicit_solution", "0"), + "-dual": ("dual_porosity_criterion", ""), + } + invalid_options = [ + option + for option, (name, default) in data_options.items() + if getattr(cmdargs, name) != default + ] + if invalid_options: + pycopm_error( + "invalid option for a TOML configuration file; only '-i', '-o', " + "'-f', and '-precision' can be used. Invalid options: " + f"{', '.join(invalid_options)}." + ) + return + # Verify the complete Flow command, including any launcher and arguments + try: + flow_arguments = shlex.split(cmdargs.flow_command) + except ValueError: + flow_arguments = [] + if not flow_arguments: + pycopm_error( + f"invalid OPM Flow command {cli_error_value(f'-f {cmdargs.flow_command}')}." + ) + try: + flow_result = subprocess.run( + [*flow_arguments, "-h"], + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + check=False, + ) + except OSError: + flow_result = None + if flow_result is None or flow_result.returncode != 0: + pycopm_error( + f"the OPM Flow executable '-f {cmdargs.flow_command}' " + "is not available or not working." + ) + coarsening = cmdargs.coarsening + x_coarsening = cmdargs.x_coarsening + y_coarsening = cmdargs.y_coarsening + z_coarsening = cmdargs.z_coarsening + refinement = cmdargs.refinement + x_refinement = cmdargs.x_refinement + y_refinement = cmdargs.y_refinement + z_refinement = cmdargs.z_refinement + vicinity = cmdargs.vicinity_specification + transformation = cmdargs.grid_transformation + directional_coarsening = any([x_coarsening, y_coarsening, z_coarsening]) + directional_refinement = any([x_refinement, y_refinement, z_refinement]) + has_coarsening = bool(coarsening or directional_coarsening) + has_refinement = bool(refinement or directional_refinement) + # General and directional coarsening options are mutually exclusive + if coarsening and directional_coarsening: + pycopm_error( + "invalid combination, either set '-c' or the '-x', '-y', and '-z' flags." + ) + # General and directional refinement options are mutually exclusive + if refinement and directional_refinement: + pycopm_error( + "invalid combination, either set '-g' or the '-rx', '-ry', and " + "'-rz' flags." + ) + # Coarsening and refinement are mutually exclusive + if has_coarsening and has_refinement: + pycopm_error( + "invalid combination, either set coarsening or refinement options." + ) + # Vicinity extraction, transformation, and refinement are mutually exclusive + if vicinity and transformation: + pycopm_error("invalid combination, either set '-v' or '-d'.") + if vicinity and has_refinement: + pycopm_error("invalid combination, either set '-v' or refinement options.") + if transformation and has_refinement: + pycopm_error("invalid combination, either set '-d' or refinement options.") + # Validate uniform coarsening and refinement levels + level_pattern = re.compile(r"\d+,\d+,\d+") + if coarsening and not level_pattern.fullmatch(coarsening): + pycopm_error( + f"invalid value {cli_error_value(f'-c {coarsening}')}, expected three non-negative " + f"integers separated by commas, {cli_correct_value('e.g., -c 2,2,1')}." + ) + if refinement and not level_pattern.fullmatch(refinement): + pycopm_error( + f"invalid value {cli_error_value(f'-g {refinement}')}, expected three non-negative " + f"integers separated by commas, {cli_correct_value('e.g., -g 2,2,1')}." + ) + # Validate directional coarsening arrays, indices, and ranges + coarsening_array_pattern = re.compile(r"\d+(?:,\d+)*") + coarsening_group_pattern = re.compile( + r"[1-9]\d*(?::[1-9]\d*)?(?:,[1-9]\d*(?::[1-9]\d*)?)*" + ) + for option, value in [ + ("-x", x_coarsening), + ("-y", y_coarsening), + ("-z", z_coarsening), + ]: + if value and not ( + coarsening_array_pattern.fullmatch(value) + or coarsening_group_pattern.fullmatch(value) + ): + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, expected a non-negative " + "coarsening array or positive indices and ranges separated by " + "commas." + ) + if ":" in value: + for entry in value.split(","): + if ":" not in entry: + continue + start, end = (int(index) for index in entry.split(":")) + if start > end: + pycopm_error( + f"invalid range '{entry}' in '{option} {value}', " + "the end must not be smaller than the start." + ) + # Validate directional refinement arrays + refinement_array_pattern = re.compile(r"\d+(?:,\d+)*") + for option, value in [ + ("-rx", x_refinement), + ("-ry", y_refinement), + ("-rz", z_refinement), + ]: + if value and not refinement_array_pattern.fullmatch(value): + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, expected non-negative " + "integers separated by commas." + ) + # Validate aggregation methods + aggregation_options = [ + ("-a", "active_cell_methods", ["min", "max", "mode"]), + ("-n", "discrete_aggregation_method", ["min", "max", "mode"]), + ( + "-s", + "continuous_aggregation_method", + ["min", "max", "mean", "pvmean"], + ), + ] + z_groups = z_coarsening.split(",") if ":" in z_coarsening else [] + for option, name, valid_methods in aggregation_options: + value = getattr(cmdargs, name).strip() + methods = value.split(",") if value else [] + if any(method not in valid_methods for method in methods): + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, valid values are " + f"{cli_correct_value(', '.join(valid_methods))}." + ) + if len(methods) > 1 and not z_groups: + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, multiple aggregation " + "methods require range coarsening with '-z'." + ) + if len(methods) > 1 and len(methods) != len(z_groups): + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, expected one aggregation " + "method for each index or range provided with '-z'." + ) + # Options controlling property aggregation require coarsening + if not has_coarsening: + if cmdargs.active_cell_methods != "mode": + pycopm_error( + f"invalid combination, {cli_error_value('-a')} can only be used with coarsening." + ) + if cmdargs.discrete_aggregation_method != "mode": + pycopm_error( + f"invalid combination, {cli_error_value('-n')} can only be used with coarsening." + ) + if cmdargs.continuous_aggregation_method: + pycopm_error( + f"invalid combination, {cli_error_value('-s')} can only be used with coarsening." + ) + if cmdargs.transmissibility_coarsening_method != "0": + pycopm_error( + f"invalid combination, {cli_error_value('-t')} can only be used with coarsening." + ) + if cmdargs.jump_thresholds: + pycopm_error( + f"invalid combination, {cli_error_value('-j')} can only be used with coarsening." + ) + if cmdargs.dual_porosity_criterion: + pycopm_error( + f"invalid combination, {cli_error_value('-dual')} can only be used with coarsening." + ) + # Fluid-in-place correction is not supported for extracted submodels + if vicinity and cmdargs.correct_fluid_in_place == "1": + pycopm_error( + f"invalid combination, {cli_error_value('-q')} cannot be used " + "with {cli_error_value('-v')}." + ) + # Validate pore-volume correction combinations + pore_volume_correction = cmdargs.pore_volume_correction + if pore_volume_correction == "1" and not (has_coarsening or vicinity): + pycopm_error( + f"invalid combination, {cli_error_value('-p 1')} requires coarsening or " + f"{cli_correct_value('-v')}." + ) + if pore_volume_correction in ["2", "3", "4"] and not vicinity: + pycopm_error( + f"invalid combination, {cli_error_value(f'-p {pore_volume_correction}')} can only be " + "used with '-v'." + ) + # Validate the jump thresholds + jump_thresholds = cmdargs.jump_thresholds + if jump_thresholds: + try: + jump_values = [float(value.strip()) for value in jump_thresholds.split(",")] + except ValueError: + jump_values = [] + if not jump_values or any(value <= 0 for value in jump_values): + pycopm_error( + f"invalid value {cli_error_value(f'-j {jump_thresholds}')}, expected positive " + "numbers separated by commas." + ) + # Validate requested input-model indices + requested_ijk = cmdargs.requested_ijk + if requested_ijk and not re.fullmatch( + r"[1-9]\d*\s*,\s*[1-9]\d*\s*,\s*[1-9]\d*", + requested_ijk, + ): + pycopm_error( + f"invalid value {cli_error_value(f'-ijk {requested_ijk}')}, expected three positive " + f"indices separated by commas, {cli_correct_value('e.g., -ijk 1,2,3')}." + ) + # Validate coordinate transformations + number = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?" + vector_transformation = re.fullmatch( + rf"(translate|scale)\s+\[\s*{number}\s*,\s*{number}\s*,\s*" rf"{number}\s*\]", + transformation, + ) + rotation_transformation = re.fullmatch( + rf"(rotatexy|rotatexz|rotateyz)\s+{number}", + transformation, + ) + if transformation and not (vector_transformation or rotation_transformation): + pycopm_error( + f"invalid value {cli_error_value(f'-d {transformation}')}, expected " + "'translate [x,y,z]', 'scale [x,y,z]', or 'rotatexy', 'rotatexz', " + "or 'rotateyz' followed by an angle." + ) + if vector_transformation and vector_transformation.group(1) == "scale": + coordinates = re.findall(number, transformation) + if any(float(value) == 0 for value in coordinates): + pycopm_error( + f"invalid value {cli_error_value(f'-d {transformation}')}, scale values cannot be " + "zero." + ) + # Validate vicinity extraction specifications + region_vicinity = re.fullmatch( + r"[A-Za-z][A-Za-z0-9_]*\s+[1-9]\d*(?:\s*,\s*[1-9]\d*)*", + vicinity, + ) + polygon_point = rf"\[\s*{number}\s*,\s*{number}\s*\]" + polygon_vicinity = re.fullmatch( + rf"xypolygon(?:\s+{polygon_point}){{4,}}", + vicinity, + ) + box_vicinity = re.fullmatch( + r"\S+\s+box(?:\s+\[\s*-?\d+\s*,\s*-?\d+\s*\]){3}", + vicinity, + ) + diamond_vicinity = re.fullmatch( + r"\S+\s+(?:diamond|diamondxy)\s+\d+", + vicinity, + ) + if vicinity and not ( + region_vicinity or polygon_vicinity or box_vicinity or diamond_vicinity + ): + pycopm_error( + f"invalid value {cli_error_value(f'-v {vicinity}')}, expected a region selection, an " + "'xypolygon' specification, or a well followed by 'box', " + "'diamond', or 'diamondxy'." + ) + if polygon_vicinity: + polygon_points = re.findall(polygon_point, vicinity) + first_point = re.findall(number, polygon_points[0]) + last_point = re.findall(number, polygon_points[-1]) + if first_point != last_point: + pycopm_error( + f"invalid value {cli_error_value(f'-v {vicinity}')}, the first and last " + "xypolygon points must be equal." + ) + if box_vicinity: + intervals = re.findall( + r"\[\s*(-?\d+)\s*,\s*(-?\d+)\s*\]", + vicinity, + ) + if any(int(start) > int(end) for start, end in intervals): + pycopm_error( + f"invalid value {cli_error_value(f'-v {vicinity}')}, the end of each box interval " + "must not be smaller than its start." + ) + # Validate the dual-porosity criterion + dual_porosity_criterion = cmdargs.dual_porosity_criterion + dual_criterion_pattern = re.compile( + rf"[A-Za-z][A-Za-z0-9_]*\s*(?:<=|>=|==|!=|<|>)\s*{number}" + r"(?:\s*,\s*vertical\s+TF\s*=\s*0)?", + re.IGNORECASE, + ) + if dual_porosity_criterion and not dual_criterion_pattern.fullmatch( + dual_porosity_criterion + ): + pycopm_error( + f"invalid value {cli_error_value(f'-dual {dual_porosity_criterion}')}, expected a " + "static property criterion such as 'poro <= 0.1', optionally " + "followed by ', vertical TF = 0'." + )
+ + + +
+[docs] +def _check_flow(flow_cmdargs: str, flow_toml: str, input_file: str) -> str: + """Select an available OPM Flow command for a TOML workflow. + + Parameters + ---------- + flow_cmdargs + Flow command supplied through the command line. + flow_toml + Flow command read from the TOML configuration. + input_file + TOML filename used in validation messages. + + Returns + ------- + str + The selected Flow command. + + Raises + ------ + SystemExit + If neither command identifies a working Flow executable.""" + flowpth = str( + next((value for value in shlex.split(flow_toml) if "flow" in value), False) + ) + if not flowpth: + pycopm_error( + f"flow is not included in the configuration file {cli_error_value(input_file)}. " + "see the pycopm documentation." + ) + + toml_command = shlex.split(flowpth) + ["-h"] + flag_command = shlex.split(flow_cmdargs) + ["-h"] + + def flow_exists(command: list[str]) -> bool: + try: + return ( + subprocess.run( + command, + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + check=False, + ).returncode + == 0 + ) + except OSError: + return False + + toml_ok = flow_exists(toml_command) + flag_ok = flow_exists(flag_command) + + if not (toml_ok or flag_ok): + pycopm_error( + f"the OPM Flow executable '{flowpth}' is not found; " + "try to install it following the pycopm documentation. If it was " + "built from source, then either add the folder location to your path, " + "or write the path to flow in the TOML configuration file " + "(e.g., flow = '/home/pycopm/build/opm-simulators/bin/flow'), " + "or using the command flag -f or --flow." + ) + if toml_ok: + flow_command = flow_toml + else: + command_parts = shlex.split(flow_cmdargs) + for index, value in enumerate(command_parts): + if "flow" in value: + command_parts[index] = flow_cmdargs + break + flow_command = " ".join(command_parts) + return flow_command
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/coarsening.html b/docs/_modules/pycopm/utils/coarsening.html new file mode 100644 index 0000000..e1869ac --- /dev/null +++ b/docs/_modules/pycopm/utils/coarsening.html @@ -0,0 +1,3190 @@ + + + + + + + + + + pycopm.utils.coarsening — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.coarsening

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0902,R0912,R0913,R0914,R0915,C0302,R0917,R1702,R0916,R0911,E1102
+
+"""Coarsen corner-point grids and aggregate reservoir properties.
+
+The module supports deck-based coarsening and the TOML workflows used to
+generate reduced Norne and Drogon models."""
+
+import argparse
+import csv
+import re
+import sys
+from contextlib import nullcontext
+from dataclasses import dataclass, field
+from pathlib import Path
+
+import numpy as np
+from alive_progress import alive_bar
+from numpy.typing import NDArray
+from opm.io.ecl import EclFile as OpmFile
+from opm.io.ecl import EGrid as OpmGrid
+from opm.io.ecl import ERst as OpmRestart
+
+from pycopm.config.config import ConfigViaDeck, ConfigViaTOML
+from pycopm.utils.files_writer import (
+    _render_template,
+    format_opm_compact_values,
+    round_like_e,
+    write_compact_property_file,
+    write_grid,
+    write_include,
+    write_property,
+    write_property_inc,
+    write_reference_to_coarse_map,
+)
+from pycopm.utils.input_values import parse_axis_modifications
+from pycopm.utils.terminal import pycopm_error, pycopm_info
+
+
+
+[docs] +@dataclass(slots=True) +class CoarseningMaps: + """Store mappings and intermediate values used during coarsening.""" + + #: Axis array marking boundaries removed by coarsening in the x direction. + #: Values greater than one identify intervals merged with the preceding + #: interval. + x: NDArray + + #: Axis array marking boundaries removed by coarsening in the y direction. + #: Values greater than one identify intervals merged with the preceding + #: interval. + y: NDArray + + #: Axis array marking boundaries removed by coarsening in the z direction. + #: Values greater than one identify intervals merged with the preceding + #: interval. + z: NDArray + + #: One-based coarse-cell identifier for each original cell, flattened in + #: ``(z, y, x)`` order. + cell_groups: NDArray + + #: Concatenated names of the coarsened axes, for example ``"xz"``. + coarsened_axes: str + + #: Per-cell mask separating matrix cells (one) from fracture or non-net + #: cells (zero) in dual-porosity models. + matrix_mask: NDArray + + #: Whether vertical matrix-fracture transfer connections are retained. + vertical_transfer_enabled: bool + + #: Coarse-cell identifier for each reference-grid cell, populated while + #: properties are coarsened. + reference_to_coarse: list[int] = field(default_factory=list) + + #: NNC include-file content accumulated while mapping non-neighbouring and + #: matrix-fracture connections. + nnc_text: str = "NNC\n" + + #: Horizontal x-direction transmissibilities for the matrix or + #: single-porosity coarse grid. + coarse_tranx: NDArray = field(default_factory=lambda: np.array([])) + + #: Horizontal y-direction transmissibilities for the matrix or + #: single-porosity coarse grid. + coarse_trany: NDArray = field(default_factory=lambda: np.array([])) + + #: Horizontal x-direction transmissibilities for the fracture continuum of + #: a dual-porosity grid. + dual_tranx: NDArray = field(default_factory=lambda: np.array([])) + + #: Horizontal y-direction transmissibilities for the fracture continuum of + #: a dual-porosity grid. + dual_trany: NDArray = field(default_factory=lambda: np.array([])) + + #: Default property values inserted into separator rows of the extended + #: dual-porosity grid. + dual_defaults: dict[str, float] = field(default_factory=dict)
+ + + +
+[docs] +def create_coarsening_maps( + dck: ConfigViaDeck, cmdargs: argparse.Namespace +) -> CoarseningMaps: + """Create axis mappings and assign original cells to coarse cells. + + Parameters + ---------- + dck + Deck configuration whose output dimensions are updated. + cmdargs + Command arguments containing ``coarsening``, ``x_coarsening``, + ``y_coarsening``, and ``z_coarsening``. + + Returns + ------- + CoarseningMaps + Axis mappings, cell groups, and dual-porosity masks.""" + cijk, refs = parse_axis_modifications( + cmdargs.coarsening, + [ + cmdargs.x_coarsening, + cmdargs.y_coarsening, + cmdargs.z_coarsening, + ], + ) + matrix_mask = np.ones(dck.original_porv.size) + vertical_transfer_enabled = True + if dck.dual_porosity_criterion: + dual_criterion = str(dck.dual_porosity_criterion) + criterion_parts = dual_criterion.split() + vertical_transfer_enabled = "vertical TF = 0" not in dual_criterion + property_name = criterion_parts[0].upper() + comparison_operator = criterion_parts[1] + comparison_value = float(criterion_parts[2].rstrip(",")) + property_values = np.asarray(dck.init_file[property_name]) + active_cells = dck.original_porv > 0 + if comparison_operator == "==": + matrix_mask[active_cells] = property_values != comparison_value + elif comparison_operator == ">=": + matrix_mask[active_cells] = property_values < comparison_value + elif comparison_operator == "<=": + matrix_mask[active_cells] = property_values > comparison_value + elif comparison_operator == "<": + matrix_mask[active_cells] = property_values >= comparison_value + elif comparison_operator == ">": + matrix_mask[active_cells] = property_values <= comparison_value + elif comparison_operator == "!=": + matrix_mask[active_cells] = property_values == comparison_value + else: + pycopm_error(f"unknown criterion for non-net cells: {dual_criterion}") + directions = ("x", "y", "z") + original_sizes = ( + dck.original_nx, + dck.original_ny, + dck.original_nz, + ) + if len(cijk) > 2: + coarsened_axes = "".join( + direction + for direction, coarsening_factor in zip(directions, cijk) + if coarsening_factor > 1 + ) + else: + coarsened_axes = "".join( + direction + for direction_index, direction in enumerate(directions) + if len(refs[direction_index]) > 0 + ) + coarsenings = [] + for direction_index, original_size in enumerate(original_sizes): + coarsening_values = np.zeros(original_size + 1, dtype=int) + if len(cijk) > 2: + coarsening_values.fill(2) + coarsening_values[: original_size : cijk[direction_index]] = 0 + coarsening_values[-1] = 0 + elif len(refs[direction_index]) > 0: + configured_values = np.asarray( + refs[direction_index], + dtype=int, + ) + coarsening_values[: configured_values.size] = configured_values + coarsenings.append(coarsening_values) + coarse_coordinates = [] + for coarsening_values, original_size in zip( + coarsenings, + original_sizes, + ): + coarse_coordinates.append( + np.concatenate( + ( + np.zeros(1, dtype=np.intp), + np.cumsum( + coarsening_values[1:original_size] <= 1, + dtype=np.intp, + ), + ) + ) + ) + coarse_i = coarse_coordinates[0] + coarse_j = coarse_coordinates[1] + coarse_k = coarse_coordinates[2] + coarse_nx = int(coarse_i[-1]) + 1 + coarse_ny = int(coarse_j[-1]) + 1 + cell_groups = ( + coarse_i[None, None, :] + + coarse_j[None, :, None] * coarse_nx + + coarse_k[:, None, None] * coarse_nx * coarse_ny + + 1 + ).reshape(-1) + for direction, coarsening_values, original_size in zip( + directions, + coarsenings, + original_sizes, + ): + setattr( + dck, + f"output_n{direction}", + original_size - int(np.count_nonzero(coarsening_values == 2)), + ) + return CoarseningMaps( + x=coarsenings[0], + y=coarsenings[1], + z=coarsenings[2], + cell_groups=cell_groups, + coarsened_axes=coarsened_axes, + matrix_mask=matrix_mask, + vertical_transfer_enabled=vertical_transfer_enabled, + )
+ + + +
+[docs] +def _grouped_sum( + values: NDArray, + groups: NDArray, + size: int | None = None, +) -> NDArray: + """Return the sum of values for each one-based group.""" + numeric_values = np.asarray(values, dtype=float) + group_indices = np.asarray(groups, dtype=int) + number_groups = int(group_indices.max()) if size is None else size + valid_values = ~np.isnan(numeric_values) + return np.bincount( + group_indices[valid_values] - 1, + weights=numeric_values[valid_values], + minlength=number_groups, + )
+ + + +
+[docs] +def _grouped_count( + values: NDArray, + groups: NDArray, + size: int | None = None, +) -> NDArray: + """Return the number of non-NaN values for each one-based group.""" + numeric_values = np.asarray(values, dtype=float) + group_indices = np.asarray(groups, dtype=int) + number_groups = int(group_indices.max()) if size is None else size + valid_values = ~np.isnan(numeric_values) + return np.bincount( + group_indices[valid_values] - 1, + minlength=number_groups, + )
+ + + +
+[docs] +def _grouped_min( + values: NDArray, + groups: NDArray, + size: int | None = None, +) -> NDArray: + """Return the minimum value for each one-based group, ignoring NaNs.""" + numeric_values = np.asarray(values, dtype=float) + group_indices = np.asarray(groups, dtype=int) + number_groups = int(group_indices.max()) if size is None else size + result = np.full(number_groups, np.inf) + valid_values = ~np.isnan(numeric_values) + np.minimum.at( + result, + group_indices[valid_values] - 1, + numeric_values[valid_values], + ) + result[np.isinf(result)] = np.nan + return result
+ + + +
+[docs] +def _grouped_max( + values: NDArray, + groups: NDArray, + size: int | None = None, +) -> NDArray: + """Return the maximum value for each one-based group, ignoring NaNs.""" + numeric_values = np.asarray(values, dtype=float) + group_indices = np.asarray(groups, dtype=int) + number_groups = int(group_indices.max()) if size is None else size + result = np.full(number_groups, -np.inf) + valid_values = ~np.isnan(numeric_values) + np.maximum.at( + result, + group_indices[valid_values] - 1, + numeric_values[valid_values], + ) + result[np.isneginf(result)] = np.nan + return result
+ + + +
+[docs] +def _grouped_mean( + values: NDArray, + groups: NDArray, + size: int | None = None, +) -> NDArray: + """Return the mean value for each one-based group, ignoring NaNs.""" + sums = _grouped_sum(values, groups, size) + counts = _grouped_count(values, groups, size) + return np.divide( + sums, + counts, + out=np.full(sums.shape, np.nan), + where=counts > 0, + )
+ + + +
+[docs] +def _grouped_first( + values: NDArray, + groups: NDArray, + size: int | None = None, +) -> NDArray: + """Return the first non-NaN value for each one-based group.""" + numeric_values = np.asarray(values, dtype=float) + group_indices = np.asarray(groups, dtype=int) + number_groups = int(group_indices.max()) if size is None else size + source_indices = np.arange(numeric_values.size) + first_indices = np.full(number_groups, numeric_values.size, dtype=int) + valid_values = ~np.isnan(numeric_values) + np.minimum.at( + first_indices, + group_indices[valid_values] - 1, + source_indices[valid_values], + ) + result = np.full(number_groups, np.nan) + valid_groups = first_indices < numeric_values.size + result[valid_groups] = numeric_values[first_indices[valid_groups]] + return result
+ + + +
+[docs] +def _grouped_last( + values: NDArray, + groups: NDArray, + size: int | None = None, +) -> NDArray: + """Return the last non-NaN value for each one-based group.""" + numeric_values = np.asarray(values, dtype=float) + group_indices = np.asarray(groups, dtype=int) + number_groups = int(group_indices.max()) if size is None else size + source_indices = np.arange(numeric_values.size) + last_indices = np.full(number_groups, -1, dtype=int) + valid_values = ~np.isnan(numeric_values) + np.maximum.at( + last_indices, + group_indices[valid_values] - 1, + source_indices[valid_values], + ) + result = np.full(number_groups, np.nan) + valid_groups = last_indices >= 0 + result[valid_groups] = numeric_values[last_indices[valid_groups]] + return result
+ + + +
+[docs] +def _grouped_mode( + values: NDArray, + group_codes: NDArray, + number_groups: int, +) -> NDArray: + """Return the smallest mode for each zero-based group, ignoring NaNs.""" + values = np.asarray(values, dtype=np.float64) + group_codes = np.asarray(group_codes) + valid = ~np.isnan(values) + if not np.any(valid): + return np.full(number_groups, np.nan, dtype=np.float64) + valid_groups = group_codes[valid] + valid_values = values[valid] + order = np.lexsort((valid_values, valid_groups)) + sorted_groups = valid_groups[order] + sorted_values = valid_values[order] + pair_start = np.empty(sorted_values.size, dtype=bool) + pair_start[0] = True + pair_start[1:] = (sorted_groups[1:] != sorted_groups[:-1]) | ( + sorted_values[1:] != sorted_values[:-1] + ) + pair_indices = np.flatnonzero(pair_start) + pair_groups = sorted_groups[pair_indices] + pair_values = sorted_values[pair_indices] + pair_counts = np.diff(np.append(pair_indices, sorted_values.size)) + best_order = np.lexsort( + ( + pair_values, + -pair_counts, + pair_groups, + ) + ) + candidate_groups = pair_groups[best_order] + candidate_values = pair_values[best_order] + first_candidate = np.empty(candidate_groups.size, dtype=bool) + first_candidate[0] = True + first_candidate[1:] = candidate_groups[1:] != candidate_groups[:-1] + result = np.full(number_groups, np.nan, dtype=np.float64) + result[candidate_groups[first_candidate]] = candidate_values[first_candidate] + return result
+ + + +
+[docs] +def coarsen_properties( + dck: ConfigViaDeck, + coarsening: CoarseningMaps, + modified_deck: list[str], + wellcind: list[int], +) -> tuple[NDArray, NDArray, NDArray, list[str]]: + """Aggregate reservoir properties onto the coarsened grid. + + Continuous properties use their configured or property-specific aggregation; + discrete properties use ``min``, ``max``, or ``mode``. The function writes + property include files and updates output pore volume and active cells. + + Parameters + ---------- + dck + Deck configuration and source INIT or restart properties. + coarsening + Cell groups and masks created by :func:`create_coarsening_maps`. + modified_deck + Deck lines updated with generated property includes. + wellcind + Coarse-cell indices containing well completions. + + Returns + ------- + cluster_minimum, cluster_maximum, removal_mask, generated_files + Activity summaries, the mask used to remove depth-jump cells, + and the generated include file names.""" + generated_files = [] + actnum = np.zeros(dck.original_cell_count, dtype=int) + top_depths = np.full(dck.original_cell_count, np.nan) + base_depths = np.full(dck.original_cell_count, np.nan) + cell_heights = np.full(dck.original_cell_count, np.nan) + top_corner_indices = (0, 1, 2, 3) + bottom_corner_indices = (4, 5, 6, 7) + actnum = (dck.original_porv > 0).astype(int) + d_x = np.full(dck.original_cell_count, np.nan) + d_y = np.full(dck.original_cell_count, np.nan) + d_z = np.full(dck.original_cell_count, np.nan) + d_ax = np.full(dck.original_cell_count, np.nan) + d_ay = np.full(dck.original_cell_count, np.nan) + d_az = np.full(dck.original_cell_count, np.nan) + permx, permy, permz = np.array([]), np.array([]), np.array([]) + + cell_volumes = np.asarray(dck.grid_model.cellvolumes()) + show_progress = sys.stdout.isatty() + if show_progress: + bar_ctx = alive_bar(dck.original_cell_count, bar="fish") + else: + bar_ctx = nullcontext() + with bar_ctx as bar_animation: + for coarse_k in range(dck.original_nz): + for coarse_j in range(dck.original_ny): + for coarse_i in range(dck.original_nx): + if show_progress: + bar_animation() + coarsening.reference_to_coarse.append( + dck.original_to_output_i[coarse_i + 1] + + (dck.original_to_output_j[coarse_j + 1] - 1) * dck.output_nx + + (dck.original_to_output_k[coarse_k + 1] - 1) + * dck.output_nx + * dck.output_ny + ) + coarse_index = ( + coarse_i + + coarse_j * dck.original_nx + + coarse_k * dck.original_nx * dck.original_ny + ) + cell_coordinates = dck.grid_model.xyz_from_ijk( + coarse_i, coarse_j, coarse_k + ) + x_length_0 = 0.0 + x_length_1 = 0.0 + y_length_0 = 0.0 + y_length_1 = 0.0 + z_length = 0.0 + for corner, row_offset, column_offset in zip( + range(4), (0, 0, 1, 1), (0, 1, 0, 1) + ): + x_length_0 += ( + abs( + cell_coordinates[0][1 + 2 * corner] + - cell_coordinates[0][2 * corner] + ) + / 4.0 + ) + x_length_1 += ( + abs( + cell_coordinates[1][1 + 2 * corner] + - cell_coordinates[1][2 * corner] + ) + / 4.0 + ) + y_length_0 += ( + abs( + cell_coordinates[0][column_offset + row_offset * 4 + 2] + - cell_coordinates[0][column_offset + row_offset * 4] + ) + / 4.0 + ) + y_length_1 += ( + abs( + cell_coordinates[1][column_offset + row_offset * 4 + 2] + - cell_coordinates[1][column_offset + row_offset * 4] + ) + / 4.0 + ) + z_length += ( + abs( + cell_coordinates[2][corner + 4] + - cell_coordinates[2][corner] + ) + / 4.0 + ) + d_x[coarse_index] = np.hypot(x_length_0, x_length_1) + d_y[coarse_index] = np.hypot(y_length_0, y_length_1) + d_z[coarse_index] = z_length + top_depths[coarse_index] = min( + cell_coordinates[2][corner_index] + for corner_index in top_corner_indices + ) + base_depths[coarse_index] = max( + cell_coordinates[2][corner_index] + for corner_index in top_corner_indices + ) + bottom_depth = max( + cell_coordinates[2][corner_index] + for corner_index in bottom_corner_indices + ) + cell_heights[coarse_index] = bottom_depth - top_depths[coarse_index] + + cluster_ids = np.asarray(coarsening.cell_groups, dtype=int) + actnum_values = np.asarray(actnum) + cluster_maximum = _grouped_max(actnum_values, cluster_ids) + cluster_minimum_all = _grouped_min(actnum_values, cluster_ids) + group_codes = np.asarray(coarsening.cell_groups, dtype=np.intp) - 1 + number_groups = int(group_codes.max()) + 1 + cluster_mode = _grouped_mode( + actnum_values, + group_codes, + number_groups, + ) + cluster_frequency = _grouped_sum(actnum_values, cluster_ids) + mean_cell_height = _grouped_mean(cell_heights, cluster_ids) + d_ax[dck.original_active_cell_mask] = d_x[dck.original_active_cell_mask] + d_ay[dck.original_active_cell_mask] = d_y[dck.original_active_cell_mask] + d_az[dck.original_active_cell_mask] = d_z[dck.original_active_cell_mask] + + x_tot = _grouped_sum(d_x, cluster_ids) + y_tot = _grouped_sum(d_y, cluster_ids) + z_tot = _grouped_sum(d_z, cluster_ids) + za_tot = _grouped_sum(d_az, cluster_ids) + z_a = _grouped_sum(d_az, cluster_ids) + if dck.dual_porosity_criterion: + za_tot = _grouped_sum(d_az * (coarsening.matrix_mask == 1), cluster_ids) + za_tot_dual = _grouped_sum(d_az * (coarsening.matrix_mask == 0), cluster_ids) + + total_volume = _grouped_sum(cell_volumes, cluster_ids) + if len(dck.active_cell_methods) == 1: + if dck.active_cell_methods[0] == "min": + cluster_minimum = cluster_minimum_all.copy() + elif dck.active_cell_methods[0] == "mode": + cluster_minimum = cluster_mode.copy() + else: + cluster_minimum = cluster_maximum.copy() + selected_actnum = cluster_minimum.copy() + else: + cluster_minimum = cluster_frequency.copy() + selected_actnum = cluster_frequency.copy() + cells_per_coarse_layer = dck.output_nx * dck.output_ny + for layer_index, aggregation in enumerate(dck.active_cell_methods): + layer_start = layer_index * cells_per_coarse_layer + layer_end = min( + layer_start + cells_per_coarse_layer, + cluster_frequency.size, + ) + if aggregation == "min": + layer_values = cluster_minimum_all[layer_start:layer_end] + elif aggregation == "mode": + layer_values = cluster_mode[layer_start:layer_end] + else: + layer_values = cluster_maximum[layer_start:layer_end] + cluster_minimum[layer_start:layer_end] = layer_values + selected_actnum[layer_start:layer_end] = layer_values + if dck.jump_thresholds[0]: + depth_difference = _grouped_max(base_depths, cluster_ids) - _grouped_min( + top_depths, cluster_ids + ) + if len(dck.jump_thresholds) == 1: + removal_mask = ( + depth_difference < float(dck.jump_thresholds[0]) * mean_cell_height + ).astype(int) + else: + removal_mask = np.zeros(cluster_frequency.size) + cells_per_coarse_layer = dck.output_nx * dck.output_ny + for layer_index, jump_value in enumerate(dck.jump_thresholds): + layer_start = layer_index * cells_per_coarse_layer + layer_end = min( + layer_start + cells_per_coarse_layer, + cluster_frequency.size, + ) + removal_mask[layer_start:layer_end] = ( + depth_difference[layer_start:layer_end] + < float(jump_value) * mean_cell_height[layer_start:layer_end] + ) + dck.output_actnum = (selected_actnum * removal_mask).astype(int) + else: + removal_mask = np.ones(mean_cell_height.size) + dck.output_actnum = selected_actnum.astype(int) + pore_volume = np.asarray(dck.original_porv, dtype=float) + if dck.dual_porosity_criterion: + matrix_pore_volume = _grouped_sum( + pore_volume * (coarsening.matrix_mask == 1), cluster_ids + ) + dual_pore_volume = _grouped_sum( + pore_volume * (coarsening.matrix_mask == 0), cluster_ids + ) + else: + matrix_pore_volume = _grouped_sum(pore_volume, cluster_ids) + dual_pore_volume = np.array([], dtype=float) + dck.output_porv = matrix_pore_volume + + pycopm_info("coarsening continuous quantities (e.g., PORO)") + number_values = dck.output_nx * dck.output_ny * dck.output_nz + dual_properties = ("porv", "poro", "tranx", "trany", "tranz") + zero_dual_properties = ("permx", "permy", "permz") + transmissibility_properties = {"tranx", "trany", "tranz"} + + show_progress = sys.stdout.isatty() + if show_progress: + bar_ctx = alive_bar(len(dck.props_keywords + dck.solution_keywords), bar="fish") + else: + bar_ctx = nullcontext() + with bar_ctx as bar_animation: + for property_name in dck.props_keywords + dck.solution_keywords: + if show_progress: + bar_animation() + property_values = np.full(dck.original_cell_count, np.nan) + if property_name in dck.props_keywords: + property_values[dck.original_active_cell_mask] = dck.init_file[ + property_name.upper() + ] + else: + property_values[dck.original_active_cell_mask] = dck.restart_file[ + property_name.upper(), 0 + ] + aggregation_property_values = property_values.copy() + use_physical_aggregation = ( + any(not method for method in dck.continuous_aggregation_method) + or property_name in transmissibility_properties + ) + if use_physical_aggregation: + if property_name in ("permx", "permy"): + property_values[dck.original_active_cell_mask] *= d_z[ + dck.original_active_cell_mask + ] + elif property_name == "permz": + active_permz = property_values[dck.original_active_cell_mask] + property_values[dck.original_active_cell_mask] = np.divide( + d_z[dck.original_active_cell_mask], + active_permz, + out=np.full(active_permz.shape, np.nan), + where=active_permz > 0, + ) + elif property_name in dck.multipliers_keywords: + property_values[dck.original_active_cell_mask] *= total_volume[ + dck.original_active_cell_mask + ] + elif property_name in transmissibility_properties: + direction = property_name[-1] + if ( + len(coarsening.coarsened_axes) == 1 + and dck.transmissibility_coarsening_method == 1 + and direction == coarsening.coarsened_axes[0] + ): + active_transmissibilities = property_values[ + dck.original_active_cell_mask + ] + property_values[dck.original_active_cell_mask] = np.divide( + 1.0, + active_transmissibilities, + out=np.full(active_transmissibilities.shape, np.nan), + where=active_transmissibilities > 0, + ) + else: + property_values[dck.original_active_cell_mask] *= dck.original_porv[ + dck.original_active_cell_mask + ] + dual_values = np.array([], dtype=float) + aggregation_dual_values = np.array([], dtype=float) + property_dual_c = np.array([], dtype=float) + if dck.dual_porosity_criterion: + dual_values = property_values.copy() + property_values[coarsening.matrix_mask == 0] = 0.0 + dual_values[coarsening.matrix_mask == 1] = 0.0 + aggregation_dual_values = np.where( + coarsening.matrix_mask == 0, + aggregation_property_values, + np.nan, + ) + aggregation_property_values = np.where( + coarsening.matrix_mask == 1, + aggregation_property_values, + np.nan, + ) + if property_name in ("tranx", "trany", "tranz"): + axis = {"tranx": 2, "trany": 1, "tranz": 0}[property_name] + neighboring_mask = np.roll( + coarsening.matrix_mask.reshape( + dck.original_nz, dck.original_ny, dck.original_nx + ), + -1, + axis=axis, + ).ravel() + property_values[neighboring_mask == 0] = 0.0 + dual_values[neighboring_mask == 1] = 0.0 + aggregation_property_values[neighboring_mask == 0] = np.nan + aggregation_dual_values[neighboring_mask == 1] = np.nan + if use_physical_aggregation: + grouped_values = _grouped_sum(property_values, cluster_ids) + grouped_dual_values = ( + _grouped_sum(dual_values, cluster_ids) + if dck.dual_porosity_criterion + else np.array([], dtype=float) + ) + if property_name in ("permx", "permy"): + thickness = np.asarray(z_tot, dtype=float) + values_c = np.divide( + grouped_values, + thickness, + out=np.zeros(grouped_values.shape), + where=(thickness * grouped_values) > 0, + ) + if dck.dual_porosity_criterion: + dual_thickness = np.asarray(za_tot_dual, dtype=float) + property_dual_c = np.divide( + grouped_dual_values, + dual_thickness, + out=np.zeros(grouped_dual_values.shape), + where=(dual_thickness * grouped_dual_values) > 0, + ) + elif property_name in ("tranx", "trany"): + direction = "x" if property_name == "tranx" else "y" + c_tot = x_tot if direction == "x" else y_tot + if direction in coarsening.coarsened_axes: + if dck.transmissibility_coarsening_method == 1: + grouped_minimum = _grouped_min(property_values, cluster_ids) + total_length = np.asarray(c_tot, dtype=float) + values_c = np.divide( + total_length, + grouped_values, + out=np.zeros(grouped_values.shape), + where=(grouped_minimum * grouped_values) > 0, + ) + if dck.dual_porosity_criterion: + grouped_dual_minimum = _grouped_min( + dual_values, cluster_ids + ) + property_dual_c = np.divide( + total_length, + grouped_dual_values, + out=np.zeros(grouped_dual_values.shape), + where=(grouped_dual_minimum * grouped_dual_values) + > 0, + ) + else: + grouped_average = _grouped_mean( + property_values, cluster_ids + ) + grouped_minimum = _grouped_min(property_values, cluster_ids) + values_c = np.where( + grouped_minimum > 0, grouped_average, 0.0 + ) + if dck.dual_porosity_criterion: + grouped_dual_average = _grouped_mean( + dual_values, cluster_ids + ) + grouped_dual_minimum = _grouped_min( + dual_values, cluster_ids + ) + property_dual_c = np.where( + grouped_dual_minimum > 0, + grouped_dual_average, + 0.0, + ) + else: + values_c = np.where(grouped_values > 0, grouped_values, 0.0) + if dck.dual_porosity_criterion: + property_dual_c = np.where( + grouped_dual_values > 0, + grouped_dual_values, + 0.0, + ) + elif property_name == "tranz": + if "z" in coarsening.coarsened_axes: + if dck.transmissibility_coarsening_method == 1: + grouped_minimum = _grouped_min(property_values, cluster_ids) + average_active_length = _grouped_mean(d_az, cluster_ids) + layer_size = dck.output_nx * dck.output_ny + if grouped_values.size > layer_size: + grouped_values[:-layer_size] = ( + ( + grouped_values[:-layer_size] + + grouped_values[layer_size:] + ) + * (grouped_minimum[:-layer_size] > 0) + * (grouped_minimum[layer_size:] > 0) + ) + total_length = np.asarray(z_tot, dtype=float) + denominator = grouped_values * total_length + valid_transmissibility = (grouped_values > 0) & ( + denominator != 0 + ) + values_c = np.divide( + average_active_length, + denominator, + out=np.zeros(grouped_values.shape), + where=valid_transmissibility, + ) + if dck.dual_porosity_criterion: + grouped_dual_minimum = _grouped_min( + dual_values, cluster_ids + ) + average_dual_length = _grouped_mean( + d_az * (coarsening.matrix_mask == 0), + cluster_ids, + ) + if grouped_dual_values.size > layer_size: + grouped_dual_values[:-layer_size] = ( + ( + grouped_dual_values[:-layer_size] + + grouped_dual_values[layer_size:] + ) + * (grouped_dual_minimum[:-layer_size] > 0) + * (grouped_dual_minimum[layer_size:] > 0) + ) + dual_denominator = grouped_dual_values * total_length + valid_dual_transmissibility = ( + grouped_dual_values > 0 + ) & (dual_denominator != 0) + property_dual_c = np.divide( + average_dual_length, + dual_denominator, + out=np.zeros(grouped_dual_values.shape), + where=valid_dual_transmissibility, + ) + else: + last_transmissibility = _grouped_last( + property_values, cluster_ids + ) + last_cell_volume = _grouped_last(cell_volumes, cluster_ids) + first_cell_volume = _grouped_first( + cell_volumes, cluster_ids + ) + layer_size = dck.original_nx * dck.original_ny + shifted_first_volume = np.roll( + first_cell_volume, -layer_size + ) + shifted_total_volume = np.roll(total_volume, -layer_size) + numerator = last_transmissibility * ( + last_cell_volume + shifted_first_volume + ) + denominator = total_volume + shifted_total_volume + values_c = np.divide( + numerator, + denominator, + out=np.zeros(last_transmissibility.shape), + where=(last_transmissibility > 0) & (denominator != 0), + ) + if dck.dual_porosity_criterion: + last_dual_transmissibility = _grouped_last( + dual_values, cluster_ids + ) + dual_numerator = last_dual_transmissibility * ( + last_cell_volume + shifted_first_volume + ) + property_dual_c = np.divide( + dual_numerator, + denominator, + out=np.zeros(last_dual_transmissibility.shape), + where=(last_dual_transmissibility > 0) + & (denominator != 0), + ) + else: + active_length = np.asarray(z_a, dtype=float) + total_length = np.asarray(z_tot, dtype=float) + valid_transmissibility = (grouped_values > 0) & ( + total_length != 0 + ) + values_c = np.divide( + grouped_values * active_length, + total_length, + out=np.zeros(grouped_values.shape), + where=valid_transmissibility, + ) + if dck.dual_porosity_criterion: + dual_active_length = np.asarray(za_tot_dual, dtype=float) + valid_dual_transmissibility = (grouped_dual_values > 0) & ( + total_length != 0 + ) + property_dual_c = np.divide( + grouped_dual_values * dual_active_length, + total_length, + out=np.zeros(grouped_dual_values.shape), + where=valid_dual_transmissibility, + ) + elif property_name == "permz": + thickness = np.asarray(za_tot, dtype=float) + values_c = np.divide( + thickness, + grouped_values, + out=np.zeros(grouped_values.shape), + where=(thickness * grouped_values) > 0, + ) + if dck.dual_porosity_criterion: + dual_thickness = np.asarray(za_tot_dual, dtype=float) + property_dual_c = np.divide( + dual_thickness, + grouped_dual_values, + out=np.zeros(grouped_dual_values.shape), + where=(dual_thickness * grouped_dual_values) > 0, + ) + elif property_name in ( + "poro", + "swatinit", + "disperc", + "thconr", + *dck.solution_keywords, + ): + values_c = np.divide( + grouped_values, + matrix_pore_volume, + out=np.zeros(grouped_values.shape), + where=matrix_pore_volume > 0, + ) + if dck.dual_porosity_criterion and property_name == "poro": + property_dual_c = np.divide( + grouped_dual_values, + dual_pore_volume, + out=np.zeros(grouped_dual_values.shape), + where=dual_pore_volume > 0, + ) + else: + values_c = np.divide( + grouped_values, + total_volume, + out=np.zeros(grouped_values.shape), + where=total_volume > 0, + ) + elif len(dck.continuous_aggregation_method) == 1: + aggregation = dck.continuous_aggregation_method[0] + if aggregation == "min": + values_c = _grouped_min(aggregation_property_values, cluster_ids) + if dck.dual_porosity_criterion: + property_dual_c = _grouped_min( + aggregation_dual_values, cluster_ids + ) + elif aggregation == "max": + values_c = _grouped_max(aggregation_property_values, cluster_ids) + if dck.dual_porosity_criterion: + property_dual_c = _grouped_max( + aggregation_dual_values, cluster_ids + ) + elif aggregation == "pvmean": + pv_property_values = aggregation_property_values * dck.original_porv + grouped_values = _grouped_sum(pv_property_values, cluster_ids) + values_c = np.divide( + grouped_values, + matrix_pore_volume, + out=np.zeros(grouped_values.shape), + where=matrix_pore_volume > 0, + ) + if dck.dual_porosity_criterion: + pv_dual_values = aggregation_dual_values * dck.original_porv + grouped_dual_values = _grouped_sum(pv_dual_values, cluster_ids) + property_dual_c = np.divide( + grouped_dual_values, + dual_pore_volume, + out=np.zeros(grouped_dual_values.shape), + where=dual_pore_volume > 0, + ) + else: + values_c = _grouped_mean(aggregation_property_values, cluster_ids) + if dck.dual_porosity_criterion: + property_dual_c = _grouped_mean( + aggregation_dual_values, cluster_ids + ) + else: + grouped_minimum = _grouped_min(aggregation_property_values, cluster_ids) + grouped_maximum = _grouped_max(aggregation_property_values, cluster_ids) + grouped_mean = _grouped_mean(aggregation_property_values, cluster_ids) + pv_property_values = aggregation_property_values * dck.original_porv + grouped_pv_values = _grouped_sum(pv_property_values, cluster_ids) + grouped_pvmean = np.divide( + grouped_pv_values, + matrix_pore_volume, + out=np.zeros(grouped_pv_values.shape), + where=matrix_pore_volume > 0, + ) + values_c = grouped_mean.copy() + if dck.dual_porosity_criterion: + grouped_dual_minimum = _grouped_min( + aggregation_dual_values, cluster_ids + ) + grouped_dual_maximum = _grouped_max( + aggregation_dual_values, cluster_ids + ) + grouped_dual_mean = _grouped_mean( + aggregation_dual_values, cluster_ids + ) + pv_dual_values = aggregation_dual_values * dck.original_porv + grouped_dual_pv_values = _grouped_sum(pv_dual_values, cluster_ids) + grouped_dual_pvmean = np.divide( + grouped_dual_pv_values, + dual_pore_volume, + out=np.zeros(grouped_dual_pv_values.shape), + where=dual_pore_volume > 0, + ) + property_dual_c = grouped_dual_mean.copy() + cells_per_coarse_layer = dck.output_nx * dck.output_ny + for layer_index, aggregation in enumerate( + dck.continuous_aggregation_method + ): + layer_start = layer_index * cells_per_coarse_layer + layer_end = min( + layer_start + cells_per_coarse_layer, + values_c.size, + ) + if aggregation == "min": + values_c[layer_start:layer_end] = grouped_minimum[ + layer_start:layer_end + ] + if dck.dual_porosity_criterion: + property_dual_c[layer_start:layer_end] = ( + grouped_dual_minimum[layer_start:layer_end] + ) + elif aggregation == "max": + values_c[layer_start:layer_end] = grouped_maximum[ + layer_start:layer_end + ] + if dck.dual_porosity_criterion: + property_dual_c[layer_start:layer_end] = ( + grouped_dual_maximum[layer_start:layer_end] + ) + elif aggregation == "pvmean": + values_c[layer_start:layer_end] = grouped_pvmean[ + layer_start:layer_end + ] + if dck.dual_porosity_criterion: + property_dual_c[layer_start:layer_end] = ( + grouped_dual_pvmean[layer_start:layer_end] + ) + else: + values_c[layer_start:layer_end] = grouped_mean[ + layer_start:layer_end + ] + if dck.dual_porosity_criterion: + property_dual_c[layer_start:layer_end] = grouped_dual_mean[ + layer_start:layer_end + ] + if ( + len(dck.continuous_aggregation_method) > 1 + and use_physical_aggregation + and property_name not in transmissibility_properties + ): + grouped_minimum = _grouped_min(aggregation_property_values, cluster_ids) + grouped_maximum = _grouped_max(aggregation_property_values, cluster_ids) + grouped_mean = _grouped_mean(aggregation_property_values, cluster_ids) + pv_property_values = aggregation_property_values * dck.original_porv + grouped_pv_values = _grouped_sum(pv_property_values, cluster_ids) + grouped_pvmean = np.divide( + grouped_pv_values, + matrix_pore_volume, + out=np.zeros(grouped_pv_values.shape), + where=matrix_pore_volume > 0, + ) + if dck.dual_porosity_criterion: + grouped_dual_minimum = _grouped_min( + aggregation_dual_values, cluster_ids + ) + grouped_dual_maximum = _grouped_max( + aggregation_dual_values, cluster_ids + ) + grouped_dual_mean = _grouped_mean( + aggregation_dual_values, cluster_ids + ) + pv_dual_values = aggregation_dual_values * dck.original_porv + grouped_dual_pv_values = _grouped_sum(pv_dual_values, cluster_ids) + grouped_dual_pvmean = np.divide( + grouped_dual_pv_values, + dual_pore_volume, + out=np.zeros(grouped_dual_pv_values.shape), + where=dual_pore_volume > 0, + ) + cells_per_coarse_layer = dck.output_nx * dck.output_ny + for layer_index, aggregation in enumerate( + dck.continuous_aggregation_method + ): + layer_start = layer_index * cells_per_coarse_layer + layer_end = min( + layer_start + cells_per_coarse_layer, + values_c.size, + ) + if aggregation == "min": + values_c[layer_start:layer_end] = grouped_minimum[ + layer_start:layer_end + ] + if dck.dual_porosity_criterion: + property_dual_c[layer_start:layer_end] = ( + grouped_dual_minimum[layer_start:layer_end] + ) + elif aggregation == "max": + values_c[layer_start:layer_end] = grouped_maximum[ + layer_start:layer_end + ] + if dck.dual_porosity_criterion: + property_dual_c[layer_start:layer_end] = ( + grouped_dual_maximum[layer_start:layer_end] + ) + elif aggregation == "pvmean": + values_c[layer_start:layer_end] = grouped_pvmean[ + layer_start:layer_end + ] + if dck.dual_porosity_criterion: + property_dual_c[layer_start:layer_end] = ( + grouped_dual_pvmean[layer_start:layer_end] + ) + elif aggregation: + values_c[layer_start:layer_end] = grouped_mean[ + layer_start:layer_end + ] + if dck.dual_porosity_criterion: + property_dual_c[layer_start:layer_end] = grouped_dual_mean[ + layer_start:layer_end + ] + values_c = np.asarray(values_c, dtype=float) + values_c[np.isnan(values_c)] = 0.0 + if property_dual_c.size: + property_dual_c = np.asarray(property_dual_c, dtype=float) + property_dual_c[np.isnan(property_dual_c)] = 0.0 + if ( + dck.coarsening_enabled + and dck.transmissibility_coarsening_method > 0 + and property_name in ["permx", "permy", "permz"] + ): + well_indices = np.asarray(wellcind, dtype=int) + property_values = np.asarray(values_c) + keep_values = np.zeros(property_values.size, dtype=bool) + keep_values[well_indices] = True + values_c = np.where(keep_values, property_values, 0.0) + if property_name == "tranx": + coarsening.coarse_tranx = values_c + if dck.dual_porosity_criterion: + coarsening.dual_tranx = property_dual_c + elif property_name == "trany": + coarsening.coarse_trany = values_c + if dck.dual_porosity_criterion: + coarsening.dual_trany = property_dual_c + if property_name == "permx": + permx = values_c + if property_name == "permy": + permy = values_c + if property_name == "permz": + permz = values_c + property_values = np.asarray(values_c) + allow_inline = not dck.dual_porosity_criterion and not ( + dck.transmissibility_coarsening_method > 0 + and property_name + in ( + "tranx", + "trany", + "tranz", + "permy", + "permz", + ) + ) + property_inlined = write_property_inc( + dck, + property_name, + values_c, + number_values, + modified_deck, + allow_inline, + ) + if property_inlined: + continue + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") + if dck.dual_porosity_criterion and property_name in [ + "poro", + "tranz", + "permx", + "permy", + "permz", + ]: + if property_name in dual_properties: + dual_values_c = np.asarray(property_dual_c) + elif property_name in zero_dual_properties: + dual_values_c = np.zeros_like(property_values) + else: + dual_values_c = property_values + property_values = _interleave_dual_property( + property_values, + dual_values_c, + dck.output_nx, + dck.output_nz, + ) + write_property_inc( + dck, + property_name, + property_values, + property_values.size, + modified_deck, + False, + "_DUAL_TMP_PYCOPM", + ) + if dck.dual_porosity_criterion: + property_values = np.asarray(dck.output_porv) + property_name = "porv" + property_values = _interleave_dual_property( + property_values, + dual_pore_volume, + dck.output_nx, + dck.output_nz, + ) + write_property_inc( + dck, + property_name, + property_values, + property_values.size, + modified_deck, + False, + "_DUAL_TMP_PYCOPM", + ) + removed = _compact_permeability_properties(dck, permx, permy, permz, modified_deck) + if "PERMY" in removed: + generated_files.remove(f"{dck.include_prefix}PERMY.INC") + if "PERMZ" in removed: + generated_files.remove(f"{dck.include_prefix}PERMZ.INC") + show_progress = sys.stdout.isatty() + if show_progress: + bar_ctx = alive_bar(len(dck.regions_keywords + dck.grids_keywords), bar="fish") + else: + bar_ctx = nullcontext() + pycopm_info("coarsening discrete quantities (e.g., SATNUM)") + with bar_ctx as bar_animation: + for property_name in dck.regions_keywords + dck.grids_keywords: + if show_progress: + bar_animation() + values = np.full(dck.original_cell_count, np.nan) + values[dck.original_active_cell_mask] = dck.init_file[property_name.upper()] + property_values = np.asarray(values, dtype=float) + valid_property_values = property_values[~np.isnan(property_values)] + default_value = 0.0 + if valid_property_values.size == 0: + grouped_values = np.full(int(cluster_ids.max()), np.nan) + elif np.max(valid_property_values) == np.min(valid_property_values): + default_value = float(valid_property_values[0]) + coarsening.dual_defaults[property_name] = default_value + grouped_values = _grouped_min(property_values, cluster_ids) + elif len(dck.discrete_aggregation_method) == 1: + aggregation = dck.discrete_aggregation_method[0] + if aggregation == "min": + grouped_values = _grouped_min(property_values, cluster_ids) + elif aggregation == "max": + grouped_values = _grouped_max(property_values, cluster_ids) + else: + group_codes = np.asarray(coarsening.cell_groups, dtype=np.intp) - 1 + number_groups = int(group_codes.max()) + 1 + grouped_values = _grouped_mode( + property_values, + group_codes, + number_groups, + ) + else: + grouped_minimum = _grouped_min(property_values, cluster_ids) + grouped_maximum = _grouped_max(property_values, cluster_ids) + group_codes = np.asarray(coarsening.cell_groups, dtype=np.intp) - 1 + number_groups = int(group_codes.max()) + 1 + grouped_mode = _grouped_mode( + property_values, + group_codes, + number_groups, + ) + grouped_values = grouped_mode.copy() + cells_per_coarse_layer = dck.output_nx * dck.output_ny + for layer_index, aggregation in enumerate( + dck.discrete_aggregation_method + ): + layer_start = layer_index * cells_per_coarse_layer + layer_end = min( + layer_start + cells_per_coarse_layer, + grouped_values.size, + ) + if aggregation == "min": + layer_values = grouped_minimum[layer_start:layer_end] + elif aggregation == "max": + layer_values = grouped_maximum[layer_start:layer_end] + else: + layer_values = grouped_mode[layer_start:layer_end] + grouped_values[layer_start:layer_end] = layer_values + values_c = np.where( + np.isnan(grouped_values), default_value, grouped_values + ).astype(int) + property_inlined = write_property_inc( + dck, + property_name, + values_c, + number_values, + modified_deck, + not dck.dual_porosity_criterion, + ) + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") + if property_inlined: + continue + if dck.dual_porosity_criterion: + default_value = coarsening.dual_defaults.get(property_name, 0) + if property_name == "fluxnum": + matrix_values = np.ones_like(values_c) + dual_values = np.full_like(values_c, 2) + else: + matrix_values = np.asarray(values_c) + dual_values = matrix_values + property_values = _interleave_dual_property( + matrix_values, + dual_values, + dck.output_nx, + dck.output_nz, + default_value, + ) + write_property_inc( + dck, + property_name, + property_values, + property_values.size, + modified_deck, + False, + "_DUAL_TMP_PYCOPM", + ) + + write_reference_to_coarse_map(dck, np.array(coarsening.reference_to_coarse)) + + generated_files.append(f"{dck.original_deck_name}_OPERNUM_PYCOPM_REFTOCOA.INC") + + return cluster_minimum, cluster_maximum, removal_mask, generated_files
+ + + +
+[docs] +def _interleave_dual_property( + property_values: NDArray, + dual_values: NDArray, + nx: int, + nz: int, + default_value: float = 0, +) -> NDArray: + """Interleave property and dual-property layers with separator rows.""" + property_values = np.asarray(property_values) + dual_values = np.asarray(dual_values) + cells_per_layer = property_values.size // nz + output_dtype = np.result_type( + property_values.dtype, + dual_values.dtype, + np.asarray(default_value).dtype, + ) + property_values = property_values.astype(output_dtype, copy=False) + dual_values = dual_values.astype(output_dtype, copy=False) + separator_values = np.full( + nx, + default_value, + dtype=output_dtype, + ) + property_blocks: list[NDArray] = [] + for layer_index in range(nz): + layer_slice = slice( + layer_index * cells_per_layer, + (layer_index + 1) * cells_per_layer, + ) + property_blocks.append(property_values[layer_slice]) + property_blocks.append(separator_values) + property_blocks.append(dual_values[layer_slice]) + return np.concatenate(property_blocks)
+ + + +
+[docs] +def _find_include_statement( + modified_deck: list[str], + include_line: str, +) -> tuple[int, int]: + """Return the list interval containing an INCLUDE statement.""" + include_index = modified_deck.index(include_line) + start_index = include_index + for index in range(include_index - 1, -1, -1): + line = modified_deck[index] + code = line.split("--", maxsplit=1)[0] + if not code.strip(): + continue + if re.fullmatch(r"\s*INCLUDE\s*", code, flags=re.IGNORECASE): + start_index = index + break + return start_index, include_index + 1
+ + + +
+[docs] +def _compact_permeability_properties( + dck: ConfigViaDeck, + permx: NDArray, + permy: NDArray, + permz: NDArray, + modified_deck: list[str], +) -> list[str]: + """Use COPY and MULTIPLY if PERMY and PERMZ can be generated from PERMX.""" + removed: list[str] = [] + copy_permy = np.array_equal(permx, permy) + copy_permz = False + output_path = Path(dck.output_directory) + permx_values = np.asarray(dck.init_file["PERMX"]) + permz_values = np.asarray(dck.init_file["PERMZ"]) + valid_permeability = (permx_values != 0) & (permz_values != 0) + facpermz = -1.0 + if np.any(valid_permeability): + first_valid_index = int(np.flatnonzero(valid_permeability)[0]) + facpermz = float( + permz_values[first_valid_index] / permx_values[first_valid_index] + ) + if facpermz > 0 and np.all(np.abs(permz - facpermz * permx) <= 1e-12): + copy_permz = True + include_statements = [] + if copy_permy: + include_line = f"'{dck.include_prefix}PERMY.INC' /\n" + include_statements.append(_find_include_statement(modified_deck, include_line)) + if copy_permz: + include_line = f"'{dck.include_prefix}PERMZ.INC' /\n" + include_statements.append(_find_include_statement(modified_deck, include_line)) + if not include_statements: + return removed + insertion_index = min(start_index for start_index, _ in include_statements) + for start_index, end_index in sorted(include_statements, reverse=True): + del modified_deck[start_index:end_index] + if copy_permy and copy_permz: + text = "COPY\nPERMX PERMY /\nPERMX PERMZ /\n/\n" + if abs(1 - facpermz) > 1e-12: + text += f"\nMULTIPLY\nPERMZ {facpermz:.4E} /\n/\n" + modified_deck.insert(insertion_index, text) + elif copy_permy: + modified_deck.insert( + insertion_index, + "COPY\nPERMX PERMY /\n/\n", + ) + elif copy_permz: + text = "COPY\nPERMX PERMZ /\n/" + if abs(1 - facpermz) > 1e-12: + text += f"\nMULTIPLY\nPERMZ {facpermz:.4E} /\n/\n" + modified_deck.insert(insertion_index, text) + if copy_permy: + permy_path = output_path / f"{dck.include_prefix}PERMY.INC" + permy_path.unlink(missing_ok=True) + permy_path = output_path / f"{dck.include_prefix}PERMY_DUAL_TMP_PYCOPM.INC" + permy_path.unlink(missing_ok=True) + removed.append("PERMY") + if copy_permz: + permz_path = output_path / f"{dck.include_prefix}PERMZ.INC" + permz_path.unlink(missing_ok=True) + permz_path = output_path / f"{dck.include_prefix}PERMZ_DUAL_TMP_PYCOPM.INC" + permz_path.unlink(missing_ok=True) + removed.append("PERMZ") + return removed
+ + + +
+[docs] +def redistribute_removed_pore_volume( + dck: ConfigViaDeck, + con: NDArray, + cluster_minimum: NDArray, + cluster_maximum: NDArray, + removal_mask: NDArray, +) -> None: + """Redistribute pore volume from removed coarse cells. + + Pore volume is divided among the nearest active neighbours without changing + the total pore volume. + + Parameters + ---------- + dck + Deck configuration whose ``output_porv`` is updated. + con + One-based coarse-cell identifier for each original cell. + cluster_minimum, cluster_maximum + Aggregated activity values used to identify changed clusters. + removal_mask + Mask identifying retained coarse cells.""" + cluster_ids = np.asarray(con, dtype=int) + pore_volumes = np.asarray(dck.original_porv, dtype=float) + dck.output_porv = np.asarray(dck.output_porv, dtype=float) + grouped_pore_volume = np.bincount( + cluster_ids, + weights=pore_volumes, + minlength=int(np.max(cluster_ids)) + 1, + ) + cluster_minimum = np.asarray(cluster_minimum) + cluster_maximum = np.asarray(cluster_maximum) + removal_mask = np.asarray(removal_mask) + changed_clusters = np.flatnonzero(cluster_maximum - cluster_minimum > 0) + 1 + removed_clusters = np.flatnonzero(removal_mask == 0) + 1 + redistribution_clusters = np.union1d(changed_clusters, removed_clusters) + maximum_distance = max(dck.output_nx, dck.output_ny, dck.output_nz) + total_cells = dck.output_nx * dck.output_ny * dck.output_nz + for cluster_value in redistribution_clusters: + cluster_id = int(cluster_value) + i, j, k = _global_index_to_ijk(dck, cluster_id - 1) + neighbor_indices: list[int] = [] + distance = 0 + offset = 0 + while not neighbor_indices and offset < total_cells: + neighbor_indices = _find_active_neighbors( + dck, + neighbor_indices, + cluster_id, + distance, + offset, + [i, j, k], + ) + distance += 1 + if distance > maximum_distance: + distance = 0 + offset += 1 + if not neighbor_indices: + pycopm_error( + "no active cell found to receive pore volume from " + f"cluster {cluster_id}" + ) + pore_volume_increment = grouped_pore_volume[cluster_id] / len(neighbor_indices) + dck.output_porv[ + np.asarray(neighbor_indices, dtype=int) + ] += pore_volume_increment
+ + + +
+[docs] +def _find_active_neighbors( + dck: ConfigViaDeck, + neighbor_indices: list[int], + cluster_id: int, + distance: int, + offset: int, + ijk: list, +) -> list[int]: + """Find active neighbouring cells for pore-volume redistribution.""" + total_cells = dck.output_nx * dck.output_ny * dck.output_nz + candidates = ( + (ijk[0] + 1 + distance < dck.output_nx, cluster_id + distance + offset), + (ijk[0] - 1 - distance >= 0, cluster_id - 2 - distance + offset), + ( + ijk[1] + 1 + distance < dck.output_ny, + cluster_id - 1 + (distance + 1) * dck.output_nx + offset, + ), + ( + ijk[1] - 1 - distance >= 0, + cluster_id - 1 - (distance + 1) * dck.output_nx + offset, + ), + ( + ijk[2] + 1 + distance < dck.output_nz, + cluster_id - 1 + (distance + 1) * dck.output_nx * dck.output_ny + offset, + ), + ( + ijk[2] - 1 - distance >= 0, + cluster_id - 1 - (distance + 1) * dck.output_nx * dck.output_ny + offset, + ), + ) + for valid_direction, candidate_index in candidates: + if ( + valid_direction + and 0 <= candidate_index < total_cells + and dck.output_actnum[candidate_index] == 1 + ): + neighbor_indices.append(candidate_index) + return neighbor_indices
+ + + +
+[docs] +def _global_index_to_ijk(dck: ConfigViaDeck, global_index: int) -> tuple[int, int, int]: + """Return the i, j, and k indices from a zero-based global cell index.""" + cells_per_layer = dck.output_nx * dck.output_ny + k_index, layer_index = divmod(global_index, cells_per_layer) + j_index, i_index = divmod(layer_index, dck.output_nx) + return i_index, j_index, k_index
+ + + +
+[docs] +def coarsen_corner_point_grid( + dck: ConfigViaDeck, coarsening: CoarseningMaps +) -> tuple[NDArray, NDArray]: + """Remove selected pillars and ZCORN surfaces from the grid. + + Parameters + ---------- + dck + Deck configuration containing the original corner-point grid. + coarsening + Axis mappings defining the removed rows, columns, and layers. + + Returns + ------- + coord, zcorn + Coarsened arrays when dual porosity is enabled; otherwise empty arrays.""" + original_nx = dck.original_nx + original_ny = dck.original_ny + original_nz = dck.original_nz + pillars_per_row = original_nx + 1 + pillar_count = pillars_per_row * (original_ny + 1) + cells_per_layer = original_nx * original_ny + total_zcorn_values = 8 * cells_per_layer * original_nz + coord_values = np.asarray(dck.egrid_file["COORD"]).reshape(-1) + zcorn_values = np.asarray(dck.egrid_file["ZCORN"]).reshape(-1) + removed_columns = np.flatnonzero(np.asarray(coarsening.x) > 1) + removed_rows = np.flatnonzero(np.asarray(coarsening.y) > 1) + removed_pillar_mask = np.zeros( + (original_ny + 1, pillars_per_row), + dtype=bool, + ) + removed_pillar_mask[:, removed_columns] = True + removed_pillar_mask[removed_rows, :] = True + coord_matrix = coord_values.reshape(pillar_count, 6) + coarsened_coord_values = coord_matrix[~removed_pillar_mask.ravel()].reshape(-1) + zcorn_removal_mask = np.zeros(total_zcorn_values, dtype=bool) + column_offsets = np.arange(2, dtype=np.intp) + row_offsets = np.arange(4 * original_nx, dtype=np.intp) + for column_index in removed_columns: + column_starts = np.arange( + 2 * column_index - 1, + total_zcorn_values, + 2 * original_nx, + dtype=np.intp, + ) + column_indices = (column_starts[:, None] + column_offsets).reshape(-1) + zcorn_removal_mask[column_indices] = True + for row_index in removed_rows: + row_starts = np.arange( + (2 * row_index - 1) * 2 * original_nx, + total_zcorn_values, + 4 * cells_per_layer, + dtype=np.intp, + ) + row_indices = (row_starts[:, None] + row_offsets).reshape(-1) + zcorn_removal_mask[row_indices] = True + zcorn_removal_indices = np.flatnonzero(zcorn_removal_mask).tolist() + zcorn_removal_indices = _collect_removed_zcorn_indices( + dck, + coarsening.z, + zcorn_removal_indices, + ) + final_zcorn_removal_indices = np.asarray( + zcorn_removal_indices, + dtype=np.intp, + ) + zcorn_removal_mask.fill(False) + if final_zcorn_removal_indices.size: + zcorn_removal_mask[final_zcorn_removal_indices] = True + coarsened_zcorn_values = zcorn_values[~zcorn_removal_mask] + write_grid( + dck, + coarsened_coord_values, + coarsened_zcorn_values, + False, + ) + if dck.dual_porosity_criterion: + return coarsened_coord_values, coarsened_zcorn_values + return np.array([]), np.array([])
+ + + +
+[docs] +def build_dual_porosity_grid( + dck: ConfigViaDeck, coarsening: CoarseningMaps, cr: NDArray, zc: NDArray +) -> tuple[NDArray, NDArray]: + """Extend a coarsened grid with a second porosity continuum. + + The matrix and fracture grids are separated in the j direction, and their + connections are added to ``coarsening.nnc_text``. + + Parameters + ---------- + dck + Deck configuration for the coarsened model. + coarsening + Coarsening data containing continuum masks and transmissibilities. + cr, zc + Coarsened ``COORD`` and ``ZCORN`` arrays. + + Returns + ------- + coord, zcorn + Extended dual-porosity grid arrays.""" + num_dig = dck.significant_digits + cells_per_layer = dck.output_nx * dck.output_ny + coord_values = np.asarray(cr, dtype=float) + y_offset = 1.075 * max( + coord_values[-6 * (dck.output_nx + 1) + 1] - coord_values[1], + coord_values[-2] - coord_values[6 * (dck.output_nx + 1) - 2], + ) + shifted_coord = coord_values.copy() + shifted_coord[1::3] += y_offset + cr = np.concatenate((coord_values, shifted_coord)) + zcorn_values = np.asarray(zc, dtype=float) + values_per_surface = 4 * cells_per_layer + zcorn_blocks: list[NDArray] = [] + for surface_index in range(zcorn_values.size // values_per_surface): + surface_start = surface_index * values_per_surface + surface_end = surface_start + values_per_surface + surface_values = zcorn_values[surface_start:surface_end] + zcorn_blocks.extend( + ( + surface_values, + surface_values[-2 * dck.output_nx :], + surface_values[: 2 * dck.output_nx], + surface_values, + ) + ) + zc = np.concatenate(zcorn_blocks) + mask_values = np.asarray(coarsening.matrix_mask) + values = np.full(dck.original_cell_count, np.nan) + values[dck.original_active_cell_mask] = dck.init_file["TRANX"] + tranx_values = np.asarray(values, dtype=float) + values = np.full(dck.original_cell_count, np.nan) + values[dck.original_active_cell_mask] = dck.init_file["TRANY"] + trany_values = np.asarray(values, dtype=float) + values = np.full(dck.original_cell_count, np.nan) + values[dck.original_active_cell_mask] = dck.init_file["TRANZ"] + tranz_values = np.asarray(values, dtype=float) + porv_values = np.asarray(dck.output_porv, dtype=float) + cluster_ids = np.asarray(coarsening.cell_groups, dtype=int) + dual_porv_values = _grouped_sum( + dck.original_porv * (coarsening.matrix_mask == 0), cluster_ids + ) + nnc_lines: list[str] = [] + show_progress = sys.stdout.isatty() + if show_progress: + bar_ctx = alive_bar(cells_per_layer * dck.output_nz, bar="fish") + else: + bar_ctx = nullcontext() + pycopm_info("processing the dual connectivity") + with bar_ctx as bar_animation: + for row_index in range(dck.original_ny): + for column_index in range(dck.original_nx): + original_layer = 0 + for layer_index in range(dck.output_nz): + if show_progress: + bar_animation() + positive_x = 0.0 + negative_x = 0.0 + positive_y = 0.0 + negative_y = 0.0 + vertical = 0.0 + while ( + original_layer + 1 < len(coarsening.z) + and coarsening.z[original_layer + 1] == 2 + ): + original_index = ( + column_index + + row_index * dck.original_nx + + original_layer * cells_per_layer + ) + coarse_index = ( + column_index + + row_index * dck.original_nx + + layer_index * cells_per_layer + ) + if ( + original_index + cells_per_layer < mask_values.size + and mask_values[original_index] + != mask_values[original_index + cells_per_layer] + and dual_porv_values[coarse_index] > 0 + and porv_values[coarse_index] > 0 + and not np.isnan(tranz_values[original_index]) + and coarsening.vertical_transfer_enabled + ): + vertical += tranz_values[original_index] + if ( + column_index < dck.original_nx - 1 + and mask_values[original_index] == 1 + and mask_values[original_index + 1] == 0 + and dual_porv_values[coarse_index + 1] > 0 + and not np.isnan(tranx_values[original_index]) + ): + positive_x += tranx_values[original_index] + if ( + column_index > 0 + and mask_values[original_index - 1] == 0 + and mask_values[original_index] == 1 + and dual_porv_values[coarse_index - 1] > 0 + and not np.isnan(tranx_values[original_index - 1]) + ): + negative_x += tranx_values[original_index - 1] + if ( + row_index < dck.original_ny - 1 + and mask_values[original_index] == 1 + and mask_values[original_index + dck.original_nx] == 0 + and dual_porv_values[coarse_index + dck.original_nx] > 0 + and not np.isnan(trany_values[original_index]) + ): + positive_y += trany_values[original_index] + if ( + row_index > 0 + and mask_values[original_index - dck.original_nx] == 0 + and mask_values[original_index] == 1 + and dual_porv_values[coarse_index - dck.original_nx] > 0 + and not np.isnan( + trany_values[original_index - dck.original_nx] + ) + ): + negative_y += trany_values[original_index - dck.original_nx] + original_layer += 1 + original_index = ( + column_index + + row_index * dck.original_nx + + original_layer * cells_per_layer + ) + coarse_index = ( + column_index + + row_index * dck.original_nx + + layer_index * cells_per_layer + ) + if ( + column_index < dck.original_nx - 1 + and mask_values[original_index] == 1 + and mask_values[original_index + 1] == 0 + and dual_porv_values[coarse_index + 1] > 0 + and not np.isnan(tranx_values[original_index]) + ): + positive_x += tranx_values[original_index] + if ( + column_index > 0 + and mask_values[original_index - 1] == 0 + and mask_values[original_index] == 1 + and dual_porv_values[coarse_index - 1] > 0 + and not np.isnan(tranx_values[original_index - 1]) + ): + negative_x += tranx_values[original_index - 1] + if ( + row_index < dck.original_ny - 1 + and mask_values[original_index] == 1 + and mask_values[original_index + dck.original_nx] == 0 + and dual_porv_values[coarse_index + dck.original_nx] > 0 + and not np.isnan(trany_values[original_index]) + ): + positive_y += trany_values[original_index] + if ( + row_index > 0 + and mask_values[original_index - dck.original_nx] == 0 + and mask_values[original_index] == 1 + and dual_porv_values[coarse_index - dck.original_nx] > 0 + and not np.isnan(trany_values[original_index - dck.original_nx]) + ): + negative_y += trany_values[original_index - dck.original_nx] + original_layer += 1 + matrix_row = row_index + 1 + fracture_row = row_index + dck.original_ny + 2 + output_layer = layer_index + 1 + if vertical > 0: + nnc_lines.append( + f"{column_index + 1} {matrix_row} {output_layer} " + f"{column_index + 1} {fracture_row} {output_layer} " + f"{round_like_e([vertical],num_dig)[0]} /\n" + ) + if positive_x > 0: + nnc_lines.append( + f"{column_index + 1} {matrix_row} {output_layer} " + f"{column_index + 2} {fracture_row} {output_layer} " + f"{round_like_e([positive_x],num_dig)[0]} /\n" + ) + if negative_x > 0: + nnc_lines.append( + f"{column_index + 1} {matrix_row} {output_layer} " + f"{column_index} {fracture_row} {output_layer} " + f"{round_like_e([negative_x],num_dig)[0]} /\n" + ) + if positive_y > 0: + nnc_lines.append( + f"{column_index + 1} {matrix_row} {output_layer} " + f"{column_index + 1} {fracture_row + 1} " + f"{output_layer} {round_like_e([positive_y],num_dig)[0]} /\n" + ) + if negative_y > 0: + nnc_lines.append( + f"{column_index + 1} {matrix_row} {output_layer} " + f"{column_index + 1} {fracture_row - 1} " + f"{output_layer} {round_like_e([negative_y],num_dig)[0]} /\n" + ) + coarsening.nnc_text += "".join(nnc_lines) + + for property_name in ["actnum", "tranx", "trany"]: + default_value = coarsening.dual_defaults.get(property_name, 0) + if property_name == "tranx": + source_values = coarsening.coarse_tranx + elif property_name == "trany": + source_values = coarsening.coarse_trany + else: + source_values = dck.output_actnum + property_blocks: list[NDArray] = [] + for layer_index in range(dck.output_nz): + layer_slice = slice( + layer_index * cells_per_layer, + (layer_index + 1) * cells_per_layer, + ) + property_blocks.append(source_values[layer_slice]) + property_blocks.append( + np.full( + dck.output_nx, + default_value, + dtype=source_values.dtype, + ) + ) + if property_name == "tranx": + property_blocks.append(np.asarray(coarsening.dual_tranx)[layer_slice]) + elif property_name == "trany": + property_blocks.append(np.asarray(coarsening.dual_trany)[layer_slice]) + else: + property_blocks.append(source_values[layer_slice]) + if property_name == "tranx": + coarsening.coarse_tranx = np.concatenate(property_blocks) + elif property_name == "trany": + coarsening.coarse_trany = np.concatenate(property_blocks) + else: + setattr(dck, f"output_{property_name}", np.concatenate(property_blocks)) + return cr, zc
+ + + +
+[docs] +def _collect_removed_zcorn_indices( + dck: ConfigViaDeck, coa_z: NDArray, removal_indices: list[int] +) -> list[int]: + """Add the ZCORN indices removed by vertical coarsening.""" + cells_per_layer = 4 * dck.original_nx * dck.original_ny + for layer_index in range(dck.original_nz + 1): + if coa_z[layer_index] > 1: + removal_indices.extend( + range( + (2 * layer_index - 1) * cells_per_layer, + (2 * layer_index + 1) * cells_per_layer, + ) + ) + return removal_indices
+ + + +
+[docs] +def map_nnc_transmissibilities( + dck: ConfigViaDeck, coarsening: CoarseningMaps +) -> list[str]: + """Map original non-neighbouring transmissibilities to the coarse grid. + + Connections that become Cartesian neighbours are accumulated in ``TRANX`` or + ``TRANY``; remaining connections are written as NNC records. + + Parameters + ---------- + dck + Deck configuration and source NNC data. + coarsening + Coarse mapping updated with transmissibilities and NNC text. + + Returns + ------- + generated_files + Names of the written include files.""" + generated_files = [] + output_directory = Path(dck.output_directory) + original_grid = OpmFile(f"{dck.input_deck_name}.EGRID") + coarsened_init = OpmFile(str(output_directory / f"{dck.output_deck_name}.INIT")) + num_dig = dck.significant_digits + first_connection_cells = np.asarray(original_grid["NNC1"], dtype=np.intp).reshape( + -1 + ) + second_connection_cells = np.asarray(original_grid["NNC2"], dtype=np.intp).reshape( + -1 + ) + connection_transmissibilities = np.asarray( + dck.init_file["TRANNNC"], + dtype=float, + ).reshape(-1) + connection_count = min( + first_connection_cells.size, + second_connection_cells.size, + connection_transmissibilities.size, + ) + first_cell_indices = first_connection_cells[:connection_count] - 1 + second_cell_indices = second_connection_cells[:connection_count] - 1 + connection_transmissibilities = connection_transmissibilities[:connection_count] + coarsened_porv = np.asarray(coarsened_init["PORV"], dtype=float).reshape(-1) + tranx_c = np.zeros(coarsened_porv.size, dtype=float) + trany_c = np.zeros(coarsened_porv.size, dtype=float) + active_cells = coarsened_porv > 0 + input_tranx = np.asarray(coarsened_init["TRANX"], dtype=float).reshape(-1) + input_trany = np.asarray(coarsened_init["TRANY"], dtype=float).reshape(-1) + if input_tranx.size == coarsened_porv.size: + tranx_c[active_cells] = input_tranx[active_cells] + elif input_tranx.size == np.count_nonzero(active_cells): + tranx_c[active_cells] = input_tranx + if input_trany.size == coarsened_porv.size: + trany_c[active_cells] = input_trany[active_cells] + elif input_trany.size == np.count_nonzero(active_cells): + trany_c[active_cells] = input_trany + cells_per_layer = dck.original_nx * dck.original_ny + first_cell_i = first_cell_indices % dck.original_nx + first_cell_j = first_cell_indices // dck.original_nx % dck.original_ny + first_cell_k = first_cell_indices // cells_per_layer + second_cell_i = second_cell_indices % dck.original_nx + second_cell_j = second_cell_indices // dck.original_nx % dck.original_ny + second_cell_k = second_cell_indices // cells_per_layer + output_i_map = np.fromiter( + ( + dck.original_to_output_i[original_index] + for original_index in range(1, dck.original_nx + 1) + ), + dtype=np.intp, + count=dck.original_nx, + ) + output_j_map = np.fromiter( + ( + dck.original_to_output_j[original_index] + for original_index in range(1, dck.original_ny + 1) + ), + dtype=np.intp, + count=dck.original_ny, + ) + output_k_map = np.fromiter( + ( + dck.original_to_output_k[original_index] + for original_index in range(1, dck.original_nz + 1) + ), + dtype=np.intp, + count=dck.original_nz, + ) + first_output_i = output_i_map[first_cell_i] + first_output_j = output_j_map[first_cell_j] + first_output_k = output_k_map[first_cell_k] + second_output_i = output_i_map[second_cell_i] + second_output_j = output_j_map[second_cell_j] + second_output_k = output_k_map[second_cell_k] + coarsening_mask = np.asarray(coarsening.matrix_mask).reshape(-1) + first_cell_mask = coarsening_mask[first_cell_indices] + second_cell_mask = coarsening_mask[second_cell_indices] + different_continuum = first_cell_mask != second_cell_mask + same_output_layer = first_output_k == second_output_k + different_horizontal_cell = (first_cell_i != second_cell_i) | ( + first_cell_j != second_cell_j + ) + horizontal_candidate = ( + ~different_continuum & same_output_layer & different_horizontal_cell + ) + positive_i_neighbour = horizontal_candidate & (first_cell_i + 1 == second_cell_i) + negative_i_neighbour = horizontal_candidate & (first_cell_i == second_cell_i + 1) + positive_j_neighbour = horizontal_candidate & (first_cell_j + 1 == second_cell_j) + negative_j_neighbour = horizontal_candidate & (first_cell_j == second_cell_j + 1) + first_coarse_indices = ( + first_output_i + - 1 + + (first_output_j - 1) * dck.output_nx + + (first_output_k - 1) * dck.output_nx * dck.output_ny + ) + second_coarse_indices = ( + second_output_i + - 1 + + (second_output_j - 1) * dck.output_nx + + (second_output_k - 1) * dck.output_nx * dck.output_ny + ) + positive_i_matrix = positive_i_neighbour & (first_cell_mask == 0) + positive_i_fracture = positive_i_neighbour & (first_cell_mask != 0) + negative_i_matrix = negative_i_neighbour & (first_cell_mask == 0) + negative_i_fracture = negative_i_neighbour & (first_cell_mask != 0) + positive_j_matrix = positive_j_neighbour & (first_cell_mask == 0) + positive_j_fracture = positive_j_neighbour & (first_cell_mask != 0) + negative_j_matrix = negative_j_neighbour & (first_cell_mask == 0) + negative_j_fracture = negative_j_neighbour & (first_cell_mask != 0) + np.add.at( + coarsening.dual_tranx, + first_coarse_indices[positive_i_matrix], + connection_transmissibilities[positive_i_matrix], + ) + np.add.at( + tranx_c, + first_coarse_indices[positive_i_fracture], + connection_transmissibilities[positive_i_fracture], + ) + np.add.at( + coarsening.dual_tranx, + second_coarse_indices[negative_i_matrix], + connection_transmissibilities[negative_i_matrix], + ) + np.add.at( + tranx_c, + second_coarse_indices[negative_i_fracture], + connection_transmissibilities[negative_i_fracture], + ) + np.add.at( + coarsening.dual_trany, + first_coarse_indices[positive_j_matrix], + connection_transmissibilities[positive_j_matrix], + ) + np.add.at( + trany_c, + first_coarse_indices[positive_j_fracture], + connection_transmissibilities[positive_j_fracture], + ) + np.add.at( + coarsening.dual_trany, + second_coarse_indices[negative_j_matrix], + connection_transmissibilities[negative_j_matrix], + ) + np.add.at( + trany_c, + second_coarse_indices[negative_j_fracture], + connection_transmissibilities[negative_j_fracture], + ) + nncc_mask = different_continuum | (~different_continuum & ~same_output_layer) + nncc_indices = np.flatnonzero(nncc_mask) + nncc_lines: list[str] = [] + for connection_index in nncc_indices: + if different_continuum[connection_index]: + if first_cell_mask[connection_index] == 1: + first_i = first_output_i[connection_index] + first_j = first_output_j[connection_index] + first_k = first_output_k[connection_index] + second_i = second_output_i[connection_index] + second_j = second_output_j[connection_index] + 1 + dck.original_ny + second_k = second_output_k[connection_index] + else: + first_i = second_output_i[connection_index] + first_j = second_output_j[connection_index] + first_k = second_output_k[connection_index] + second_i = first_output_i[connection_index] + second_j = first_output_j[connection_index] + 1 + dck.original_ny + second_k = first_output_k[connection_index] + elif first_cell_mask[connection_index] == 1: + first_i = first_output_i[connection_index] + first_j = first_output_j[connection_index] + first_k = first_output_k[connection_index] + second_i = second_output_i[connection_index] + second_j = second_output_j[connection_index] + second_k = second_output_k[connection_index] + else: + first_i = first_output_i[connection_index] + first_j = first_output_j[connection_index] + 1 + dck.original_ny + first_k = first_output_k[connection_index] + second_i = second_output_i[connection_index] + second_j = second_output_j[connection_index] + 1 + dck.original_ny + second_k = second_output_k[connection_index] + nncc_lines.append( + f"{first_i} {first_j} {first_k} " + f"{second_i} {second_j} {second_k} " + f"{round_like_e([connection_transmissibilities[connection_index]],num_dig)[0]} /\n" + ) + if nncc_lines: + coarsening.nnc_text += "".join(nncc_lines) + show_progress = sys.stdout.isatty() + if show_progress: + bar_ctx = alive_bar(connection_count, bar="fish") + else: + bar_ctx = nullcontext() + pycopm_info("processing non-neighbouring transmissibilities NNC (input model)") + with bar_ctx as bar_animation: + if show_progress and connection_count: + bar_animation(connection_count) + if not dck.dual_porosity_criterion: + generated_files.append(f"{dck.include_prefix}TRANX.INC") + property_path = output_directory / f"{dck.include_prefix}TRANX.INC" + write_property(property_path, "TRANX", tranx_c, num_dig) + property_path = output_directory / f"{dck.include_prefix}TRANY.INC" + write_property(property_path, "TRANY", trany_c, num_dig) + else: + coarsening.coarse_tranx = tranx_c + coarsening.coarse_trany = trany_c + return generated_files
+ + + +
+[docs] +def create_coarsening_map(cfg: ConfigViaTOML) -> NDArray: + """Map each fine-grid cell to a one-based coarse-cell identifier. + + The output dimensions and original-to-output axis mappings in ``cfg`` are also + updated. + + Parameters + ---------- + cfg + TOML configuration containing the axis coarsening arrays. + + Returns + ------- + NDArray + One-based coarse-cell identifier for every fine-grid cell.""" + cell_number = 0 + coarse_cell_number = 1 + nx_fine, ny_fine, nz_fine = cfg.original_nx, cfg.original_ny, cfg.original_nz + coa_map = np.zeros(cfg.original_cell_count, dtype=int) + for layer_index in range(nz_fine): + for row_index in range(ny_fine): + for column_index in range(nx_fine): + if coa_map[cell_number] == 0: + coa_map[cell_number] = coarse_cell_number + coarse_cell_number += 1 + if ( + column_index + 1 < nx_fine + and cfg.x_coarsening[column_index + 1] > 1 + ): + coa_map[cell_number + 1] = coa_map[cell_number] + if row_index + 1 < ny_fine and cfg.y_coarsening[row_index + 1] > 1: + coa_map[cell_number + nx_fine] = coa_map[cell_number] + if layer_index + 1 < nz_fine and cfg.z_coarsening[layer_index + 1] > 1: + coa_map[cell_number + nx_fine * ny_fine] = coa_map[cell_number] + cell_number += 1 + cfg.output_nx = nx_fine - int(np.count_nonzero(cfg.x_coarsening == 2)) + cfg.output_ny = ny_fine - int(np.count_nonzero(cfg.y_coarsening == 2)) + cfg.output_nz = nz_fine - int(np.count_nonzero(cfg.z_coarsening == 2)) + + cfg.original_to_output_i = np.zeros(nx_fine + 1, dtype=int) + coarse_index, fine_index = 1, 1 + for axis_index in range(nx_fine): + if cfg.original_to_output_i[fine_index] == 0: + cfg.original_to_output_i[fine_index] = coarse_index + coarse_index += 1 + if axis_index + 1 < nx_fine and cfg.x_coarsening[axis_index + 1] > 1: + cfg.original_to_output_i[fine_index + 1] = cfg.original_to_output_i[ + axis_index + 1 + ] + fine_index += 1 + + cfg.original_to_output_j = np.zeros(ny_fine + 1, dtype=int) + coarse_index, fine_index = 1, 1 + for axis_index in range(ny_fine): + if cfg.original_to_output_j[fine_index] == 0: + cfg.original_to_output_j[fine_index] = coarse_index + coarse_index += 1 + if axis_index + 1 < ny_fine and cfg.y_coarsening[axis_index + 1] > 1: + cfg.original_to_output_j[fine_index + 1] = cfg.original_to_output_j[ + axis_index + 1 + ] + fine_index += 1 + + cfg.original_to_output_k = np.zeros(nz_fine + 1, dtype=int) + coarse_index, fine_index = 1, 1 + for axis_index in range(nz_fine): + if cfg.original_to_output_k[fine_index] == 0: + cfg.original_to_output_k[fine_index] = coarse_index + coarse_index += 1 + if axis_index + 1 < nz_fine and cfg.z_coarsening[axis_index + 1] > 1: + cfg.original_to_output_k[fine_index + 1] = cfg.original_to_output_k[ + axis_index + 1 + ] + fine_index += 1 + + return coa_map
+ + + +
+[docs] +def _group_minimum_zero_based( + values: NDArray, + groups: NDArray, + number_groups: int, +) -> NDArray: + result = np.full(number_groups, np.inf) + np.minimum.at(result, groups, values) + return result
+ + + +
+[docs] +def _group_maximum_zero_based( + values: NDArray, + groups: NDArray, + number_groups: int, +) -> NDArray: + result = np.full(number_groups, -np.inf) + np.maximum.at(result, groups, values) + return result
+ + + +
+[docs] +def _group_sum_zero_based( + values: NDArray, + groups: NDArray, + number_groups: int, +) -> NDArray: + return np.bincount(groups, weights=values, minlength=number_groups)
+ + + +
+[docs] +def _read_satnum( + cfg: ConfigViaTOML, actnum: NDArray, nxyz: int, satnum_opm: NDArray +) -> NDArray: + """Read or generate fine-grid SATNUM values. + + Parameters + ---------- + cfg + TOML configuration controlling the SATNUM source. + actnum + Fine-grid active-cell mask. + nxyz + Number of fine-grid cells. + satnum_opm + SATNUM values read from the reference INIT file. + + Returns + ------- + NDArray + SATNUM value for every fine-grid cell.""" + reference_folder = ( + Path(cfg.resource_directory) / "reference_simulation" / cfg.model_name + ) + satnum = np.ones(nxyz, dtype=int) + if cfg.model_name == "norne": + satnum = np.load(reference_folder / "satnum.npy") + elif cfg.satnum_generation_method > 0: + satnum_files = {1: "satnum_5.out", 3: "satnum_60.out"} + satnum_values = [] + with open( + reference_folder / satnum_files[cfg.satnum_generation_method], + "r", + encoding="utf8", + ) as file: + for row in csv.reader(file, delimiter="#"): + satnum_values.append(int(row[0])) + satnum = np.asarray(satnum_values) + else: + satnum[actnum] = satnum_opm + return satnum
+ + + +
+[docs] +def coarsen_and_write_properties(cfg: ConfigViaTOML, coa_map: NDArray) -> int: + """Aggregate and write properties for a TOML-generated model. + + Parameters + ---------- + cfg + TOML configuration and reference-case settings. + coa_map + One-based fine-to-coarse cell mapping. + + Returns + ------- + int + Highest generated SATNUM value, used as the number of saturation tables.""" + reference_folder = ( + Path(cfg.resource_directory) / "reference_simulation" / cfg.model_name + ) + case_path = ( + Path(cfg.resource_directory) + / "reference_simulation" + / cfg.model_name + / cfg.reference_case_name + ) + pycopm_info("coarsening and writing the static properties") + preprocessing_path = Path(cfg.output_directory) / "preprocessing" + num_cells = cfg.output_nx * cfg.output_ny * cfg.output_nz + num_dig = cfg.significant_digits + groups = np.asarray(coa_map) - 1 + nxyz = cfg.original_cell_count + opm = OpmGrid(f"{case_path}.EGRID") + vol = np.asarray(opm.cellvolumes()) + 1e-10 + vol_c = _group_sum_zero_based(vol, groups, num_cells) + opm = OpmFile(f"{case_path}.INIT") + porv = np.asarray(opm["PORV"]) + actnum = porv > 0 + actnum_c = _group_minimum_zero_based(actnum, groups, num_cells).astype(int) + active = actnum_c > 0 + active_volumes = vol * actnum + props = ["poro", "ntg"] + for property_name in props: + values = np.zeros(nxyz, dtype=float) + values[actnum] = opm[property_name.upper()] + values_c = np.zeros(num_cells, dtype=float) + values_c[active] = ( + _group_sum_zero_based(values * active_volumes, groups, num_cells)[active] + / vol_c[active] + ) + write_compact_property_file( + preprocessing_path, property_name, values_c, num_dig + ) + + porv_c = np.zeros(num_cells, dtype=float) + porv_c[active] = _group_sum_zero_based(porv, groups, num_cells)[active] + if cfg.pore_volume_correction == 1: + coarse_pore_volume = np.sum(porv_c) + correction = np.sum(porv) / coarse_pore_volume + porv_c *= correction + write_compact_property_file(preprocessing_path, "porv", porv_c, num_dig) + + props = ["fipnum", "eqlnum"] + if cfg.model_name == "norne": + props += ["fluxnum"] + else: + props += ["multnum", "pvtnum", "fipzon"] + + for property_name in props: + values = np.zeros(nxyz, dtype=int) + values[actnum] = opm[property_name.upper()] + values_c = _group_minimum_zero_based(values, groups, num_cells).astype(int) + write_compact_property_file( + preprocessing_path, property_name, values_c, num_dig + ) + + satnum_c = np.ones(num_cells, dtype=int) + if cfg.model_name == "drogon" or ( + cfg.saturation_function_method == 1 and cfg.satnum_generation_method in (1, 3) + ): + values = _read_satnum(cfg, actnum, nxyz, opm["SATNUM"]) + satnum_c[:] = _group_minimum_zero_based(values, groups, num_cells).astype(int) + + if cfg.saturation_function_method == 1 and cfg.satnum_generation_method == 2: + preceding_active_cells = np.concatenate(([0], np.cumsum(actnum_c[:-1]))) + mask = preceding_active_cells > 1 + satnum_c[mask] += preceding_active_cells[mask] - 1 + + write_compact_property_file(preprocessing_path, "satnum", satnum_c, num_dig) + + endpoint_lines = [] + for property_name in ("swl", "sgu", "swcr"): + values_c = np.zeros(num_cells, dtype=float) + for coarse_index in np.flatnonzero(active): + values = np.zeros(nxyz, dtype=float) + values[actnum] = opm[property_name.upper()] + fine_indices = np.flatnonzero(groups == coarse_index) + fine_pore_volume = porv[fine_indices] + coarse_pore_volume = np.sum(fine_pore_volume) + values_c[coarse_index] = ( + np.sum(values[fine_indices] * fine_pore_volume) / coarse_pore_volume + ) + endpoint_lines.append(f"{property_name.upper()}\n") + endpoint_lines.extend( + format_opm_compact_values(round_like_e(values_c, num_dig)) + ) + endpoint_lines.append("/\n") + + write_include(preprocessing_path / "endpoints.inc", "".join(endpoint_lines)) + + if cfg.model_name == "norne": + values = np.load(reference_folder / "multz.npy") + multz_c = np.zeros(num_cells, dtype=float) + multz_minimum = _group_minimum_zero_based(values, groups, num_cells) + multz_c[active] = multz_minimum[active] + write_property( + preprocessing_path / "regionbarriers.inc", "MULTZ", multz_c, num_dig + ) + + active = actnum_c > 0 + active_volumes = vol * actnum + + project_path = Path(cfg.output_directory) + template_path = Path(cfg.resource_directory) / "template_scripts" + active_indices = np.flatnonzero(np.asarray(actnum_c) == 1) + last_active_index = int(active_indices[-1]) if active_indices.size else 0 + + for axis_index, property_name in enumerate(["permx", "permy", "permz"]): + values = np.zeros(nxyz, dtype=float) + values[actnum] = opm[property_name.upper()] + values_c = np.zeros(num_cells, dtype=float) + + if cfg.rock_property_settings[axis_index][2] == "max": + maximum = _group_maximum_zero_based(values, groups, num_cells) + values_c[active] = maximum[active] + else: + weighted_sum = _group_sum_zero_based( + values * active_volumes, groups, num_cells + ) + values_c[active] = weighted_sum[active] / vol_c[active] + + write_compact_property_file( + preprocessing_path, property_name, values_c, num_dig + ) + + values_c_min_max = np.zeros((num_cells, 2), dtype=float) + minimum = np.full(num_cells, np.inf) + maximum = np.full(num_cells, -np.inf) + np.minimum.at(minimum, groups, values) + np.maximum.at(maximum, groups, values) + values_c_min_max[:, 0] = minimum + values_c_min_max[:, 1] = 1.1 * maximum + + if cfg.rock_property_settings[axis_index][1] == 1 and cfg.execution_mode in ( + "files", + "ert", + ): + property_name = cfg.rock_property_settings[axis_index][0] + variables = { + "rock_property_settings": cfg.rock_property_settings, + "execution_mode": cfg.execution_mode, + "last_active_index": last_active_index, + "values_c": values_c, + "values_c_min_max": values_c_min_max, + "i": axis_index, + "active": active, + } + _render_template( + template_path / "common" / "perm.mako", + project_path / "parameters" / f"{property_name}.tmpl", + **variables, + ) + _render_template( + template_path / "common" / "perm_priors.mako", + project_path / "parameters" / f"{property_name}_priors.data", + **variables, + ) + _render_template( + template_path / "common" / "perm_eval.mako", + project_path / "jobs" / f"{property_name}_eval.py", + **variables, + ) + + props = ["swat"] + if cfg.initialization_method != 0: + props += ["sgas", "pressure", "rs", "rv"] + + swat = np.zeros(nxyz, dtype=float) + opm = OpmRestart(f"{case_path}.UNRST") + + init_lines = [] + for property_name in props: + values = np.zeros(nxyz, dtype=float) + values[actnum] = opm[property_name.upper(), 0] + if property_name == "swat": + swat = values.copy() + + values_c = np.zeros(num_cells, dtype=float) + weighted_sum = _group_sum_zero_based(values * porv, groups, num_cells) + values_c[active] = weighted_sum[active] / porv_c[active] + init_lines.append(f"{property_name.upper()}\n") + init_lines.extend(format_opm_compact_values(round_like_e(values_c, num_dig))) + init_lines.append("/\n") + write_include(preprocessing_path / "init.inc", "".join(init_lines)) + + swatinit_c = np.zeros(num_cells, dtype=float) + swatinit_sum = _group_sum_zero_based(swat * porv, groups, num_cells) + swatinit_c[active] = swatinit_sum[active] / porv_c[active] + + write_property(preprocessing_path / "swatinit.inc", "SWATINIT", swatinit_c, num_dig) + + return int(np.max(satnum_c))
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/files_writer.html b/docs/_modules/pycopm/utils/files_writer.html new file mode 100644 index 0000000..0ae8b60 --- /dev/null +++ b/docs/_modules/pycopm/utils/files_writer.html @@ -0,0 +1,1233 @@ + + + + + + + + + + pycopm.utils.files_writer — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.files_writer

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0913,R0914,R0916,R0917
+
+"""Write OPM Flow decks, corner-point grids, properties, and ERT files."""
+
+import shlex
+from pathlib import Path
+from typing import Any
+
+import numpy as np
+from mako.template import Template
+from numpy.typing import NDArray
+from opm.io.ecl import EclFile as OpmFile
+
+from pycopm.config.config import ConfigViaDeck, ConfigViaTOML
+
+HEADER = (
+    "-- Copyright (C) 2024-2026 NORCE Research AS\n"
+    "-- This deck was generated by pycopm https://github.com/cssr-tools/pycopm\n"
+)
+
+
+
+[docs] +def format_opm_compact_values(v: NDArray) -> list[str]: + """Convert values to OPM repeated-value notation. + + Parameters + ---------- + v + One-dimensional values to compact. + + Returns + ------- + list[str] + Values formatted as ``n*value`` where consecutive values repeat.""" + v = np.array(v) + change_idx = np.flatnonzero(np.diff(v, prepend=v[0] - 1)) + counts = np.diff(np.append(change_idx, v.size)) + vals = v[change_idx] + + out = [] + for val, n in zip(vals, counts): + val_str = str(int(val)) if float(val).is_integer() else str(val) + out.append(f"{n}*{val_str} " if n > 1 else f"{val_str} ") + return out
+ + + +
+[docs] +def round_like_e( + values: NDArray | list, + significant_digits: int, +) -> NDArray: + """Round values to a number of significant digits. + + Parameters + ---------- + values + Values to round. + significant_digits + Number of significant digits, or zero to preserve machine precision. + + Returns + ------- + NDArray + Rounded values with the original shape.""" + if significant_digits == 0: + return np.asarray(values) + values = np.asarray(values) + decimal_digits = significant_digits - 1 + rounded_values = np.fromiter( + (float(f"{value:.{decimal_digits}E}") for value in values.flat), + dtype=float, + count=values.size, + ) + return rounded_values.reshape(values.shape)
+ + + +
+[docs] +def _identify_removed_pillars(cfg: ConfigViaTOML) -> tuple[NDArray, NDArray]: + """Find COORD and ZCORN indices removed by TOML coarsening. + + Parameters + ---------- + cfg + TOML configuration containing axis coarsening arrays. + + Returns + ------- + coord_indices, zcorn_indices + Indices to remove from the reference grid arrays.""" + mr: list[float] = [] + ir: list[float] = [] + nx_fine, ny_fine, nz_fine = cfg.original_nx, cfg.original_ny, cfg.original_nz + nxyz = cfg.original_cell_count + for column_index in range(nx_fine + 1): + if cfg.x_coarsening[column_index] > 1: + for pillar_index in range( + column_index, (nx_fine + 1) * (ny_fine + 1), nx_fine + 1 + ): + mr.extend(range(pillar_index * 6, pillar_index * 6 + 6)) + for zcorn_index in range(2 * column_index - 1, 8 * nxyz, 2 * nx_fine): + ir.extend([zcorn_index, zcorn_index + 1]) + for row_index in range(ny_fine + 1): + if cfg.y_coarsening[row_index] > 1: + pillar_start = row_index * (nx_fine + 1) + pillar_stop = (row_index + 1) * (nx_fine + 1) + for pillar_index in range(pillar_start, pillar_stop): + mr.extend(range(pillar_index * 6, pillar_index * 6 + 6)) + for zcorn_index in range( + (2 * row_index - 1) * 2 * nx_fine, + 8 * nxyz, + 4 * nx_fine * ny_fine, + ): + ir.extend(range(zcorn_index, zcorn_index + 4 * nx_fine)) + layer_size = 4 * nx_fine * ny_fine + for layer_index in range(nz_fine + 1): + if cfg.z_coarsening[layer_index] > 1: + start_index = (2 * layer_index - 1) * layer_size + stop_index = (2 * layer_index + 1) * layer_size + ir.extend(range(start_index, stop_index)) + return np.asarray(mr, dtype=int), np.asarray(ir, dtype=int)
+ + + +
+[docs] +def _render_template(template_path: Path, output_path: Path, **variables: Any) -> None: + """Render a Mako template to a UTF-8 file. + + Parameters + ---------- + template_path + Source template path. + output_path + Destination path. + **variables + Values passed to the template.""" + template = Template(filename=str(template_path)) + output_path.write_text(template.render(**variables), encoding="utf8")
+ + + +
+[docs] +def write_coarsened_model_files(cfg: ConfigViaTOML, number_tables: int) -> None: + """Write grid, deck, schedule, job, observation, and ERT files. + + Parameters + ---------- + cfg + TOML configuration for the generated project. + number_tables + Number of saturation-function tables to generate.""" + if cfg.saturation_function_method > 0: + _write_let_tables(cfg, number_tables) + _write_grid_files(cfg) + _write_ert_files(cfg, number_tables) + project_path = Path(cfg.output_directory) + template_path = Path(cfg.resource_directory) / "template_scripts" + variables = { + "original_to_output_i": cfg.original_to_output_i, + "original_to_output_j": cfg.original_to_output_j, + "original_to_output_k": cfg.original_to_output_k, + } + _render_template( + template_path / cfg.model_name / "schedule.mako", + project_path / "preprocessing" / "schedule.SCH", + **variables, + ) + _render_template( + template_path / "common" / "time_eval.mako", + project_path / "jobs" / "time_eval.py", + name=cfg.reference_case_name, + ) + _render_template( + template_path / "common" / "flow_eval.mako", + project_path / "jobs" / "flow_eval.py", + flow=shlex.split(cfg.flow_command.strip()) + + [f"{cfg.reference_case_name}_COARSER.DATA"], + ) + _render_template( + template_path / cfg.model_name / "deck.mako", + project_path / "preprocessing" / f"{cfg.reference_case_name}_COARSER.DATA", + output_nx=cfg.output_nx, + output_ny=cfg.output_ny, + output_nz=cfg.output_nz, + output_directory=cfg.output_directory, + rock_property_settings=cfg.rock_property_settings, + execution_mode=cfg.execution_mode, + saturation_function_method=cfg.saturation_function_method, + use_let_tables=cfg.use_let_tables, + initial=cfg.initialization_method, + original_to_output_k=cfg.original_to_output_k, + number_tables=number_tables, + )
+ + + +
+[docs] +def _write_ert_files(cfg: ConfigViaTOML, number_tables: int) -> None: + """Write the ERT configuration, parameter, job, and observation files.""" + project_path = Path(cfg.output_directory) + template_path = Path(cfg.resource_directory) / "template_scripts" + for coefficient_index, coefficient in enumerate(cfg.let_parameters): + if ( + coefficient[2] == 1 + and cfg.execution_mode in ("files", "ert") + and cfg.saturation_function_method == 1 + ): + cfg.use_let_tables = True + variables = { + "i": coefficient_index, + "number_tables": number_tables, + "let_parameters": cfg.let_parameters, + } + coefficient_name = str(coefficient[0]) + _render_template( + template_path / "common" / "let.mako", + project_path / "parameters" / f"coeff_{coefficient_name}.tmpl", + **variables, + ) + _render_template( + template_path / "common" / "let_priors.mako", + project_path / "parameters" / f"coeff_{coefficient_name}_priors.data", + **variables, + ) + _render_template( + template_path / cfg.model_name / "observations.mako", + project_path / "observations" / "observations.data", + minerror=cfg.observation_minimum_errors, + error=cfg.observation_relative_errors, + ) + variables = { + "let_parameters": cfg.let_parameters, + "execution_mode": cfg.execution_mode, + "number_tables": number_tables, + } + if cfg.use_let_tables: + _render_template( + template_path / cfg.model_name / "table_eval.mako", + project_path / "jobs" / "table_eval.py", + **variables, + ) + _render_template( + template_path / "common" / "ert.mako", + project_path / "ert.ert", + execution_mode=cfg.execution_mode, + ensemble_size=cfg.ensemble_size, + min_successful_realizations=cfg.min_successful_realizations, + max_realization_runtime_seconds=cfg.max_realization_runtime_seconds, + max_parallel_realizations=cfg.max_parallel_realizations, + random_seed=cfg.random_seed, + reference_case_name=cfg.reference_case_name, + rock_property_settings=cfg.rock_property_settings, + use_let_tables=cfg.use_let_tables, + let_parameters=cfg.let_parameters, + resource_directory=cfg.resource_directory, + model_name=cfg.model_name, + number_tables=number_tables, + )
+ + + +
+[docs] +def _write_let_tables(cfg: ConfigViaTOML, number_tables: int) -> None: + """Write the LET saturation-function tables.""" + table_lines = [HEADER] + + if cfg.model_name == "norne": + table_lines.append("SWOFLET\n") + for _ in range(number_tables): + table_lines.append( + f"0 0.0001 {max(1.1, cfg.let_parameters[0][1])} " + f"{10.0 ** cfg.let_parameters[1][1]} {max(1.1, cfg.let_parameters[2][1])} " + "0.5 0 0 " + f"{max(1.0, cfg.let_parameters[12][1]) * max(1.1, cfg.let_parameters[3][1])} " + f"{10.0 ** cfg.let_parameters[4][1]} {max(1.1, cfg.let_parameters[5][1])} " + "1 0.69977 17.56167 0.95615 3.76138 0.03819 /\n" + ) + for _ in range(number_tables): + table_lines.append( + f"0 0.0001 {max(1.1, cfg.let_parameters[0][1])} " + f"{10.0 ** cfg.let_parameters[1][1]} {max(1.1, cfg.let_parameters[2][1])} " + f"0.5 0 0 {max(1.1, cfg.let_parameters[3][1])} " + f"{max(0.9, cfg.let_parameters[13][1]) * 10.0 ** cfg.let_parameters[4][1]} " + f"{max(1.0, cfg.let_parameters[14][1]) * max(1.1, cfg.let_parameters[5][1])} " + "1 0.69977 17.56167 0.95615 3.76138 0.03819 /\n" + ) + + table_lines.append("SGOFLET\n") + for _ in range(number_tables): + table_lines.append( + f"0 0 {max(1.0, cfg.let_parameters[15][1]) * max(1.1, cfg.let_parameters[6][1])} " + f"{10.0 ** cfg.let_parameters[7][1]} {max(1.1, cfg.let_parameters[8][1])} " + f"0.95 0 0.0001 {max(1.1, cfg.let_parameters[9][1])} " + f"{10.0 ** cfg.let_parameters[10][1]} {max(1.1, cfg.let_parameters[11][1])} " + "0.99997432 1 1 1 0 0 /\n" + ) + for _ in range(number_tables): + table_lines.append( + f"0 0 {max(1.1, cfg.let_parameters[6][1])} " + f"{max(1.0, cfg.let_parameters[16][1]) * 10.0 ** cfg.let_parameters[7][1]} " + f"{max(1.0, cfg.let_parameters[17][1]) * max(1.1, cfg.let_parameters[8][1])} " + f"0.95 0 0.0001 {max(1.1, cfg.let_parameters[9][1])} " + f"{10.0 ** cfg.let_parameters[10][1]} {max(1.1, cfg.let_parameters[11][1])} " + "0.99997432 1 1 1 0 0 /\n" + ) + + elif cfg.model_name == "drogon": + table_lines.append("SWOFLET\n") + for _ in range(number_tables): + table_lines.append( + f"0 0.0001 {max(1.1, cfg.let_parameters[0][1])} " + f"{10.0 ** cfg.let_parameters[1][1]} {max(1.1, cfg.let_parameters[2][1])} " + f"{cfg.let_parameters[14][1]} 0 0 " + f"{max(1.0, cfg.let_parameters[12][1]) * max(1.1, cfg.let_parameters[3][1])} " + f"{10.0 ** cfg.let_parameters[4][1]} {max(1.1, cfg.let_parameters[5][1])} " + f"{cfg.let_parameters[15][1]} 0.69977 17.56167 0.95615 3.76138 0.03819 /\n" + ) + + table_lines.append("SGOFLET\n") + for _ in range(number_tables): + table_lines.append( + f"0 0 {max(1.0, cfg.let_parameters[13][1]) * max(1.1, cfg.let_parameters[6][1])} " + f"{10.0 ** cfg.let_parameters[7][1]} {max(1.1, cfg.let_parameters[8][1])} " + f"{cfg.let_parameters[16][1]} 0 0.0001 {max(1.1, cfg.let_parameters[9][1])} " + f"{10.0 ** cfg.let_parameters[10][1]} {max(1.1, cfg.let_parameters[11][1])} " + f"{cfg.let_parameters[17][1]} 1 1 1 0 0 /\n" + ) + + (Path(cfg.output_directory) / "preprocessing" / "tables.inc").write_text( + "".join(table_lines), encoding="utf8" + )
+ + + +
+[docs] +def _write_grid_files(cfg: ConfigViaTOML) -> None: + """Write the OPM grid-related files.""" + case = ( + Path(cfg.resource_directory) + / "reference_simulation" + / cfg.model_name + / cfg.reference_case_name + ) + grid_path = f"{case}.EGRID" + grid_file = OpmFile(grid_path) + zc, cr = grid_file["ZCORN"], grid_file["COORD"] + coord_indices, zcorn_indices = _identify_removed_pillars(cfg) + cr = np.delete(np.asarray(cr), coord_indices) + zc = np.delete(np.asarray(zc), zcorn_indices) + write_grid(cfg, cr, zc, False) + project_path = Path(cfg.output_directory) + template_path = Path(cfg.resource_directory) / "template_scripts" / cfg.model_name + variables = { + "original_to_output_i": cfg.original_to_output_i, + "original_to_output_j": cfg.original_to_output_j, + "original_to_output_k": cfg.original_to_output_k, + } + _render_template( + template_path / "fault.mako", + project_path / "preprocessing" / "fault.inc", + **variables, + ) + if cfg.model_name == "norne": + _render_template( + template_path / "localbarriers.mako", + project_path / "preprocessing" / "localbarriers.inc", + **variables, + ) + elif cfg.model_name == "drogon": + _render_template( + template_path / "trans.mako", + project_path / "preprocessing" / "trans.inc", + **variables, + )
+ + + +
+[docs] +def write_include(output_path: Path, text: str) -> None: + """Write text to an OPM include file with the pycopm header. + + Parameters + ---------- + output_path + Destination include path. + text + OPM deck text written after the header.""" + output_path.write_text(f"{HEADER}{text}", encoding="utf8")
+ + + +
+[docs] +def write_property(output_path: Path, keyword: str, values: NDArray, num_dig) -> None: + """Write one compact OPM property. + + Parameters + ---------- + output_path + Destination include path. + keyword + OPM property keyword. + values + Property values in global-cell order. + num_dig + Number of significant digits.""" + compacted_values = format_opm_compact_values( + round_like_e(np.asarray(values), num_dig) + ) + output_path.write_text( + f"{HEADER}{keyword}\n{''.join(compacted_values)}/\n", encoding="utf8" + )
+ + + +
+[docs] +def write_compact_property_file( + preprocessing_path: Path, property_name: str, values: NDArray, num_dig +) -> None: + """Write one compacted OPM property to a file.""" + write_property( + preprocessing_path / f"{property_name}.inc", + property_name.upper(), + values, + num_dig, + )
+ + + +
+[docs] +def write_porv(dck: ConfigViaDeck, modified_deck: list[str]) -> None: + """Write the OPM grid-related files.""" + write_property_inc( + dck, + "porv", + dck.output_porv, + dck.output_porv.size, + modified_deck, + False, + )
+ + + +
+[docs] +def write_property_inc( + dck: ConfigViaDeck, + property_name: str, + property_values: NDArray, + number_values: int, + modified_deck: list[str], + allow_inline: bool = False, + file_suffix: str = "", +) -> bool: + """Write a property include or inline a constant property. + + Parameters + ---------- + dck + Deck configuration controlling paths, prefixes, and precision. + property_name + OPM property name. + property_values + Values in output-grid order. + number_values + Expected number of output values. + modified_deck + Deck lines in which a constant property may be inlined. + allow_inline + Inline a property when all values are equal. + file_suffix + Optional suffix added before ``.INC``. + + Returns + ------- + bool + ``True`` when the property was inlined, otherwise ``False``.""" + output_directory = Path(dck.output_directory) + num_dig = dck.significant_digits + property_values = np.asarray(property_values) + compacted_values = format_opm_compact_values(round_like_e(property_values, num_dig)) + property_path = output_directory / ( + f"{dck.include_prefix}{property_name.upper()}{file_suffix}.INC" + ) + if ( + allow_inline + and not file_suffix + and compacted_values + and len(compacted_values) == 1 + and "*" in compacted_values[0] + and int(compacted_values[0].split("*", maxsplit=1)[0]) == number_values + ): + include_line = f"'{dck.include_prefix}{property_name.upper()}.INC' /\n" + include_index = modified_deck.index(include_line) + repeated_value = compacted_values[0].split("*", maxsplit=1)[1] + del modified_deck[include_index] + del modified_deck[include_index - 1] + modified_deck.insert( + include_index - 1, + f"{property_name.upper()}\n{number_values}*{repeated_value}/\n", + ) + property_path.unlink(missing_ok=True) + return True + property_path.write_text( + HEADER + f"{property_name.upper()}\n{''.join(compacted_values)}/\n", + encoding="utf8", + ) + return False
+ + + +
+[docs] +def write_grid( + cfg: ConfigViaDeck | ConfigViaTOML, cr: NDArray, zc: NDArray, dual: bool +) -> None: + """Write a corner-point grid in GRDECL syntax. + + Parameters + ---------- + cfg + Deck or TOML configuration containing output dimensions and precision. + cr, zc + Flattened ``COORD`` and ``ZCORN`` arrays. + dual + Extend the j dimension for a dual-porosity grid.""" + if isinstance(cfg, ConfigViaDeck): + cfg.output_cell_count = cfg.output_nx * cfg.output_ny * cfg.output_nz + output_grid = [ + HEADER, + "SPECGRID\n", + f"{cfg.output_nx} {cfg.output_ny * (int(dual) + 1) + int(dual)} {cfg.output_nz} /\n", + "COORD\n", + ] + num_dig = cfg.significant_digits + output_grid.extend(format_opm_compact_values(round_like_e(np.asarray(cr), num_dig))) + output_grid.extend(["/\n", "ZCORN\n"]) + output_grid.extend(format_opm_compact_values(round_like_e(np.asarray(zc), num_dig))) + output_grid.append("/") + if isinstance(cfg, ConfigViaDeck): + if not cfg.dual_porosity_criterion and not cfg.grid_transformation: + output_grid.append("\nACTNUM\n") + output_grid.extend( + format_opm_compact_values( + round_like_e(np.asarray(cfg.output_actnum), num_dig) + ) + ) + output_grid.append("/") + output_grid.append("\n") + output_path = Path(cfg.output_directory) / f"{cfg.include_prefix}GRID.INC" + else: + output_grid.append("\n") + output_path = ( + Path(cfg.output_directory) + / "preprocessing" + / f"{cfg.reference_case_name}_COARSER.GRDECL" + ) + output_path.write_text("".join(output_grid), encoding="utf8")
+ + + +
+[docs] +def write_reference_to_coarse_map(dck: ConfigViaDeck, reftocoa: NDArray) -> None: + """Write the reference-to-coarse mapping as OPERNUM. + + Parameters + ---------- + dck + Deck configuration controlling the output path and precision. + reftocoa + Coarse-cell identifier for every reference-grid cell.""" + num_dig = dck.significant_digits + output_directory = Path(dck.output_directory) + opernum_output = format_opm_compact_values(round_like_e(reftocoa, num_dig)) + opernum_path = ( + output_directory / f"{dck.original_deck_name}_OPERNUM_PYCOPM_REFTOCOA.INC" + ) + opernum_path.write_text( + f"{HEADER}OPERNUM\n{''.join(opernum_output)}/\n", encoding="utf8" + )
+ + + +
+[docs] +def write_dual_properties( + dck: ConfigViaDeck, + coarsening, + number_values: int, + modified_deck: list[str], +) -> None: + """Finalize property files for a dual-porosity grid. + + Parameters + ---------- + dck + Deck configuration and generated property names. + coarsening + Coarsening data containing matrix and fracture transmissibilities. + number_values + Number of cells in the extended dual grid. + modified_deck + Deck lines that may receive inlined properties.""" + names = ( + dck.props_keywords + + dck.grids_keywords + + dck.regions_keywords + + dck.solution_keywords + + ["porv"] + ) + output_directory = Path(dck.output_directory) + for property_name in names: + property_path = output_directory / ( + f"{dck.include_prefix}{property_name.upper()}.INC" + ) + if property_name not in ["tranx", "trany"]: + temporary_property_path = output_directory / ( + f"{dck.include_prefix}{property_name.upper()}_DUAL_TMP_PYCOPM.INC" + ) + if temporary_property_path.is_file(): + temporary_property_path.replace(property_path) + continue + property_values = ( + coarsening.coarse_tranx + if property_name == "tranx" + else coarsening.coarse_trany + ) + write_property_inc( + dck, + property_name, + property_values, + number_values, + modified_deck, + True, + )
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/generate_decks.html b/docs/_modules/pycopm/utils/generate_decks.html new file mode 100644 index 0000000..e7a66d1 --- /dev/null +++ b/docs/_modules/pycopm/utils/generate_decks.html @@ -0,0 +1,1292 @@ + + + + + + + + + + pycopm.utils.generate_decks — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.generate_decks

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0912,R0913,R0914,R0915,C0302,R0917,R1702,R0916,R0911,R0801,E1102
+
+"""Coordinate coarsening, refinement, submodel extraction, and grid transformations."""
+
+import argparse
+import csv
+import subprocess
+import sys
+from pathlib import Path
+from shutil import copy2
+from typing import cast
+
+import numpy as np
+from opm.io.ecl import EclFile as OpmFile
+from opm.io.ecl import EGrid as OpmGrid
+
+from pycopm.config.config import ConfigViaDeck
+from pycopm.utils.coarsening import (
+    CoarseningMaps,
+    build_dual_porosity_grid,
+    coarsen_corner_point_grid,
+    coarsen_properties,
+    create_coarsening_maps,
+    map_nnc_transmissibilities,
+    redistribute_removed_pore_volume,
+)
+from pycopm.utils.files_writer import (
+    write_dual_properties,
+    write_grid,
+    write_include,
+    write_porv,
+)
+from pycopm.utils.parser_deck import find_multiplier_keywords, process_deck
+from pycopm.utils.refinement import (
+    RefinementMaps,
+    create_refinement_maps,
+    refine_grid,
+    refine_properties,
+)
+from pycopm.utils.terminal import (
+    cli_correct_value,
+    cli_error_value,
+    cli_info_value,
+    pycopm_error,
+    pycopm_info,
+    pycopm_success,
+    pycopm_warning,
+)
+from pycopm.utils.transformation import (
+    transform_grid,
+    transform_properties,
+)
+from pycopm.utils.vicinity import (
+    VicinityMaps,
+    apply_boundary_pore_volume_correction,
+    create_vicinity_maps,
+    extract_vicinity_grid,
+    map_vicinity_properties,
+)
+
+
+
+[docs] +def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: + """Generate a modified OPM deck and its include files. + + The selected workflow can preprocess the input deck, coarsen or refine the + grid, extract a vicinity submodel, transform coordinates, and optionally run + a validation dry run. + + Parameters + ---------- + dck + Deck configuration populated from command-line arguments. + cmdargs + Parsed command arguments used to build coarsening or refinement maps.""" + output_directory = Path(dck.output_directory) + source_deck = Path(f"{dck.input_deck_path}.DATA") + generated_files = [] + if dck.requested_ijk[0]: + dck.requested_ijk = [int(value) for value in dck.requested_ijk[0].split(",")] + if not dck.output_deck_name: + dck.output_deck_name = f"{dck.input_deck_name}_PYCOPM" + flags_dry_run = ( + "--parsing-strictness=low --check-satfunc-consistency=false " + "--enable-dry-run=true --output-mode=none" + ) + output_types = [".INIT", ".EGRID"] + dry_run_name = f"{dck.input_deck_name}_PREP_PYCOPM_DRYRUN" + dry_run_deck = output_directory / f"{dry_run_name}.DATA" + if dck.execution_mode in ("prep", "prep_deck", "all"): + if dck.write_explicit_solution: + output_types.append(".UNRST") + modified_deck = [] + with source_deck.open("r", encoding="utf8") as file_handle: + for csv_row in csv.reader(file_handle): + deck_line = str(csv_row)[2:-2].strip() + if deck_line == "SCHEDULE": + modified_deck.append(deck_line) + modified_deck.append("RPTRST\n'BASIC=2'/\n") + modified_deck.append("TSTEP\n1*0.0001/\n") + break + modified_deck.append(deck_line) + dry_run_deck.write_text( + "".join(f"{deck_line}\n" for deck_line in modified_deck), + encoding="utf8", + ) + pycopm_info( + f"temporary {cli_info_value(dry_run_deck.name)} created from " + f"{cli_info_value(str(source_deck))} for the initial run that generates " + "the grid (.EGRID), static (.INIT), and initial (.UNRST) properties." + ) + subprocess.run( + [ + dck.flow_command, + dry_run_deck.name, + "--output-mode=none", + "--parsing-strictness=low", + "--enable-opm-rst-file=1", + ], + cwd=output_directory, + check=True, + ) + dry_run_deck.unlink(missing_ok=True) + copy2(source_deck, dry_run_deck) + pycopm_info( + f"cloning {cli_info_value(str(source_deck))} to " + f"{cli_info_value(dry_run_deck.name)}." + ) + else: + copy2(source_deck, dry_run_deck) + pycopm_info( + f"cloning {cli_info_value(str(source_deck))} to " + f"{cli_info_value(dry_run_deck.name)} for the initial dry run that " + "generates the grid (.EGRID) and static (.INIT) properties." + ) + subprocess.run( + [dck.flow_command, dry_run_deck.name, *flags_dry_run.split()], + cwd=output_directory, + check=True, + ) + for output_type in output_types: + output_file = output_directory / f"{dry_run_name}{output_type}" + if not output_file.is_file(): + if output_type == ".INIT": + pycopm_error( + f"required file {cli_error_value(str(output_file))} was not found; " + f"add {cli_correct_value('INIT')} to the GRID section of " + f"{cli_error_value(f'{dck.input_deck_name}.DATA')}." + ) + elif output_type == ".EGRID": + pycopm_error( + f"required file {cli_error_value(str(output_file))} was not found; " + f"remove {cli_error_value('GRIDFILE')} from the GRID section of " + f"{cli_error_value(f'{dck.input_deck_name}.DATA')}." + ) + else: + pycopm_error( + f"required file {cli_error_value(str(output_file))} was not found; " + f"check input deck {cli_error_value(f'{dck.input_deck_name}.DATA')}." + ) + pycopm_success( + f"initial dry run succeeded; 3 files ({dry_run_name}.DATA, .EGRID, and .INIT)\n" + " written to ", + str(output_directory), + [], + ) + if dck.execution_mode in ("prep_deck", "deck", "deck_dry", "all"): + dck.original_deck_name = dck.input_deck_name + dck.input_deck_name = str(output_directory / dry_run_name) + for output_type in output_types: + output_file = Path(f"{dck.input_deck_name}{output_type}") + if not output_file.is_file(): + pycopm_error( + f"required file {cli_error_value(str(output_file))} was not found; " + f"run pycopm with {cli_correct_value('-m prep_deck')} and without " + f"{cli_error_value('-ijk')}." + ) + dck.props_keywords = ["permx", "permy", "permz", "poro"] + dck.base_keywords = dck.props_keywords + ["grid"] + if dck.refinement_enabled: + pycopm_info("generating the refined files, please wait...") + elif dck.vicinity_specification: + pycopm_info("generating the submodel files, please wait...") + elif dck.grid_transformation: + pycopm_info("generating the transformed files, please wait...") + else: + pycopm_info("generating the coarsened files, please wait...") + _initialize_deck_data(dck) + dck.original_cell_count = dck.original_nx * dck.original_ny * dck.original_nz + if dck.transmissibility_coarsening_method > 0: + dck.props_keywords.extend(("tranx", "trany", "tranz")) + dck.original_active_cell_mask = dck.original_porv > 0 + vicinity: VicinityMaps = cast(VicinityMaps, None) + refinement: RefinementMaps = cast(RefinementMaps, None) + coarsening: CoarseningMaps = cast(CoarseningMaps, None) + if dck.refinement_enabled: + refinement = create_refinement_maps(dck, cmdargs) + elif dck.vicinity_specification: + vicinity = create_vicinity_maps(dck) + elif dck.coarsening_enabled: + coarsening = create_coarsening_maps(dck, cmdargs) + if not dck.grid_transformation: + _create_index_mappings(dck, vicinity, refinement, coarsening) + if dck.requested_ijk[0]: + pycopm_success( + f"mapped indices: " + f"{cli_info_value(str(dck.original_to_output_i[dck.requested_ijk[0]]))}, " + f"{cli_info_value(str(dck.original_to_output_j[dck.requested_ijk[1]]))}, " + f"{cli_info_value(str(dck.original_to_output_k[dck.requested_ijk[2]]))}", + "", + [], + ) + sys.exit(0) + modified_deck, wellcind = process_deck(dck, vicinity) + pycopm_info("processing the mappings") + cr, zc = np.array([]), np.array([]) + if dck.grid_transformation: + generated_files.extend(transform_properties(dck, modified_deck)) + transform_grid(dck) + elif dck.refinement_enabled: + generated_files.extend(refine_properties(dck, refinement, modified_deck)) + refine_grid(dck, refinement) + elif dck.vicinity_specification: + generated_files.extend( + map_vicinity_properties(dck, vicinity, modified_deck) + ) + extract_vicinity_grid(dck, vicinity) + apply_boundary_pore_volume_correction(dck, vicinity) + write_porv(dck, modified_deck) + else: + cluster_minimum, cluster_maximum, removed_cells, file_names = ( + coarsen_properties( + dck, + coarsening, + modified_deck, + wellcind, + ) + ) + generated_files.extend(file_names) + if dck.pore_volume_correction == 1: + redistribute_removed_pore_volume( + dck, + coarsening.cell_groups, + cluster_minimum, + cluster_maximum, + removed_cells, + ) + write_porv(dck, modified_deck) + generated_files.append(f"{dck.include_prefix}PORV.INC") + cr, zc = coarsen_corner_point_grid(dck, coarsening) + generated_files.append(f"{dck.include_prefix}GRID.INC") + generated_deck = output_directory / f"{dck.output_deck_name}.DATA" + generated_deck.write_text( + "".join(f"{deck_line}\n" for deck_line in modified_deck), + encoding="utf8", + ) + if dck.correct_fluid_in_place == 1: + _correct_fluid_in_place(dck, modified_deck) + if ( + dck.coarsening_enabled + and dck.egrid_file.count("NNC1") + and dck.transmissibility_coarsening_method > 0 + ): + pycopm_info( + "calling OPM Flow for a dry run of the generated model, " + "needed for the NNC transmissibilities" + ) + subprocess.run( + [dck.flow_command, generated_deck.name, *flags_dry_run.split()], + cwd=output_directory, + check=False, + ) + generated_grid = output_directory / f"{dck.output_deck_name}.EGRID" + if ( + OpmFile(str(generated_grid)).count("NNC1") + or OpmFile(f"{dck.input_deck_name}.EGRID").count("NNC1") + ) and dck.transmissibility_coarsening_method > 0: + generated_files.extend(map_nnc_transmissibilities(dck, coarsening)) + else: + pycopm_warning("no NNC transmissibilities were found.") + generated_grid.unlink() + tmp = output_directory / f"{dck.output_deck_name}.INIT" + tmp.unlink() + if dck.coarsening_enabled: + if dck.dual_porosity_criterion: + cr, zc = build_dual_porosity_grid(dck, coarsening, cr, zc) + write_grid(dck, cr, zc, True) + write_dual_properties( + dck, + coarsening, + dck.output_nx * (2 * dck.output_ny + 1) * dck.output_nz, + modified_deck, + ) + grid_include_index = modified_deck.index( + f"'{dck.include_prefix}GRID.INC' /\n" + ) + modified_deck.insert( + grid_include_index + 1, + f"INCLUDE\n'{dck.include_prefix}NNC.INC' /\n", + ) + dimension_index = modified_deck.index( + f"{dck.output_nx} {dck.output_ny} {dck.output_nz} /" + ) + dual_ny = 2 * dck.output_ny + 1 + modified_deck[dimension_index] = ( + f"{dck.output_nx} {dual_ny} {dck.output_nz} /" + ) + generated_deck.write_text( + "".join(f"{deck_line}\n" for deck_line in modified_deck), + encoding="utf8", + ) + coarsening.nnc_text += "/\n" + write_include( + output_directory / f"{dck.include_prefix}NNC.INC", + "".join(coarsening.nnc_text), + ) + generated_files.append(f"{dck.include_prefix}NNC.INC") + elif coarsening.nnc_text != "NNC\n": + grid_include_index = modified_deck.index( + f"'{dck.include_prefix}GRID.INC' /\n" + ) + modified_deck.insert( + grid_include_index + 1, + f"INCLUDE\n'{dck.include_prefix}NNC.INC' /\n", + ) + generated_deck.write_text( + "".join(f"{deck_line}\n" for deck_line in modified_deck), + encoding="utf8", + ) + coarsening.nnc_text += "/\n" + write_include( + output_directory / f"{dck.include_prefix}NNC.INC", + "".join(coarsening.nnc_text), + ) + generated_files.append(f"{dck.include_prefix}NNC.INC") + generated_files.append(f"{dck.output_deck_name}.DATA") + pycopm_success( + "", + str(output_directory), + sorted(set(generated_files)), + ) + if dck.execution_mode in ("deck_dry", "dry", "all"): + pycopm_info("calling OPM Flow for a dry run of the generated model.") + completed_process = subprocess.run( + [dck.flow_command, f"{dck.output_deck_name}.DATA", *flags_dry_run.split()], + cwd=output_directory, + check=False, + ) + if completed_process.returncode != 0: + pycopm_error( + "the dry run of " + f"{cli_error_value(str(output_directory / (dck.output_deck_name + '.DATA')))} " + "failed. Check the OPM Flow output in the terminal. Correct the input " + f"deck {cli_error_value(str(source_deck))} or the generated deck; otherwise, " + "raise an issue at https://github.com/cssr-tools/pycopm/issues." + ) + else: + pycopm_success( + "dry-run results of the generated deck by pycopm were written to ", + str(output_directory), + [], + )
+ + + +
+[docs] +def _correct_fluid_in_place(dck: ConfigViaDeck, modified_deck: list[str]) -> None: + """Adjust output pore volume to match input oil and gas in place. + + Short Flow runs provide the fluid-in-place values used for two successive pore + volume corrections. + + Parameters + ---------- + dck + Deck configuration whose ``output_porv`` is updated. + modified_deck + Generated deck lines used to create the correction case.""" + output_directory = Path(dck.output_directory) + flags_one_step = ( + "--parsing-strictness=low --check-satfunc-consistency=false " + "--output-mode=none --solver-max-restarts=20 " + "--solver-continue-on-convergence-failure=true " + f"--output-dir={output_directory}" + ) + threshold = 1e-1 + deck_file = Path(f"{dck.input_deck_name}.DATA") + deck_lines = deck_file.read_text(encoding="utf8").splitlines() + schedule_index = deck_lines.index("SCHEDULE") + deck_lines = deck_lines[: schedule_index + 1] + [ + "TSTEP", + "0.01 /", + ] + restart_index = deck_lines.index("RPTRST") + restart_options = deck_lines[restart_index + 1].split("/")[0] + deck_lines[restart_index + 1] = f"{restart_options} FIP /" + one_step_deck = output_directory / f"{dck.output_deck_name}_1STEP.DATA" + correction_deck = output_directory / f"{dck.output_deck_name}_CORR.DATA" + one_step_deck.write_text( + "".join(f"{deck_line}\n" for deck_line in deck_lines), + encoding="utf8", + ) + schedule_index = modified_deck.index("SCHEDULE") + deckcorr = modified_deck[: schedule_index + 1] + [ + "TSTEP", + "0.01 /", + ] + restart_index = deckcorr.index("RPTRST") + restart_options = deckcorr[restart_index + 1].split("/")[0] + deckcorr[restart_index + 1] = f"{restart_options} FIP /" + correction_deck.write_text( + "".join(f"{deck_line}\n" for deck_line in deckcorr), + encoding="utf8", + ) + pycopm_info( + f"running {cli_info_value(str(one_step_deck))} and " + f"{cli_info_value(str(correction_deck))} to correct the pore volume." + ) + subprocess.run( + [dck.flow_command, str(correction_deck), *flags_one_step.split()], + check=False, + ) + subprocess.run( + [dck.flow_command, str(one_step_deck), *flags_one_step.split()], + check=False, + ) + reference_restart = OpmFile( + str(output_directory / f"{dck.output_deck_name}_1STEP.UNRST") + ) + corrected_restart = OpmFile( + str(output_directory / f"{dck.output_deck_name}_CORR.UNRST") + ) + corrected_init = OpmFile( + str(output_directory / f"{dck.output_deck_name}_CORR.INIT") + ) + reference_fip_gas = np.asarray(reference_restart["FIPGAS", 0]) + reference_fip_oil = np.asarray(reference_restart["FIPOIL", 0]) + corrected_porv = np.asarray(corrected_init["PORV"]) + corrected_fip_gas = np.asarray(corrected_restart["FIPGAS", 0]) + corrected_fip_oil = np.asarray(corrected_restart["FIPOIL", 0]) + active_porv = corrected_porv[corrected_porv > 0] + correction_factor = np.sum(reference_fip_oil) / np.sum(corrected_fip_oil) - 1 + low_oil_cells = corrected_fip_oil <= threshold + high_oil_cells = corrected_fip_oil > threshold + active_porv[low_oil_cells] -= ( + correction_factor + * np.sum(active_porv[high_oil_cells]) + / np.count_nonzero(low_oil_cells) + ) + active_porv[high_oil_cells] *= 1 + correction_factor + corrected_porv[corrected_porv > 0] = active_porv + corrected_porv[np.isnan(corrected_porv)] = 0 + dck.output_porv = corrected_porv + write_porv(dck, modified_deck) + subprocess.run( + [dck.flow_command, str(correction_deck), *flags_one_step.split()], + check=False, + ) + corrected_restart = OpmFile( + str(output_directory / f"{dck.output_deck_name}_CORR.UNRST") + ) + corrected_init = OpmFile( + str(output_directory / f"{dck.output_deck_name}_CORR.INIT") + ) + corrected_porv = np.asarray(corrected_init["PORV"]) + corrected_fip_gas = np.asarray(corrected_restart["FIPGAS", 0]) + corrected_fip_oil = np.asarray(corrected_restart["FIPOIL", 0]) + corrected_sgas = np.asarray(corrected_restart["SGAS", 0]) + active_porv = corrected_porv[corrected_porv > 0] + high_gas_cells = corrected_sgas > threshold + low_oil_cells = corrected_fip_oil <= threshold + correction_factor = ( + np.sum(reference_fip_gas) - np.sum(corrected_fip_gas) + ) / np.sum(corrected_fip_gas[high_gas_cells]) + active_porv[low_oil_cells] -= ( + correction_factor + * np.sum(active_porv[high_gas_cells]) + / np.count_nonzero(low_oil_cells) + ) + active_porv[high_gas_cells] *= 1 + correction_factor + corrected_porv[corrected_porv > 0] = active_porv + corrected_porv[np.isnan(corrected_porv)] = 0 + dck.output_porv = corrected_porv + write_porv(dck, modified_deck) + pycopm_info( + f"running {cli_info_value(str(correction_deck))} with the corrected pore volume." + ) + subprocess.run( + [dck.flow_command, str(correction_deck), *flags_one_step.split()], + check=False, + )
+ + + +
+[docs] +def _create_index_mappings( + dck: ConfigViaDeck, + vicinity: VicinityMaps, + refinement: RefinementMaps, + coarsening: CoarseningMaps, +) -> None: + """Create original-to-output mappings for each grid axis. + + Depending on the selected workflow, the mappings represent coarse cells, + submodel indices, or the first and last cells created by refinement. + + Parameters + ---------- + dck + Deck configuration updated with the index mappings. + vicinity + Vicinity bounds for submodel extraction. + refinement + Per-axis refinement values. + coarsening + Per-axis coarsening values.""" + if dck.refinement_enabled: + for index_name, direction, ref_dir in ( + ("i", "x", refinement.x), + ("j", "y", refinement.y), + ("k", "z", refinement.z), + ): + original_size = getattr(dck, f"original_n{direction}") + setattr( + dck, + f"original_to_output_{index_name}", + np.zeros(original_size + 1, dtype=int), + ) + setattr( + dck, + f"original_to_first_refined_{index_name}", + np.zeros(original_size + 1, dtype=int), + ) + setattr( + dck, + f"original_to_last_refined_{index_name}", + np.zeros(original_size + 1, dtype=int), + ) + next_index = 2 + for original_index in range( + getattr(dck, f"original_to_output_{index_name}").size - 1 + ): + midpoint_count = 1 + refinement_factor = int(ref_dir[original_index]) + for refinement_index in range(refinement_factor): + next_index += 1 + if refinement_index % 2 == 0: + midpoint_count += 1 + getattr(dck, f"original_to_output_{index_name}")[original_index + 1] = ( + next_index - midpoint_count + ) + getattr(dck, f"original_to_first_refined_{index_name}")[ + original_index + 1 + ] = ( + next_index - 2 * (midpoint_count - 1) - (refinement_factor + 1) % 2 + ) + value = next_index - 1 + getattr(dck, f"original_to_last_refined_{index_name}")[ + original_index + 1 + ] = value + next_index += 1 + elif dck.vicinity_specification: + for index_name, dimension_name, minimum_index, maximum_index in ( + ("i", "x", vicinity.min_i, vicinity.max_i), + ("j", "y", vicinity.min_j, vicinity.max_j), + ("k", "z", vicinity.min_k, vicinity.max_k), + ): + original_size = getattr(dck, f"original_n{dimension_name}") + setattr( + dck, + f"original_to_output_{index_name}", + np.zeros(original_size + 1, dtype=int), + ) + mapped_index = 1 + original_size = getattr(dck, f"original_to_output_{index_name}").size - 1 + for original_index in range(original_size): + if minimum_index <= original_index + 1 <= maximum_index: + getattr(dck, f"original_to_output_{index_name}")[ + original_index + 1 + ] = mapped_index + mapped_index += 1 + setattr(dck, f"output_n{dimension_name}", mapped_index - 1) + else: + for index_name, direction, coa_dir in ( + ("i", "x", coarsening.x), + ("j", "y", coarsening.y), + ("k", "z", coarsening.z), + ): + original_size = getattr(dck, f"original_n{direction}") + setattr( + dck, + f"original_to_output_{index_name}", + np.zeros(original_size + 1, dtype=int), + ) + mapped_index = 1 + for original_index in range(getattr(dck, f"original_n{direction}")): + source_index = original_index + 1 + if getattr(dck, f"original_to_output_{index_name}")[source_index] == 0: + getattr(dck, f"original_to_output_{index_name}")[ + source_index + ] = mapped_index + mapped_index += 1 + if coa_dir[source_index] > 1: + getattr(dck, f"original_to_output_{index_name}")[ + source_index + 1 + ] = getattr(dck, f"original_to_output_{index_name}")[source_index]
+ + + +
+[docs] +def _initialize_deck_data(dck: ConfigViaDeck) -> None: + """Load dry-run grid and property data into the deck configuration. + + The function opens EGRID, INIT, and optional restart files, determines grid + dimensions, and collects available property keywords. + + Parameters + ---------- + dck + Deck configuration updated with OPM files, dimensions, and keyword lists.""" + special_properties = [ + "swatinit", + "sowcr", + "sogcr", + "swcr", + "sgu", + "swl", + "krwr", + "krw", + "krorw", + "krorg", + "kro", + "krgr", + "krg", + ] + dck.egrid_file = OpmFile(f"{dck.input_deck_name}.EGRID") + dck.grid_model = OpmGrid(f"{dck.input_deck_name}.EGRID") + dck.init_file = OpmFile(f"{dck.input_deck_name}.INIT") + for property_name in special_properties: + if dck.init_file.count(property_name.upper()): + dck.props_keywords.append(property_name) + dck.special_keywords.append(property_name) + multipliers_names = ["multx", "multx-", "multy", "multy-", "multz", "multz-"] + maindeckmultflt, multipliers_values = find_multiplier_keywords(dck) + for mlt_val, mlt_name in zip(multipliers_values, multipliers_names): + keyword = mlt_name.upper() + if dck.init_file.count(keyword): + multiplier_deck = np.asarray(dck.init_file[keyword]) + if np.any(multiplier_deck != 1) and (mlt_val or not maindeckmultflt): + dck.props_keywords.append(mlt_name) + dck.multipliers_keywords.append(mlt_name) + for property_name in ("multnum", "fluxnum"): + keyword = property_name.upper() + if dck.init_file.count(keyword): + property_values = np.asarray(dck.init_file[keyword]) + if np.any(property_values != 1): + dck.grids_keywords.append(property_name) + for property_name in ("thconr", "disperc"): + if dck.init_file.count(property_name.upper()): + dck.grids_keywords.append(property_name) + for property_name in ( + "endnum", + "eqlnum", + "fipnum", + "imbnum", + "miscnum", + "opernum", + "pvtnum", + "rocknum", + "satnum", + ): + keyword = property_name.upper() + if dck.init_file.count(keyword): + property_values = np.asarray(dck.init_file[keyword]) + if np.any(property_values != 1): + dck.regions_keywords.append(property_name) + dck.original_nx, dck.original_ny, dck.original_nz = dck.grid_model.dimension + dck.output_nx, dck.output_ny, dck.output_nz = dck.grid_model.dimension + dck.original_porv = np.asarray(dck.init_file["PORV"]) + if dck.write_explicit_solution: + dck.restart_file = OpmFile(f"{dck.input_deck_name}.UNRST") + for property_name in ( + "sgas", + "soil", + "swat", + "rs", + "rv", + "rsw", + "rvw", + "pressure", + "sbiof", + "scalc", + "smicr", + "soxyg", + "surea", + "ssol", + "spoly", + "surf", + "saltp", + "salt", + ): + if dck.restart_file.count(property_name.upper()): + dck.solution_keywords.append(property_name)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/input_values.html b/docs/_modules/pycopm/utils/input_values.html new file mode 100644 index 0000000..6fd7448 --- /dev/null +++ b/docs/_modules/pycopm/utils/input_values.html @@ -0,0 +1,1526 @@ + + + + + + + + + + pycopm.utils.input_values — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.input_values

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0912,R0913,R0914,R0915,R0917,R1702
+"""Create configuration objects from command-line arguments and TOML files."""
+
+import argparse
+import datetime as dt
+import math
+import tomllib
+from pathlib import Path
+from typing import Any, TypeGuard
+
+import numpy as np
+from numpy.typing import NDArray
+from opm.io.ecl import EGrid as OpmGrid
+
+from pycopm.config.config import ConfigViaDeck, ConfigViaTOML
+from pycopm.utils.terminal import (
+    cli_correct_value,
+    cli_error_value,
+    cli_warning_value,
+    pycopm_error,
+    pycopm_warning,
+)
+
+TOML_KEYS = {
+    "flow_command",
+    "model_name",
+    "execution_mode",
+    "ensemble_size",
+    "max_parallel_realizations",
+    "max_realization_runtime_seconds",
+    "min_successful_realizations",
+    "random_seed",
+    "saturation_function_method",
+    "pore_volume_correction",
+    "initialization_method",
+    "observation_relative_errors",
+    "observation_minimum_errors",
+    "history_matching_end_date",
+    "ert_arguments",
+    "let_parameters",
+    "rock_property_settings",
+    "x_coarsening",
+    "y_coarsening",
+    "z_coarsening",
+    "satnum_generation_method",
+    "cleanup_file_suffixes",
+}
+
+INTERNAL_KEYS = {
+    "output_directory",
+    "resource_directory",
+    "reference_case_name",
+    "use_let_tables",
+    "significant_digits",
+    "original_nx",
+    "original_ny",
+    "original_nz",
+    "output_nx",
+    "output_ny",
+    "output_nz",
+    "original_cell_count",
+    "original_to_output_i",
+    "original_to_output_j",
+    "original_to_output_k",
+}
+
+MODEL_AXIS_LENGTHS = {
+    "norne": {"x_coarsening": 47, "y_coarsening": 113, "z_coarsening": 23},
+    "drogon": {"x_coarsening": 47, "y_coarsening": 74, "z_coarsening": 32},
+}
+
+MODEL_LET_NAMES = {
+    "norne": [
+        "lw",
+        "ew",
+        "tw",
+        "lo",
+        "eo",
+        "to",
+        "lg",
+        "eg",
+        "tg",
+        "log",
+        "eog",
+        "tog",
+        "lmlto",
+        "emlto",
+        "tmlto",
+        "lmltg",
+        "emltg",
+        "tmltg",
+    ],
+    "drogon": [
+        "lw",
+        "ew",
+        "tw",
+        "lo",
+        "eo",
+        "to",
+        "lg",
+        "eg",
+        "tg",
+        "log",
+        "eog",
+        "tog",
+        "lmlto",
+        "lmltg",
+        "kwow",
+        "kwoo",
+        "kwgw",
+        "kwgg",
+    ],
+}
+
+ROCK_PROPERTY_NAMES = {"PERMX", "PERMY", "PERMZ"}
+
+
+
+[docs] +def create_deck_config(cmdargs: argparse.Namespace) -> ConfigViaDeck: + """Create a deck configuration from parsed command arguments. + + Parameters + ---------- + cmdargs : argparse.Namespace + Command-line arguments for the deck-based workflow. + + Returns + ------- + ConfigViaDeck + Configuration populated from the command-line values.""" + return ConfigViaDeck( + output_directory=str(Path(cmdargs.output_directory).expanduser().resolve()), + flow_command=cmdargs.flow_command, + input_deck_name=Path(cmdargs.input_deck_path).stem, + input_deck_path=str(Path(cmdargs.input_deck_path).with_suffix("")), + active_cell_methods=cmdargs.active_cell_methods.split(","), + discrete_aggregation_method=cmdargs.discrete_aggregation_method.split(","), + continuous_aggregation_method=cmdargs.continuous_aggregation_method.split(","), + jump_thresholds=cmdargs.jump_thresholds.split(","), + output_deck_name=cmdargs.output_deck_name, + execution_mode=cmdargs.execution_mode, + include_prefix=cmdargs.include_prefix, + requested_ijk=[cmdargs.requested_ijk], + completion_removal_level=int(cmdargs.completion_removal_level), + deck_encoding=cmdargs.deck_encoding, + pore_volume_correction=int(cmdargs.pore_volume_correction), + correct_fluid_in_place=int(cmdargs.correct_fluid_in_place), + transmissibility_coarsening_method=int( + cmdargs.transmissibility_coarsening_method + ), + vicinity_specification=cmdargs.vicinity_specification, + grid_transformation=cmdargs.grid_transformation, + write_explicit_solution=int(cmdargs.write_explicit_solution) == 1, + dual_porosity_criterion=cmdargs.dual_porosity_criterion, + significant_digits=int(cmdargs.significant_digits), + refinement_enabled=bool( + cmdargs.x_refinement + or cmdargs.y_refinement + or cmdargs.z_refinement + or cmdargs.refinement + ), + coarsening_enabled=bool( + cmdargs.x_coarsening + or cmdargs.y_coarsening + or cmdargs.z_coarsening + or cmdargs.coarsening + ), + )
+ + + +
+[docs] +def parse_axis_modifications(uniform: str, localized: list) -> tuple[NDArray, list]: + """Parse uniform or axis-specific grid modifications. + + Uniform input contains one value for each axis. Axis-specific coarsening + also accepts one-based indices and inclusive ranges such as ``2:4,7``. + + Parameters + ---------- + uniform : str + Comma-separated x, y, and z modification values. + localized : list + Axis-specific specifications in x, y, and z order. + + Returns + ------- + cijk, axis_values + Uniform axis values and the three parsed axis-specific arrays. Only one + representation is populated.""" + if uniform: + cijk = np.fromstring(uniform, sep=",", dtype=int) + refs: list = [[], [], []] + else: + cijk = np.array([]) + refs = [] + for i in range(3): + argument = localized[i] + if argument: + if ":" in argument: + values = [0] + index = 1 + for value in argument.split(","): + entry = value.split(":") + start_index = int(entry[0]) + values.extend([0] * max(0, start_index - index)) + if len(entry) == 2: + end_index = int(entry[1]) + values.extend([2] * max(0, end_index - start_index)) + index = end_index + else: + index = start_index + values.append(0) + refs.append(values) + else: + refs.append( + list(np.fromstring(argument, sep=",", dtype=int).tolist()) + ) + else: + refs.append([]) + return cijk, refs
+ + + +
+[docs] +def _is_finite_number(value: Any) -> TypeGuard[int | float]: + """Check whether a value is a finite non-Boolean number. + + Parameters + ---------- + value : Any + Value to inspect. + + Returns + ------- + bool + Whether the value is a finite integer or floating-point number.""" + return ( + isinstance(value, (int, float)) + and not isinstance(value, bool) + and math.isfinite(value) + )
+ + + +
+[docs] +def _is_integer(value: Any) -> TypeGuard[int]: + """Check whether a value is a non-Boolean integer. + + Parameters + ---------- + value : Any + Value to inspect. + + Returns + ------- + bool + Whether the value is an integer and not a Boolean.""" + return isinstance(value, int) and not isinstance(value, bool)
+ + + +
+[docs] +def _add_validation_error(errors: list[str], message: str) -> None: + """Add a TOML validation error. + + Parameters + ---------- + errors : list[str] + Validation messages collected during the current validation pass. + message : str + Human-readable validation message.""" + errors.append(message)
+ + + +
+[docs] +def _warn(message: str) -> None: + """Display a TOML validation warning. + + Parameters + ---------- + message : str + Human-readable warning passed to the shared terminal helper.""" + pycopm_warning(message)
+ + + +
+[docs] +def _validate_string(cfg_file: dict[str, Any], key: str, errors: list[str]) -> bool: + """Check that a TOML variable is a non-empty string. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + key : str + Configuration variable name. + errors : list[str] + Validation messages collected during the current validation pass. + + Returns + ------- + bool + Whether the variable is present and contains a non-empty string.""" + if key not in cfg_file: + return False + value = cfg_file[key] + if not isinstance(value, str) or not value.strip(): + _add_validation_error( + errors, + f"variable {cli_error_value(key)} has invalid value " + f"{cli_error_value(str(value))}, expected " + f"{cli_correct_value('a non-empty string')}.", + ) + return False + return True
+ + + +
+[docs] +def _validate_integer( + cfg_file: dict[str, Any], key: str, errors: list[str], minimum: int = 0 +) -> bool: + """Check that a TOML variable is an integer within its lower bound. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + key : str + Configuration variable name. + errors : list[str] + Validation messages collected during the current validation pass. + minimum : int, optional + Inclusive lower bound for accepted values. + + Returns + ------- + bool + Whether the variable is present and satisfies the integer constraint.""" + if key not in cfg_file: + return False + value = cfg_file[key] + if not _is_integer(value) or value < minimum: + _add_validation_error( + errors, + f"variable {cli_error_value(key)} has invalid value " + f"{cli_error_value(str(value))}, expected an integer " + f"greater than or equal to {minimum}.", + ) + return False + return True
+ + + +
+[docs] +def _validate_number_array( + cfg_file: dict[str, Any], + key: str, + errors: list[str], + *, + length: int | None = None, + minimum: float | None = None, + maximum: float | None = None, +) -> bool: + """Check the shape and values of a numeric TOML array. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + key : str + Configuration variable name. + errors : list[str] + Validation messages collected during the current validation pass. + length : int | None, optional + Required number of entries. + minimum : float | None, optional + Inclusive lower bound for every entry. + maximum : float | None, optional + Inclusive upper bound for every entry. + + Returns + ------- + bool + Whether the variable is present and satisfies all array constraints.""" + if key not in cfg_file: + return False + value = cfg_file[key] + if not isinstance(value, list): + _add_validation_error(errors, f"variable '{key}' must be an array.") + return False + valid = True + if length is not None and len(value) != length: + _add_validation_error( + errors, f"variable '{key}' has {len(value)} entries, expected {length}." + ) + valid = False + for index, entry in enumerate(value): + if not _is_finite_number(entry): + _add_validation_error( + errors, + f"variable {cli_error_value(f'{key}[{index}]')} has invalid value " + f"{cli_error_value(str(entry))}, expected " + f"{cli_correct_value('a finite number')}.", + ) + valid = False + elif minimum is not None and entry < minimum: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{key}[{index}]')} has invalid value " + f"{cli_error_value(str(entry))}, expected a value greater than or " + f"equal to {cli_correct_value(str(minimum))}.", + ) + valid = False + elif maximum is not None and entry > maximum: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{key}[{index}]')} has invalid value " + f"{cli_error_value(str(entry))}, expected a value less than or " + f"equal to {cli_correct_value(str(maximum))}.", + ) + valid = False + return valid
+ + + +
+[docs] +def _validate_coarsening( + cfg_file: dict[str, Any], key: str, expected_length: int, errors: list[str] +) -> None: + """Validate one model-specific axis coarsening array. + + The array must contain non-negative integers, match the number of grid + boundaries for the selected model, and retain both outer boundaries. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + key : str + Name of the x, y, or z coarsening variable. + expected_length : int + Required number of entries for the selected reference model. + errors : list[str] + Validation messages collected during the current validation pass.""" + if key not in cfg_file: + return + values = cfg_file[key] + if not isinstance(values, list): + _add_validation_error(errors, f"variable '{key}' must be an array.") + return + if len(values) != expected_length: + _add_validation_error( + errors, + f"variable '{key}' has {len(values)} entries, expected {expected_length}.", + ) + for index, value in enumerate(values): + if not _is_integer(value) or value < 0: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{key}[{index}]')} has invalid value " + f"{cli_error_value(str(value))}, " + "expected a non-negative integer.", + ) + if values and values[0] != 0: + _add_validation_error( + errors, f"variable '{key}[0]' must be 0 to retain the first grid boundary." + ) + if values and values[-1] != 0: + _add_validation_error( + errors, f"variable '{key}[-1]' must be 0 to retain the last grid boundary." + )
+ + + +
+[docs] +def _validate_let_parameters( + cfg_file: dict[str, Any], model: str | None, errors: list[str] +) -> None: + """Validate the ordered LET-parameter definitions. + + Each row contains a coefficient name, initial value, estimation flag, + distribution name, lower bound, and upper bound. The order is checked because + downstream table generation addresses coefficients by position. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + model : str | None + Normalized reference-model name, when valid. + errors : list[str] + Validation messages collected during the current validation pass.""" + if "let_parameters" not in cfg_file: + return + rows = cfg_file["let_parameters"] + if not isinstance(rows, list): + _add_validation_error( + errors, "variable 'let_parameters' must be an array of arrays." + ) + return + expected_names = MODEL_LET_NAMES.get(model) if model is not None else None + if expected_names and len(rows) != len(expected_names): + _add_validation_error( + errors, + f"variable 'let_parameters' has {len(rows)} rows, expected " + f"{len(expected_names)} for model '{model}'.", + ) + seen: set[str] = set() + for index, row in enumerate(rows): + name = f"let_parameters[{index}]" + if not isinstance(row, list) or len(row) != 6: + size = len(row) if isinstance(row, list) else type(row).__name__ + _add_validation_error( + errors, + f"variable {cli_error_value(name)} has invalid shape " + f"{cli_error_value(str(size))}, expected " + f"{cli_correct_value('6 entries')}.", + ) + continue + coefficient, initial, estimated, distribution, lower, upper = row + if not isinstance(coefficient, str) or not coefficient.strip(): + _add_validation_error( + errors, f"variable '{name}[0]' must be a non-empty coefficient name." + ) + elif coefficient in seen: + _add_validation_error( + errors, f"duplicate LET-parameter name '{coefficient}'." + ) + else: + seen.add(coefficient) + if ( + expected_names + and index < len(expected_names) + and coefficient != expected_names[index] + ): + _add_validation_error( + errors, + f"variable {cli_error_value(f'{name}[0]')} has invalid value " + f"{cli_error_value(str(coefficient))}, expected " + f"{cli_correct_value(expected_names[index])} at this position.", + ) + if not _is_finite_number(initial): + _add_validation_error( + errors, f"variable '{name}[1]' must be a finite number." + ) + if not _is_integer(estimated) or estimated not in {0, 1}: + _add_validation_error(errors, f"variable '{name}[2]' must be 0 or 1.") + if not isinstance(distribution, str) or not distribution.strip(): + _add_validation_error( + errors, f"variable '{name}[3]' must be a non-empty distribution name." + ) + if not _is_finite_number(lower) or not _is_finite_number(upper): + _add_validation_error( + errors, f"variables '{name}[4:6]' must be finite numbers." + ) + elif lower >= upper: + _add_validation_error( + errors, + f"variable {cli_error_value(name)} has invalid uniform bounds " + f"{cli_error_value(str([lower, upper]))}, expected the lower " + f"bound to be less than {cli_correct_value('the upper bound')}.", + ) + elif ( + isinstance(distribution, str) + and distribution.lower() == "uniform" + and _is_finite_number(initial) + and not lower <= initial <= upper + ): + _add_validation_error( + errors, + f"variable {cli_error_value(f'{name}[1]')} has invalid value " + f"{cli_error_value(str(initial))}, expected a value between " + f"{cli_correct_value(str(lower))} and " + f"{cli_correct_value(str(upper))} for the uniform distribution.", + )
+ + + +
+[docs] +def _validate_rock_properties(cfg_file: dict[str, Any], errors: list[str]) -> None: + """Validate rock-property history matching settings. + + Each row contains a permeability name, estimation flag, and aggregation + method. Property names are normalized to uppercase after validation. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + errors : list[str] + Validation messages collected during the current validation pass.""" + if "rock_property_settings" not in cfg_file: + return + rows = cfg_file["rock_property_settings"] + if not isinstance(rows, list): + _add_validation_error( + errors, "variable 'rock_property_settings' must be an array of arrays." + ) + return + seen: set[str] = set() + for index, row in enumerate(rows): + name = f"rock_property_settings[{index}]" + if not isinstance(row, list) or len(row) != 3: + _add_validation_error( + errors, + f"variable '{name}' must contain name, estimation flag, and aggregation method.", + ) + continue + prop, estimated, method = row + if not isinstance(prop, str) or prop.upper() not in ROCK_PROPERTY_NAMES: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{name}[0]')} has invalid value " + f"{cli_error_value(str(prop))}, expected " + "PERMX, PERMY, or PERMZ.", + ) + elif prop.upper() in seen: + _add_validation_error(errors, f"duplicate rock property '{prop.upper()}'.") + else: + seen.add(prop.upper()) + row[0] = prop.upper() + if not _is_integer(estimated) or estimated not in {0, 1}: + _add_validation_error(errors, f"variable '{name}[1]' must be 0 or 1.") + if method not in {"max", "mean"}: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{name}[2]')} has invalid value " + f"{cli_error_value(str(method))}, expected " + f"{cli_correct_value('max')} or {cli_correct_value('mean')}.", + ) + missing = ROCK_PROPERTY_NAMES - seen + if missing: + _add_validation_error( + errors, f"missing rock property settings for {', '.join(sorted(missing))}." + )
+ + + +
+[docs] +def _validate_toml(cfg_file: dict[str, Any]) -> dict[str, Any]: + """Validate and normalize TOML configuration values. + + Unknown and internally managed variables are reported and removed. Remaining + errors are collected so the user receives one complete validation report. + + Parameters + ---------- + cfg_file : dict[str, Any] + Raw values loaded from the TOML configuration. + + Returns + ------- + dict[str, Any] + Validated and normalized values suitable for ``ConfigViaTOML``. + + Raises + ------ + SystemExit + If one or more configuration values are invalid.""" + if not isinstance(cfg_file, dict): + pycopm_error( + f"invalid TOML content {cli_error_value(type(cfg_file).__name__)}, " + f"expected {cli_correct_value('a dictionary of configuration variables')}." + ) + cfg_file = cfg_file.copy() + errors: list[str] = [] + + internal = sorted(INTERNAL_KEYS & cfg_file.keys()) + if internal: + formatted = ", ".join(cli_warning_value(key) for key in internal) + plural = len(internal) != 1 + pycopm_warning( + f"variable{'s' if plural else ''} {formatted} " + f"{'are' if plural else 'is'} managed internally and will be ignored." + ) + for key in internal: + cfg_file.pop(key) + unknown = sorted(cfg_file.keys() - TOML_KEYS) + if unknown: + formatted = ", ".join(cli_warning_value(key) for key in unknown) + plural = len(unknown) != 1 + pycopm_warning( + f"unknown TOML variable{'s' if plural else ''} {formatted} will be ignored." + ) + for key in unknown: + cfg_file.pop(key) + + required = set(TOML_KEYS) - {"satnum_generation_method"} + for key in sorted(required - cfg_file.keys()): + _add_validation_error(errors, f"missing required TOML variable '{key}'.") + + _validate_string(cfg_file, "flow_command", errors) + if _validate_string(cfg_file, "model_name", errors): + model = cfg_file["model_name"].lower() + cfg_file["model_name"] = model + if model not in MODEL_AXIS_LENGTHS: + _add_validation_error( + errors, + f"variable {cli_error_value('model_name')} has invalid value " + f"{cli_error_value(str(model))}, expected " + f"{cli_correct_value('norne')} or {cli_correct_value('drogon')}.", + ) + else: + model = None + + if _validate_string(cfg_file, "execution_mode", errors): + mode = cfg_file["execution_mode"].lower() + cfg_file["execution_mode"] = mode + if mode not in {"single-run", "files", "ert"}: + _add_validation_error( + errors, + f"variable {cli_error_value('execution_mode')} has invalid value " + f"{cli_error_value(str(mode))}, " + "expected 'single-run', 'files', or 'ert'.", + ) + else: + mode = None + + for key, minimum in ( + ("ensemble_size", 1), + ("max_parallel_realizations", 1), + ("max_realization_runtime_seconds", 0), + ("min_successful_realizations", 1), + ("random_seed", 0), + ): + _validate_integer(cfg_file, key, errors, minimum) + + ensemble = cfg_file.get("ensemble_size") + parallel = cfg_file.get("max_parallel_realizations") + successful = cfg_file.get("min_successful_realizations") + if _is_integer(ensemble): + if _is_integer(parallel) and parallel > ensemble: + _add_validation_error( + errors, + "variable 'max_parallel_realizations' cannot exceed 'ensemble_size'.", + ) + if _is_integer(successful) and successful > ensemble: + _add_validation_error( + errors, + "variable 'min_successful_realizations' cannot exceed 'ensemble_size'.", + ) + + for key, choices in ( + ("saturation_function_method", {0, 1}), + ("pore_volume_correction", {0, 1}), + ("initialization_method", {0, 1}), + ): + if _validate_integer(cfg_file, key, errors) and cfg_file[key] not in choices: + _add_validation_error( + errors, + f"variable {cli_error_value(key)} has invalid value " + f"{cli_error_value(str(cfg_file[key]))}, " + f"expected one of {sorted(choices)}.", + ) + + if "satnum_generation_method" in cfg_file: + if _validate_integer(cfg_file, "satnum_generation_method", errors) and cfg_file[ + "satnum_generation_method" + ] not in {0, 1, 2}: + _add_validation_error( + errors, "variable 'satnum_generation_method' must be 0, 1, or 2." + ) + if model == "drogon": + pycopm_warning( + f"variable {cli_warning_value('satnum_generation_method')} is only " + f"effective for {cli_correct_value('model_name = norne')} and will be ignored." + ) + cfg_file.pop("satnum_generation_method", None) + elif model == "norne": + cfg_file["satnum_generation_method"] = 0 + + _validate_number_array( + cfg_file, "observation_relative_errors", errors, length=3, minimum=0, maximum=1 + ) + _validate_number_array( + cfg_file, "observation_minimum_errors", errors, length=3, minimum=0 + ) + + if "history_matching_end_date" in cfg_file: + value = cfg_file["history_matching_end_date"] + if isinstance(value, dt.datetime): + cfg_file["history_matching_end_date"] = value.date().isoformat() + elif isinstance(value, dt.date): + cfg_file["history_matching_end_date"] = value.isoformat() + elif isinstance(value, str): + try: + cfg_file["history_matching_end_date"] = dt.date.fromisoformat( + value + ).isoformat() + except ValueError: + _add_validation_error( + errors, + "variable 'history_matching_end_date' must be an ISO date in " + "YYYY-MM-DD format.", + ) + else: + _add_validation_error( + errors, + "variable 'history_matching_end_date' must be a TOML date or ISO date string.", + ) + + _validate_string(cfg_file, "ert_arguments", errors) + if mode != "ert" and "ert_arguments" in cfg_file: + pycopm_warning( + f"variable {cli_warning_value('ert_arguments')} is not executed for " + f"{cli_warning_value(f'execution_mode = {mode}')}, but is retained for " + "generated ERT files." + ) + + if model in MODEL_AXIS_LENGTHS: + for key, length in MODEL_AXIS_LENGTHS[model].items(): + _validate_coarsening(cfg_file, key, length, errors) + + if "cleanup_file_suffixes" in cfg_file: + suffixes = cfg_file["cleanup_file_suffixes"] + if not isinstance(suffixes, list): + _add_validation_error( + errors, "variable 'cleanup_file_suffixes' must be an array of strings." + ) + else: + for index, suffix in enumerate(suffixes): + if not isinstance(suffix, str) or not suffix.strip(): + _add_validation_error( + errors, + f"variable 'cleanup_file_suffixes[{index}]' must be a non-empty string.", + ) + elif any(character in suffix for character in "'\"/*?[]"): + _add_validation_error( + errors, + f"variable 'cleanup_file_suffixes[{index}]' contains unsafe " + "filename-pattern characters.", + ) + + _validate_let_parameters(cfg_file, model, errors) + _validate_rock_properties(cfg_file, errors) + + if cfg_file.get("saturation_function_method") == 1 and not cfg_file.get( + "let_parameters" + ): + _add_validation_error( + errors, "LET saturation functions require a non-empty 'let_parameters'." + ) + + estimated_let = any( + isinstance(row, list) and len(row) >= 3 and row[2] == 1 + for row in cfg_file.get("let_parameters", []) + ) + estimated_rock = any( + isinstance(row, list) and len(row) >= 2 and row[1] == 1 + for row in cfg_file.get("rock_property_settings", []) + ) + if mode == "ert" and not (estimated_let or estimated_rock): + _add_validation_error( + errors, + "execution_mode 'ert' requires at least one estimated LET or rock-property parameter.", + ) + + if errors: + details = "\n".join(f" - {error}" for error in errors) + pycopm_error(f"invalid TOML configuration:\n{details}") + return cfg_file
+ + + +
+[docs] +def load_toml_config( + input_file: str, + output_directory: str, + resource_directory: str, + significant_digits: int, +) -> ConfigViaTOML: + """Load, validate, and initialize a TOML configuration. + + Validation and normalization occur before the reference EGRID is opened and + before ``ConfigViaTOML`` is constructed. + + Parameters + ---------- + input_file : str + TOML configuration path. + output_directory : str + Generated-project directory. + resource_directory : str + Directory containing templates and reference simulations. + significant_digits : int + Precision used when writing floating-point values. + + Returns + ------- + ConfigViaTOML + Validated configuration populated with reference-grid metadata. + + Raises + ------ + SystemExit + If the TOML configuration is invalid.""" + with open(input_file, "rb") as file_handle: + cfg_file = _validate_toml(tomllib.load(file_handle)) + + suffixes = cfg_file["cleanup_file_suffixes"] + cfg_file["cleanup_file_suffixes"] = ",".join(f"'{suffix}'" for suffix in suffixes) + for key in ("x_coarsening", "y_coarsening", "z_coarsening"): + cfg_file[key] = np.asarray(cfg_file[key], dtype=int) + + name = "NORNE_ATW2013" if cfg_file["model_name"] == "norne" else "DROGON" + case_path = ( + Path(resource_directory) + / "reference_simulation" + / cfg_file["model_name"] + / name + ) + grid = OpmGrid(f"{case_path}.EGRID") + cfg = ConfigViaTOML( + output_directory=output_directory, + resource_directory=resource_directory, + significant_digits=significant_digits, + reference_case_name=name, + original_nx=grid.dimension[0], + original_ny=grid.dimension[1], + original_nz=grid.dimension[2], + original_cell_count=int(np.prod(grid.dimension)), + **cfg_file, + ) + return cfg
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/parser_deck.html b/docs/_modules/pycopm/utils/parser_deck.html new file mode 100644 index 0000000..efe2e90 --- /dev/null +++ b/docs/_modules/pycopm/utils/parser_deck.html @@ -0,0 +1,2441 @@ + + + + + + + + + + pycopm.utils.parser_deck — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.parser_deck

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0902,R0912,R0913,R0914,R0915,C0302,R0917,R1702,R0916,R0911,R1705
+
+"""Parse an OPM deck and update records for the modified grid."""
+
+import csv
+import os
+import sys
+from dataclasses import dataclass, field
+from pathlib import Path
+
+import numpy as np
+from numpy.typing import NDArray
+
+from pycopm.config.config import ConfigViaDeck
+from pycopm.utils.vicinity import VicinityMaps
+
+csv.field_size_limit(sys.maxsize)
+
+
+
+[docs] +@dataclass(slots=True) +class _ParserState: + """Store temporary state while parsing an OPM deck. + + The Boolean fields indicate active keyword blocks. The list fields track wells, + completions, and segmented-well records retained in the generated deck.""" + + dimens: bool = False + grid: bool = False + welspecs: bool = False + welsegs: bool = False + complump: bool = False + compdat: bool = False + compsegs: bool = False + mapaxes: bool = False + multregt: bool = False + process_edit: bool = False + editnnc: bool = False + multiply: bool = False + props: bool = False + operation: bool = False + regions: bool = False + equil: bool = False + faults: bool = False + multflt: bool = False + welldims: bool = False + skip_block: bool = False + aqucon: bool = False + aqunum: bool = False + aquancon: bool = False + bccon: bool = False + bwpr: bool = False + source: bool = False + pinch: bool = False + has_edit: bool = False + + previous_completion: list[str] = field(default_factory=list) + compsegs_wells: list[str] = field(default_factory=list) + retained_wells: list[str] = field(default_factory=list) + completion_wells: list[str] = field(default_factory=list) + segmented_wells: list[str] = field(default_factory=list) + + separator: str = "" + schedule_keyword: str = ""
+ + + +_SCHEDULE_KEYWORDS = frozenset( + { + "wconhist", + "wdfac", + "weltarg", + "wrftplt", + "compord", + "wtracer", + "wconinjh", + "wconinje", + "wconprod", + "wtest", + "welopen", + "wsegvalv", + "wecon", + "cskin", + "wpavedep", + } +) + + +
+[docs] +def process_deck( + dck: ConfigViaDeck, vicinity: VicinityMaps +) -> tuple[list[str], list[int]]: + """Rewrite deck records for the modified grid. + + The parser updates dimensions, properties, grid-index ranges, wells, aquifers, + faults, and selected schedule records. + + Parameters + ---------- + dck + Deck configuration and axis index mappings. + vicinity + Vicinity selection used when extracting a submodel. + + Returns + ------- + modified_deck, well_cell_indices + Rewritten deck lines and coarse cells containing well completions.""" + modified_deck: list[str] = [] + wellcind: list[int] = [] + kwr = _ParserState() + hvicinity = bool(vicinity and vicinity.shape) + if dck.refinement_enabled: + _collect_segmented_well_names(dck, kwr) + elif dck.vicinity_specification: + _collect_vicinity_well_names(dck, kwr, vicinity) + deck_path = Path(f"{dck.input_deck_name}.DATA") + with deck_path.open("r", encoding=dck.deck_encoding) as deck_file: + for row in csv.reader(deck_file): + parsed_line = str(row)[2:-2].strip() + parsed_line = parsed_line.replace("\\t", " ") + parsed_line = parsed_line.replace("', '", ",") + parsed_line = parsed_line.replace("-- Generated : Petrel", "") + parsed_line = parsed_line.strip() + if not kwr.separator and parsed_line.count("-") > 70: + kwr.separator = parsed_line + if _handle_dimens(dck, kwr, modified_deck, parsed_line): + continue + if _handle_welldims(dck, kwr, modified_deck, parsed_line): + continue + if _handle_grid_props(dck, kwr, modified_deck, parsed_line): + continue + if _handle_props(dck, vicinity, kwr, modified_deck, parsed_line): + continue + if _handle_regions(dck, kwr, modified_deck, parsed_line): + continue + if _handle_equil(dck, kwr, modified_deck, parsed_line): + continue + if not dck.grid_transformation: + if _handle_bwpr(dck, kwr, modified_deck, parsed_line): + continue + if _handle_wells(dck, kwr, modified_deck, parsed_line, hvicinity): + continue + if _handle_source(dck, kwr, modified_deck, parsed_line): + continue + if _handle_aquancon(dck, kwr, modified_deck, parsed_line): + continue + if dck.vicinity_specification: + if _handle_welsegs(kwr, modified_deck, parsed_line): + continue + if __handle_schedule_keyword(kwr, modified_deck, parsed_line): + continue + if _handle_segmented_wells( + dck, kwr, modified_deck, parsed_line, wellcind + ): + continue + if modified_deck: + for special_name in dck.special_keywords: + if ( + f"{special_name}." in parsed_line.lower() + or f".{special_name}" in parsed_line.lower() + ) or ( + modified_deck[-1] == "INCLUDE" + and special_name in parsed_line.lower() + ): + parsed_line = ( + f"{dck.include_prefix}{special_name.upper()}.INC /" + ) + modified_deck.append(parsed_line) + if ( + len(modified_deck) > 1 + and modified_deck[-2] == "INCLUDE" + and _include_contains_endbox(dck, parsed_line) + ): + modified_deck[-2] = "--" + modified_deck[-2] + modified_deck[-1] = "--" + modified_deck[-1] + return modified_deck, wellcind
+ + + +
+[docs] +def _include_contains_endbox(dck: ConfigViaDeck, nrwo: str) -> bool: + """Return whether an included file contains ENDBOX.""" + include_text = nrwo + if "--" in include_text: + include_text = include_text.split("--", maxsplit=1)[0] + include_text = include_text.replace(" /", "") + include_text = include_text.rstrip("/").strip().strip("'\"") + deck_path = Path(f"{dck.input_deck_name}.DATA").absolute() + include_path = (deck_path.parent / include_text).absolute() + if not include_path.exists(): + return False + with include_path.open("r", encoding=dck.deck_encoding) as include_file: + for row in csv.reader(include_file): + parsed_line = str(row)[2:-2].strip() + if parsed_line == "ENDBOX": + return True + return False
+ + + +
+[docs] +def __handle_schedule_keyword( + kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Filter supported schedule records by wells retained in the submodel.""" + keyword_name = nrwo.lower() + if keyword_name in _SCHEDULE_KEYWORDS: + kwr.schedule_keyword = keyword_name + modified_deck.append(nrwo) + return True + if not kwr.schedule_keyword: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + kwr.schedule_keyword = "" + if len(tokens) <= 1: + return False + if tokens[0].startswith("--"): + return True + well_name = tokens[0].replace("'", "") + if kwr.schedule_keyword == "wsegvalv" and well_name in kwr.segmented_wells: + return True + if well_name.endswith("*"): + well_prefix = well_name[:-1] + for retained_well in kwr.retained_wells: + if retained_well.startswith(well_prefix): + return False + return well_name not in kwr.retained_wells
+ + + +
+[docs] +def _collect_vicinity_well_names( + dck: ConfigViaDeck, kwr: _ParserState, vicinity: VicinityMaps +) -> None: + """Identify wells and segmented wells retained in the vicinity model.""" + segmented_well = "" + deck_path = Path(f"{dck.input_deck_name}.DATA") + with deck_path.open("r", encoding=dck.deck_encoding) as deck_file: + for row in csv.reader(deck_file): + parsed_line = str(row)[2:-2].strip() + if parsed_line == "COMPDAT": + kwr.compdat = True + continue + if kwr.compdat: + tokens = parsed_line.split() + if tokens: + if tokens[0] == "/": + kwr.previous_completion = [] + kwr.compdat = False + well_name = tokens[0].replace("'", "") + if well_name in kwr.retained_wells: + continue + if len(tokens) > 4 and not tokens[0].startswith("--"): + if well_name not in kwr.completion_wells: + kwr.completion_wells.append(well_name) + source_i = int(tokens[1]) + source_j = int(tokens[2]) + source_k1 = int(tokens[3]) + source_k2 = int(tokens[4]) + if ( + dck.original_to_output_i[source_i] + * dck.original_to_output_j[source_j] + * dck.original_to_output_k[source_k1] + * dck.original_to_output_k[source_k2] + > 0 + ): + kwr.retained_wells.append(well_name) + if parsed_line == "COMPSEGS": + kwr.compsegs = True + continue + if kwr.compsegs: + tokens = parsed_line.split() + if tokens and tokens[0] == "/": + kwr.compsegs = False + if len(tokens) > 1 and not tokens[0].startswith("--"): + well_name = tokens[0].replace("'", "") + if well_name in kwr.completion_wells: + segmented_well = well_name + elif len(tokens) > 2: + source_i = int(tokens[0]) + source_j = int(tokens[1]) + source_k = int(tokens[2]) + if ( + dck.original_to_output_i[source_i] + * dck.original_to_output_j[source_j] + * dck.original_to_output_k[source_k] + != 0 + and segmented_well not in kwr.segmented_wells + ): + kwr.segmented_wells.append(segmented_well) + if vicinity and vicinity.selector: + selected_well_names = {vicinity.selector, f"'{vicinity.selector}'"} + kwr.segmented_wells = [ + well_name.replace("'", "") + for well_name in kwr.segmented_wells + if well_name in selected_well_names + ] + kwr.retained_wells = [ + well_name.replace("'", "") + for well_name in kwr.retained_wells + if well_name in selected_well_names + ] + kwr.completion_wells = [ + well_name.replace("'", "") + for well_name in kwr.completion_wells + if well_name in selected_well_names + ]
+ + + +
+[docs] +def _collect_segmented_well_names(dck: ConfigViaDeck, kwr: _ParserState) -> None: + """Identify wells requiring segmented-well completion handling.""" + deck_path = Path(f"{dck.input_deck_name}.DATA") + with deck_path.open("r", encoding=dck.deck_encoding) as deck_file: + for row in csv.reader(deck_file): + parsed_line = str(row)[2:-2].strip() + if parsed_line == "COMPDAT": + kwr.compdat = True + continue + if kwr.compdat: + tokens = parsed_line.split() + if tokens: + if tokens[0] == "/": + kwr.compdat = False + well_name = tokens[0].replace("'", "") + if well_name in kwr.segmented_wells: + continue + if len(tokens) > 2 and not tokens[0].startswith("--"): + if not kwr.previous_completion: + kwr.previous_completion = tokens + else: + previous_well = kwr.previous_completion[0].replace("'", "") + changed_column = ( + tokens[1] != kwr.previous_completion[1] + or tokens[2] != kwr.previous_completion[2] + ) + if changed_column and well_name == previous_well: + kwr.segmented_wells.append(well_name) + kwr.previous_completion = tokens + if parsed_line == "COMPSEGS": + kwr.compsegs = True + continue + if kwr.compsegs: + tokens = parsed_line.split() + if len(tokens) > 1 and not tokens[0].startswith("--"): + well_name = tokens[0].replace("'", "") + kwr.compsegs_wells.append(well_name) + kwr.compsegs = False
+ + + +
+[docs] +def _handle_dimens( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Replace the original DIMENS values with the modified grid dimensions.""" + if nrwo == "DIMENS": + kwr.dimens = True + modified_deck.append(nrwo) + modified_deck.append(f"{dck.output_nx} {dck.output_ny} {dck.output_nz} /") + return True + + if not kwr.dimens: + return False + + tokens = nrwo.split() + if ( + tokens + and not tokens[0].startswith("--") + and (tokens[-1] == "/" or tokens[0] == "/") + ): + kwr.dimens = False + return True
+ + + +
+[docs] +def _handle_welldims( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update WELLDIMS for a refined grid.""" + if not dck.refinement_enabled: + return False + if nrwo == "WELLDIMS": + kwr.welldims = True + modified_deck.append(nrwo) + return True + if not kwr.welldims: + return False + tokens = nrwo.split() + if tokens and not tokens[0].startswith("--"): + if len(tokens) > 2: + tokens[1] = str(dck.output_nx + dck.output_ny + dck.output_nz) + modified_deck.append(" ".join(tokens)) + if "/" in nrwo: + kwr.welldims = False + if tokens[0] == "/": + modified_deck.append(nrwo) + kwr.welldims = False + return True
+ + + +
+[docs] +def _handle_props( + dck: ConfigViaDeck, + vicinity: VicinityMaps, + kwr: _ParserState, + modified_deck: list[str], + nrwo: str, +) -> bool: + """Handle the PROPS section and its supported operations.""" + if nrwo == "PROPS" and not kwr.props: + kwr.props = True + if kwr.separator: + modified_deck.append(kwr.separator) + modified_deck.append(nrwo) + return True + if not kwr.props: + return False + if _handle_oper(dck, vicinity, kwr, modified_deck, nrwo): + return True + if nrwo in {"REGIONS", "SOLUTION"}: + kwr.props = False + return False
+ + + +
+[docs] +def _handle_oper( + dck: ConfigViaDeck, + vicinity: VicinityMaps, + kwr: _ParserState, + modified_deck: list[str], + nrwo: str, +) -> bool: + """Update supported operation records for the modified grid.""" + if nrwo in {"EQUALS", "COPY", "ADD", "MULTIPLY"}: + if nrwo == "COPY" and not kwr.props: + return False + kwr.operation = True + modified_deck.append(nrwo) + return True + if not kwr.operation: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + kwr.operation = False + if len(tokens) > 7 and not tokens[0].startswith("--"): + if "PERM" in tokens[0]: + tokens[1] = "1" + source_i1 = int(tokens[2]) + source_i2 = int(tokens[3]) + source_j1 = int(tokens[4]) + source_j2 = int(tokens[5]) + source_k1 = int(tokens[6]) + source_k2 = int(tokens[7]) + if dck.refinement_enabled: + tokens[2] = str(dck.original_to_first_refined_i[source_i1]) + tokens[3] = str(dck.original_to_last_refined_i[source_i2]) + tokens[4] = str(dck.original_to_first_refined_j[source_j1]) + tokens[5] = str(dck.original_to_last_refined_j[source_j2]) + tokens[6] = str(dck.original_to_first_refined_k[source_k1]) + tokens[7] = str(dck.original_to_last_refined_k[source_k2]) + elif dck.vicinity_specification: + intersects_i = ( + vicinity.min_i - source_i1 + 1 > 0 + or source_i2 - vicinity.max_i + 1 > 0 + or dck.original_to_output_i[source_i2] > 0 + ) + intersects_j = ( + vicinity.min_j - source_j1 + 1 > 0 + or source_j2 - vicinity.max_j + 1 > 0 + or dck.original_to_output_j[source_j2] > 0 + ) + intersects_k = ( + vicinity.min_k - source_k1 + 1 > 0 + or source_k2 - vicinity.max_k + 1 > 0 + or dck.original_to_output_k[source_k2] > 0 + ) + if not (intersects_i and intersects_j and intersects_k): + return True + tokens[2] = str(max(1, dck.original_to_output_i[source_i1])) + tokens[3] = str( + dck.output_nx + if dck.original_to_output_i[source_i2] == 0 + else dck.original_to_output_i[source_i2] + ) + tokens[4] = str(max(1, dck.original_to_output_j[source_j1])) + tokens[5] = str( + dck.output_ny + if dck.original_to_output_j[source_j2] == 0 + else dck.original_to_output_j[source_j2] + ) + tokens[6] = str(max(1, dck.original_to_output_k[source_k1])) + tokens[7] = str( + dck.output_nz + if dck.original_to_output_k[source_k2] == 0 + else dck.original_to_output_k[source_k2] + ) + else: + tokens[2] = str(dck.original_to_output_i[source_i1]) + tokens[3] = str(dck.original_to_output_i[source_i2]) + tokens[4] = str(dck.original_to_output_j[source_j1]) + tokens[5] = str(dck.original_to_output_j[source_j2]) + tokens[6] = str(dck.original_to_output_k[source_k1]) + tokens[7] = str(dck.original_to_output_k[source_k2]) + modified_deck.append(" ".join(tokens)) + return True + if not kwr.props: + modified_deck.append(nrwo) + return False
+ + + +
+[docs] +def _handle_bwpr( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update BWPR grid indices for the modified grid.""" + if nrwo == "BWPR": + kwr.bwpr = True + modified_deck.append(nrwo) + return True + if not kwr.bwpr: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + kwr.bwpr = False + if len(tokens) > 2 and not tokens[0].startswith("--"): + source_i = int(tokens[0]) + source_j = int(tokens[1]) + source_k = int(tokens[2]) + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i] + * dck.original_to_output_j[source_j] + * dck.original_to_output_k[source_k] + == 0 + ): + return True + tokens[0] = str(dck.original_to_output_i[source_i]) + tokens[1] = str(dck.original_to_output_j[source_j]) + tokens[2] = str(dck.original_to_output_k[source_k]) + modified_deck.append(" ".join(tokens)) + return True + return False
+ + + +
+[docs] +def _handle_regions( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Replace the REGIONS content with generated include files.""" + if nrwo == "REGIONS" and not kwr.regions: + kwr.regions = True + modified_deck.append(nrwo) + if len(modified_deck) > 1 and modified_deck[-2].startswith("---"): + modified_deck.append(modified_deck[-2]) + return True + if not kwr.regions: + return False + if nrwo != "SOLUTION": + return True + kwr.regions = False + for region_name in dck.regions_keywords: + modified_deck.append("INCLUDE") + modified_deck.append(f"'{dck.include_prefix}{region_name.upper()}.INC' /\n") + if kwr.separator: + modified_deck.append(kwr.separator) + return False
+ + + +
+[docs] +def _handle_equil( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Replace EQUIL with explicit initialization include files.""" + if not dck.write_explicit_solution: + return False + if "EQUIL" in nrwo: + tokens = nrwo.split() + if not tokens or tokens[0] != "EQUIL": + return False + kwr.equil = True + modified_deck.append("--EQUIL --pycopm explicit initialization") + return True + if not kwr.equil: + return False + tokens = nrwo.split() + if tokens: + if tokens[0].startswith("--") or tokens[0][0].isdigit(): + modified_deck.append("--" + nrwo) + else: + _append_explicit_solution_includes(dck, modified_deck) + modified_deck.append(tokens[0]) + kwr.equil = False + return True
+ + + +
+[docs] +def _append_explicit_solution_includes( + dck: ConfigViaDeck, modified_deck: list[str] +) -> None: + """Append include statements for the explicit solution properties.""" + for property_name in dck.solution_keywords: + modified_deck.append("INCLUDE") + modified_deck.append(f"'{dck.include_prefix}{property_name.upper()}.INC' /\n")
+ + + +
+[docs] +def _handle_grid_props( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Replace GRID properties and preserve supported GRID-section keywords.""" + if nrwo == "GRID" and not kwr.grid: + kwr.grid = True + modified_deck.append(nrwo) + if len(modified_deck) > 1 and modified_deck[-2].startswith("---"): + modified_deck.append(modified_deck[-2]) + modified_deck.append("INIT") + for property_name in ( + dck.base_keywords + dck.grids_keywords + dck.multipliers_keywords + ): + modified_deck.append("INCLUDE") + modified_deck.append( + f"'{dck.include_prefix}{property_name.upper()}.INC' /\n" + ) + return True + if not kwr.grid: + return False + if _handle_fault(dck, kwr, modified_deck, nrwo): + return True + if _handle_mapaxes(kwr, modified_deck, nrwo): + return True + if _handle_aqunum(dck, kwr, modified_deck, nrwo): + return True + if _handle_aqucon(dck, kwr, modified_deck, nrwo): + return True + if _handle_aquancon(dck, kwr, modified_deck, nrwo): + return True + if _handle_bccon(dck, kwr, modified_deck, nrwo): + return True + if nrwo == "EDIT": + kwr.has_edit = True + if kwr.separator: + modified_deck.append(kwr.separator) + modified_deck.append(nrwo) + if kwr.separator: + modified_deck.append(kwr.separator) + if dck.transmissibility_coarsening_method == 0: + if _handle_pinch(kwr, modified_deck, nrwo): + return True + if _handle_multregt(kwr, modified_deck, nrwo): + return True + if _handle_multflt(kwr, modified_deck, nrwo): + return True + if nrwo == "EDIT": + kwr.process_edit = True + modified_deck.append("INCLUDE") + modified_deck.append(f"'{dck.include_prefix}PORV.INC' /\n") + if nrwo == "PROPS": + kwr.grid = False + if not kwr.process_edit: + if not kwr.has_edit: + if kwr.separator: + modified_deck.append(kwr.separator) + modified_deck.append("EDIT") + if kwr.separator: + modified_deck.append(kwr.separator) + modified_deck.append("INCLUDE") + modified_deck.append(f"'{dck.include_prefix}PORV.INC' /\n") + if dck.transmissibility_coarsening_method > 0: + for property_name in ("tranx", "trany", "tranz"): + modified_deck.append("INCLUDE") + modified_deck.append( + f"'{dck.include_prefix}{property_name.upper()}.INC' /\n" + ) + elif kwr.process_edit or ( + kwr.has_edit and (dck.refinement_enabled or dck.vicinity_specification) + ): + if _handle_editnnc(dck, kwr, modified_deck, nrwo): + return True + if _handle_multiply(dck, kwr, modified_deck, nrwo): + return True + else: + return True + return False
+ + + +
+[docs] +def _handle_aqunum( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update AQUNUM grid indices for the modified grid.""" + if nrwo == "AQUNUM": + kwr.aqunum = True + modified_deck.append(nrwo) + return True + if not kwr.aqunum: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + modified_deck.append(nrwo) + kwr.aqunum = False + if len(tokens) > 3 and not tokens[0].startswith("--"): + source_i = int(tokens[1]) + source_j = int(tokens[2]) + source_k = int(tokens[3]) + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i] + * dck.original_to_output_j[source_j] + * dck.original_to_output_k[source_k] + == 0 + ): + return True + tokens[1] = str(dck.original_to_output_i[source_i]) + tokens[2] = str(dck.original_to_output_j[source_j]) + tokens[3] = str(dck.original_to_output_k[source_k]) + modified_deck.append(" ".join(tokens)) + return True + return False
+ + + +
+[docs] +def _handle_aquancon( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update AQUANCON grid-index ranges for the modified grid.""" + if nrwo == "AQUANCON": + kwr.aquancon = True + modified_deck.append(nrwo) + return True + if not kwr.aquancon: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + modified_deck.append(nrwo) + kwr.aquancon = False + return True + if len(tokens) <= 7 or tokens[0].startswith("--"): + return False + source_i1 = int(tokens[1]) + source_i2 = int(tokens[2]) + source_j1 = int(tokens[3]) + source_j2 = int(tokens[4]) + source_k1 = int(tokens[5]) + source_k2 = int(tokens[6]) + if dck.refinement_enabled: + direction = tokens[7] + expanded_tokens = tokens.copy() + mapped_k1 = int(dck.original_to_first_refined_k[source_k1]) + mapped_k2 = int(dck.original_to_last_refined_k[source_k2]) + expanded_tokens[5] = str(mapped_k1) + expanded_tokens[6] = str(mapped_k2) + if direction in {"I", "X"}: + mapped_i = int(dck.original_to_last_refined_i[source_i1]) + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + mapped_j1 = int(dck.original_to_first_refined_j[source_j1]) + mapped_j2 = int(dck.original_to_last_refined_j[source_j2]) + for mapped_j in range(mapped_j1, mapped_j2 + 1): + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"I-", "X-"}: + mapped_i = int(dck.original_to_first_refined_i[source_i1]) + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + mapped_j1 = int(dck.original_to_first_refined_j[source_j1]) + mapped_j2 = int(dck.original_to_last_refined_j[source_j2]) + for mapped_j in range(mapped_j1, mapped_j2 + 1): + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"J", "Y"}: + mapped_j = int(dck.original_to_last_refined_j[source_j1]) + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + mapped_i1 = int(dck.original_to_first_refined_i[source_i1]) + mapped_i2 = int(dck.original_to_last_refined_i[source_i2]) + for mapped_i in range(mapped_i1, mapped_i2 + 1): + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"J-", "Y-"}: + mapped_j = int(dck.original_to_first_refined_j[source_j1]) + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + mapped_i1 = int(dck.original_to_first_refined_i[source_i1]) + mapped_i2 = int(dck.original_to_last_refined_i[source_i2]) + for mapped_i in range(mapped_i1, mapped_i2 + 1): + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + modified_deck.append(" ".join(expanded_tokens)) + tokens[1] = str(dck.original_to_first_refined_i[source_i1]) + tokens[2] = str(dck.original_to_last_refined_i[source_i2]) + tokens[3] = str(dck.original_to_first_refined_j[source_j1]) + tokens[4] = str(dck.original_to_last_refined_j[source_j2]) + tokens[5] = str(mapped_k1) + tokens[6] = str(mapped_k2) + return True + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i1] == 0 + or dck.original_to_output_i[source_i2] == 0 + or dck.original_to_output_j[source_j1] == 0 + or dck.original_to_output_j[source_j2] == 0 + or dck.original_to_output_k[source_k1] == 0 + or dck.original_to_output_k[source_k2] == 0 + ): + return True + tokens[1] = str(dck.original_to_output_i[source_i1]) + tokens[2] = str(dck.original_to_output_i[source_i2]) + tokens[3] = str(dck.original_to_output_j[source_j1]) + tokens[4] = str(dck.original_to_output_j[source_j2]) + tokens[5] = str(dck.original_to_output_k[source_k1]) + tokens[6] = str(dck.original_to_output_k[source_k2]) + modified_deck.append(" ".join(tokens)) + return True
+ + + +
+[docs] +def _handle_aqucon( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update AQUCON grid-index ranges for the modified grid.""" + if nrwo == "AQUCON": + kwr.aqucon = True + modified_deck.append(nrwo) + return True + if not kwr.aqucon: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + modified_deck.append(nrwo) + kwr.aqucon = False + if len(tokens) <= 7 or tokens[0].startswith("--"): + return False + source_i1 = int(tokens[1]) + source_i2 = int(tokens[2]) + source_j1 = int(tokens[3]) + source_j2 = int(tokens[4]) + source_k1 = int(tokens[5]) + source_k2 = int(tokens[6]) + if dck.refinement_enabled: + direction = tokens[7] + expanded_tokens = tokens.copy() + mapped_k1 = int(dck.original_to_first_refined_k[source_k1]) + mapped_k2 = int(dck.original_to_last_refined_k[source_k2]) + expanded_tokens[5] = str(mapped_k1) + expanded_tokens[6] = str(mapped_k2) + if direction in {"I", "X"}: + mapped_i = int(dck.original_to_last_refined_i[source_i1]) + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + mapped_j1 = int(dck.original_to_first_refined_j[source_j1]) + mapped_j2 = int(dck.original_to_last_refined_j[source_j2]) + for mapped_j in range(mapped_j1, mapped_j2 + 1): + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"I-", "X-"}: + mapped_i = int(dck.original_to_first_refined_i[source_i1]) + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + mapped_j1 = int(dck.original_to_first_refined_j[source_j1]) + mapped_j2 = int(dck.original_to_last_refined_j[source_j2]) + for mapped_j in range(mapped_j1, mapped_j2 + 1): + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"J", "Y"}: + mapped_j = int(dck.original_to_last_refined_j[source_j1]) + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + mapped_i1 = int(dck.original_to_first_refined_i[source_i1]) + mapped_i2 = int(dck.original_to_last_refined_i[source_i2]) + for mapped_i in range(mapped_i1, mapped_i2 + 1): + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"J-", "Y-"}: + mapped_j = int(dck.original_to_first_refined_j[source_j1]) + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + mapped_i1 = int(dck.original_to_first_refined_i[source_i1]) + mapped_i2 = int(dck.original_to_last_refined_i[source_i2]) + for mapped_i in range(mapped_i1, mapped_i2 + 1): + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + modified_deck.append(" ".join(expanded_tokens)) + tokens[1] = str(dck.original_to_first_refined_i[source_i1]) + tokens[2] = str(dck.original_to_last_refined_i[source_i2]) + tokens[3] = str(dck.original_to_first_refined_j[source_j1]) + tokens[4] = str(dck.original_to_last_refined_j[source_j2]) + tokens[5] = str(mapped_k1) + tokens[6] = str(mapped_k2) + return True + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i1] == 0 + or dck.original_to_output_i[source_i2] == 0 + or dck.original_to_output_j[source_j1] == 0 + or dck.original_to_output_j[source_j2] == 0 + or dck.original_to_output_k[source_k1] == 0 + or dck.original_to_output_k[source_k2] == 0 + ): + return True + tokens[1] = str(dck.original_to_output_i[source_i1]) + tokens[2] = str(dck.original_to_output_i[source_i2]) + tokens[3] = str(dck.original_to_output_j[source_j1]) + tokens[4] = str(dck.original_to_output_j[source_j2]) + tokens[5] = str(dck.original_to_output_k[source_k1]) + tokens[6] = str(dck.original_to_output_k[source_k2]) + modified_deck.append(" ".join(tokens)) + return True
+ + + +
+[docs] +def _handle_multflt(kwr: _ParserState, modified_deck: list[str], nrwo: str) -> bool: + """Preserve fault multiplier records from the input deck.""" + if "MULTFLT" in nrwo: + tokens = nrwo.split() + if not tokens or tokens[0] != "MULTFLT": + return False + kwr.multflt = True + modified_deck.append(tokens[0]) + return True + if not kwr.multflt: + return False + tokens = nrwo.split() + if tokens: + modified_deck.append(nrwo) + if tokens[0] == "/": + kwr.multflt = False + return True
+ + + +
+[docs] +def _handle_mapaxes(kwr: _ParserState, modified_deck: list[str], nrwo: str) -> bool: + """Preserve MAPAXES so the generated grids retain the same map view.""" + if "MAPAXES" in nrwo: + tokens = nrwo.split() + if not tokens or tokens[0] != "MAPAXES": + return False + kwr.mapaxes = True + modified_deck.append(tokens[0]) + return True + if not kwr.mapaxes: + return False + tokens = nrwo.split() + if tokens: + modified_deck.append(nrwo) + if tokens[-1] == "/" or tokens[0] == "/": + kwr.mapaxes = False + return True
+ + + +
+[docs] +def _handle_pinch(kwr: _ParserState, modified_deck: list[str], nrwo: str) -> bool: + """Preserve PINCH records from the input deck.""" + if "PINCH" in nrwo: + tokens = nrwo.split() + if not tokens or tokens[0] != "PINCH": + return False + kwr.pinch = True + modified_deck.append(tokens[0]) + return True + if not kwr.pinch: + return False + tokens = nrwo.split() + if tokens and not tokens[0].startswith("--"): + modified_deck.append(nrwo) + if "/" in tokens[0] or "/" in tokens[-1]: + kwr.pinch = False + return True
+ + + +
+[docs] +def _handle_multregt(kwr: _ParserState, modified_deck: list[str], nrwo: str) -> bool: + """Preserve MULTREGT records from the GRID section.""" + if "MULTREGT" in nrwo and "/" not in nrwo: + tokens = nrwo.split() + if not tokens or tokens[0] != "MULTREGT": + return False + kwr.multregt = True + modified_deck.append(tokens[0]) + return True + if not kwr.multregt: + return False + tokens = nrwo.split() + if tokens: + modified_deck.append(nrwo) + if tokens[0] == "/": + kwr.multregt = False + return True
+ + + +
+[docs] +def _handle_bccon( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update BCCON grid-index ranges for the modified grid.""" + if nrwo == "BCCON": + kwr.bccon = True + modified_deck.append(nrwo) + return True + if not kwr.bccon: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + modified_deck.append(nrwo) + kwr.bccon = False + return True + if len(tokens) <= 6 or tokens[0].startswith("--"): + return False + source_i1 = int(tokens[1]) + source_i2 = int(tokens[2]) + source_j1 = int(tokens[3]) + source_j2 = int(tokens[4]) + source_k1 = int(tokens[5]) + source_k2 = int(tokens[6]) + if dck.refinement_enabled: + tokens[1] = str(dck.original_to_first_refined_i[source_i1]) + tokens[2] = str(dck.original_to_last_refined_i[source_i2]) + tokens[3] = str(dck.original_to_first_refined_j[source_j1]) + tokens[4] = str(dck.original_to_last_refined_j[source_j2]) + tokens[5] = str(dck.original_to_first_refined_k[source_k1]) + tokens[6] = str(dck.original_to_last_refined_k[source_k2]) + else: + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i1] == 0 + or dck.original_to_output_i[source_i2] == 0 + or dck.original_to_output_j[source_j1] == 0 + or dck.original_to_output_j[source_j2] == 0 + or dck.original_to_output_k[source_k1] == 0 + or dck.original_to_output_k[source_k2] == 0 + ): + return True + tokens[1] = str(dck.original_to_output_i[source_i1]) + tokens[2] = str(dck.original_to_output_i[source_i2]) + tokens[3] = str(dck.original_to_output_j[source_j1]) + tokens[4] = str(dck.original_to_output_j[source_j2]) + tokens[5] = str(dck.original_to_output_k[source_k1]) + tokens[6] = str(dck.original_to_output_k[source_k2]) + modified_deck.append(" ".join(tokens)) + return True
+ + + +
+[docs] +def _handle_multiply( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update MULTIPLY grid-index ranges for the modified grid.""" + if nrwo == "MULTIPLY": + kwr.multiply = True + modified_deck.append(nrwo) + return True + if kwr.multiply: + tokens = nrwo.split() + if tokens and tokens[0] == "/": + modified_deck.append(nrwo) + kwr.multiply = False + if len(tokens) > 7 and not tokens[0].startswith("--"): + source_i1 = int(tokens[2]) + source_i2 = int(tokens[3]) + source_j1 = int(tokens[4]) + source_j2 = int(tokens[5]) + source_k1 = int(tokens[6]) + source_k2 = int(tokens[7]) + if dck.refinement_enabled: + tokens[2] = str(dck.original_to_first_refined_i[source_i1]) + tokens[3] = str(dck.original_to_last_refined_i[source_i2]) + tokens[4] = str(dck.original_to_first_refined_j[source_j1]) + tokens[5] = str(dck.original_to_last_refined_j[source_j2]) + tokens[6] = str(dck.original_to_first_refined_k[source_k1]) + tokens[7] = str(dck.original_to_last_refined_k[source_k2]) + else: + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i1] == 0 + or dck.original_to_output_i[source_i2] == 0 + or dck.original_to_output_j[source_j1] == 0 + or dck.original_to_output_j[source_j2] == 0 + or dck.original_to_output_k[source_k1] == 0 + or dck.original_to_output_k[source_k2] == 0 + ): + return True + tokens[2] = str(dck.original_to_output_i[source_i1]) + tokens[3] = str(dck.original_to_output_i[source_i2]) + tokens[4] = str(dck.original_to_output_j[source_j1]) + tokens[5] = str(dck.original_to_output_j[source_j2]) + tokens[6] = str(dck.original_to_output_k[source_k1]) + tokens[7] = str(dck.original_to_output_k[source_k2]) + modified_deck.append(" ".join(tokens)) + return True + return True
+ + + +
+[docs] +def _handle_editnnc( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update EDITNNC grid indices for the modified grid.""" + if nrwo == "EDITNNC": + kwr.editnnc = True + modified_deck.append(nrwo) + return True + if not kwr.editnnc: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + modified_deck.append(nrwo) + kwr.editnnc = False + if len(tokens) <= 5 or tokens[0].startswith("--"): + return False + source_i1 = int(tokens[0]) + source_j1 = int(tokens[1]) + source_k1 = int(tokens[2]) + source_i2 = int(tokens[3]) + source_j2 = int(tokens[4]) + source_k2 = int(tokens[5]) + if dck.refinement_enabled: + tokens[0] = str(dck.original_to_first_refined_i[source_i1]) + tokens[1] = str(dck.original_to_first_refined_j[source_j1]) + tokens[2] = str(dck.original_to_first_refined_k[source_k1]) + tokens[3] = str(dck.original_to_last_refined_i[source_i2]) + tokens[4] = str(dck.original_to_last_refined_j[source_j2]) + tokens[5] = str(dck.original_to_last_refined_k[source_k2]) + else: + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i1] == 0 + or dck.original_to_output_j[source_j1] == 0 + or dck.original_to_output_k[source_k1] == 0 + or dck.original_to_output_i[source_i2] == 0 + or dck.original_to_output_j[source_j2] == 0 + or dck.original_to_output_k[source_k2] == 0 + ): + return True + tokens[0] = str(dck.original_to_output_i[source_i1]) + tokens[1] = str(dck.original_to_output_j[source_j1]) + tokens[2] = str(dck.original_to_output_k[source_k1]) + tokens[3] = str(dck.original_to_output_i[source_i2]) + tokens[4] = str(dck.original_to_output_j[source_j2]) + tokens[5] = str(dck.original_to_output_k[source_k2]) + modified_deck.append(" ".join(tokens)) + return True
+ + + +
+[docs] +def _handle_fault( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update FAULTS grid-index ranges for the modified grid.""" + if nrwo == "FAULTS": + kwr.faults = True + modified_deck.append(nrwo) + return True + if not kwr.faults: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + modified_deck.append(nrwo) + kwr.faults = False + if len(tokens) <= 7 or tokens[0].startswith("--"): + return False + source_i1 = int(tokens[1]) + source_i2 = int(tokens[2]) + source_j1 = int(tokens[3]) + source_j2 = int(tokens[4]) + source_k1 = int(tokens[5]) + source_k2 = int(tokens[6]) + if dck.refinement_enabled: + direction = tokens[7] + expanded_tokens = tokens.copy() + mapped_k1 = int(dck.original_to_first_refined_k[source_k1]) + mapped_k2 = int(dck.original_to_last_refined_k[source_k2]) + expanded_tokens[5] = str(mapped_k1) + expanded_tokens[6] = str(mapped_k2) + if direction in {"I", "X"}: + mapped_i = int(dck.original_to_last_refined_i[source_i1]) + mapped_j1 = int(dck.original_to_first_refined_j[source_j1]) + mapped_j2 = int(dck.original_to_last_refined_j[source_j2]) + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + for mapped_j in range(mapped_j1, mapped_j2 + 1): + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"I-", "X-"}: + mapped_i = int(dck.original_to_first_refined_i[source_i1]) + mapped_j1 = int(dck.original_to_first_refined_j[source_j1]) + mapped_j2 = int(dck.original_to_last_refined_j[source_j2]) + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + for mapped_j in range(mapped_j1, mapped_j2 + 1): + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"J", "Y"}: + mapped_j = int(dck.original_to_last_refined_j[source_j1]) + mapped_i1 = int(dck.original_to_first_refined_i[source_i1]) + mapped_i2 = int(dck.original_to_last_refined_i[source_i2]) + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + for mapped_i in range(mapped_i1, mapped_i2 + 1): + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + modified_deck.append(" ".join(expanded_tokens)) + elif direction in {"J-", "Y-"}: + mapped_j = int(dck.original_to_first_refined_j[source_j1]) + mapped_i1 = int(dck.original_to_first_refined_i[source_i1]) + mapped_i2 = int(dck.original_to_last_refined_i[source_i2]) + expanded_tokens[3] = str(mapped_j) + expanded_tokens[4] = str(mapped_j) + for mapped_i in range(mapped_i1, mapped_i2 + 1): + expanded_tokens[1] = str(mapped_i) + expanded_tokens[2] = str(mapped_i) + modified_deck.append(" ".join(expanded_tokens)) + tokens[1] = str(dck.original_to_first_refined_i[source_i1]) + tokens[2] = str(dck.original_to_last_refined_i[source_i2]) + tokens[3] = str(dck.original_to_first_refined_j[source_j1]) + tokens[4] = str(dck.original_to_last_refined_j[source_j2]) + tokens[5] = str(mapped_k1) + tokens[6] = str(mapped_k2) + return True + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i1] == 0 + or dck.original_to_output_i[source_i2] == 0 + or dck.original_to_output_j[source_j1] == 0 + or dck.original_to_output_j[source_j2] == 0 + or dck.original_to_output_k[source_k1] == 0 + or dck.original_to_output_k[source_k2] == 0 + ): + return True + tokens[1] = str(dck.original_to_output_i[source_i1]) + tokens[2] = str(dck.original_to_output_i[source_i2]) + tokens[3] = str(dck.original_to_output_j[source_j1]) + tokens[4] = str(dck.original_to_output_j[source_j2]) + tokens[5] = str(dck.original_to_output_k[source_k1]) + tokens[6] = str(dck.original_to_output_k[source_k2]) + modified_deck.append(" ".join(tokens)) + return True
+ + + +
+[docs] +def _handle_welsegs(kwr: _ParserState, modified_deck: list[str], nrwo: str) -> bool: + """Filter WELSEGS records by wells retained in the submodel.""" + if nrwo == "WELSEGS": + kwr.welsegs = True + modified_deck.append(nrwo) + return True + if not kwr.welsegs: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + kwr.welsegs = False + if kwr.skip_block: + kwr.skip_block = False + return True + if len(tokens) > 1: + if not tokens[0].startswith("--") and modified_deck[-1] == "WELSEGS": + well_name = tokens[0].replace("'", "") + if ( + well_name not in kwr.retained_wells + or well_name not in kwr.segmented_wells + ): + del modified_deck[-1] + if modified_deck and modified_deck[-1] == "WELSEGS": + del modified_deck[-1] + kwr.skip_block = True + return True + elif not tokens[0].startswith("--") and not kwr.skip_block: + modified_deck.append(nrwo) + return True + else: + return True + elif kwr.skip_block: + return True + return False
+ + + +
+[docs] +def _handle_compsegs(kwr: _ParserState, modified_deck: list[str], nrwo: str) -> bool: + """Filter COMPSEGS records by wells retained in the submodel.""" + if nrwo == "COMPSEGS": + kwr.compsegs = True + modified_deck.append(nrwo) + return True + if not kwr.compsegs: + return False + tokens = nrwo.split() + if tokens and tokens[0] == "/": + kwr.compsegs = False + if kwr.skip_block: + kwr.skip_block = False + return True + if len(tokens) > 1: + if not tokens[0].startswith("--") and modified_deck[-1] == "COMPSEGS": + well_name = tokens[0].replace("'", "") + if well_name not in kwr.retained_wells: + del modified_deck[-1] + if modified_deck and modified_deck[-1] == "COMPSEGS": + del modified_deck[-1] + kwr.skip_block = True + return True + elif not kwr.skip_block: + modified_deck.append(nrwo) + return True + else: + return True + elif kwr.skip_block: + return True + return False
+ + + +
+[docs] +def _handle_segmented_wells( + dck: ConfigViaDeck, + kwr: _ParserState, + modified_deck: list[str], + nrwo: str, + wellcind: list, +) -> bool: + """Update COMPDAT, COMPSEGS, and COMPLUMP records for the modified grid.""" + if nrwo == "COMPSEGS": + kwr.compsegs = True + modified_deck.append(nrwo) + return True + if kwr.compdat: + tokens = nrwo.split() + if tokens and tokens[0] == "/": + kwr.previous_completion = [] + kwr.compdat = False + if len(tokens) > 4 and not tokens[0].startswith("--"): + well_name = tokens[0].replace("'", "") + source_i = int(tokens[1]) + source_j = int(tokens[2]) + source_k1 = int(tokens[3]) + source_k2 = int(tokens[4]) + if dck.vicinity_specification and ( + well_name not in kwr.retained_wells + or dck.original_to_output_i[source_i] + * dck.original_to_output_j[source_j] + * dck.original_to_output_k[source_k1] + * dck.original_to_output_k[source_k2] + == 0 + ): + return True + if ( + dck.completion_removal_level > 0 + and len(tokens) > 7 + and tokens[7] != "/" + ): + tokens[7] = "1*" + if ( + dck.completion_removal_level > 0 + and len(tokens) > 9 + and tokens[9] not in {"1*", "2*", "3*", "/"} + ): + tokens[9] = "1*" + if ( + dck.completion_removal_level > 1 + and len(tokens) > 12 + and tokens[-2] != "/" + ): + tokens[-2] = "" + tokens[1] = str(dck.original_to_output_i[source_i]) + tokens[2] = str(dck.original_to_output_j[source_j]) + if dck.refinement_enabled: + if kwr.previous_completion: + previous_tokens = kwr.previous_completion + previous_well = previous_tokens[0].replace("'", "") + previous_source_i = int(previous_tokens[1]) + previous_source_j = int(previous_tokens[2]) + previous_source_k1 = int(previous_tokens[3]) + previous_source_k2 = int(previous_tokens[4]) + previous_i = int(dck.original_to_output_i[previous_source_i]) + previous_j = int(dck.original_to_output_j[previous_source_j]) + if ( + well_name == previous_well + and previous_well not in kwr.compsegs_wells + and ( + tokens[1] != str(previous_i) or tokens[2] != str(previous_j) + ) + ): + tokens[3] = str(dck.original_to_output_k[source_k1]) + tokens[4] = str(dck.original_to_output_k[source_k2]) + current_completion = tokens.copy() + previous_completion = tokens.copy() + previous_completion[3] = str( + dck.original_to_output_k[previous_source_k1] + ) + previous_completion[4] = str( + dck.original_to_output_k[previous_source_k2] + ) + if int(tokens[1]) != previous_i: + difference = int(tokens[1]) - previous_i + for offset in range(abs(difference) - 1): + intermediate_i = previous_i + int( + (offset + 1) * difference / abs(difference) + ) + current_completion[1] = str(intermediate_i) + previous_completion[1] = str(intermediate_i) + if offset < (abs(difference) - 1) / 2: + modified_deck.append(" ".join(previous_completion)) + else: + modified_deck.append(" ".join(current_completion)) + elif int(tokens[2]) != previous_j: + difference = int(tokens[2]) - previous_j + for offset in range(abs(difference) - 1): + intermediate_j = previous_j + int( + (offset + 1) * difference / abs(difference) + ) + current_completion[2] = str(intermediate_j) + previous_completion[2] = str(intermediate_j) + if offset < (abs(difference) - 1) / 2: + modified_deck.append(" ".join(previous_completion)) + else: + modified_deck.append(" ".join(current_completion)) + elif ( + well_name == previous_well + and previous_well not in kwr.compsegs_wells + and previous_well in kwr.segmented_wells + and tokens[1] == str(previous_i) + and tokens[2] == str(previous_j) + and dck.original_to_output_k[source_k1] + != dck.original_to_output_k[previous_source_k1] + ): + mapped_k = int(dck.original_to_output_k[source_k1]) + previous_k = int(dck.original_to_output_k[previous_source_k1]) + if previous_k < mapped_k: + k_values = range(previous_k, mapped_k) + else: + k_values = range(mapped_k, previous_k) + for mapped_k_value in k_values: + tokens[3] = str(mapped_k_value + 1) + tokens[4] = str(mapped_k_value + 1) + modified_deck.append(" ".join(tokens)) + kwr.previous_completion = nrwo.split() + return True + elif well_name in kwr.segmented_wells + kwr.compsegs_wells: + tokens[3] = str(dck.original_to_output_k[source_k1]) + tokens[4] = str(dck.original_to_output_k[source_k2]) + else: + tokens[3] = str(dck.original_to_first_refined_k[source_k1]) + tokens[4] = str(dck.original_to_last_refined_k[source_k2]) + elif well_name in kwr.segmented_wells + kwr.compsegs_wells: + tokens[3] = str(dck.original_to_output_k[source_k1]) + tokens[4] = str(dck.original_to_output_k[source_k2]) + else: + tokens[3] = str(dck.original_to_first_refined_k[source_k1]) + tokens[4] = str(dck.original_to_last_refined_k[source_k2]) + else: + tokens[3] = str(dck.original_to_output_k[source_k1]) + tokens[4] = str(dck.original_to_output_k[source_k2]) + if dck.coarsening_enabled and dck.transmissibility_coarsening_method > 0: + completion_i = int(tokens[1]) + completion_j = int(tokens[2]) + for completion_k in range(int(tokens[3]), int(tokens[4]) + 1): + cell_index = ( + completion_i + - 1 + + (completion_j - 1) * dck.output_nx + + (completion_k - 1) * dck.output_nx * dck.output_ny + ) + wellcind.append(cell_index) + modified_deck.append(" ".join(tokens)) + kwr.previous_completion = nrwo.split() + return True + if kwr.compsegs: + tokens = nrwo.split() + if tokens and tokens[0] == "/": + kwr.compsegs = False + if modified_deck[-1].split()[0] in kwr.completion_wells: + del modified_deck[-1] + del modified_deck[-1] + return True + if ( + len(tokens) > 1 + and not tokens[0].startswith("--") + and modified_deck[-1].split()[0] == "COMPSEGS" + and dck.vicinity_specification + ): + well_name = tokens[0].replace("'", "") + if ( + well_name not in kwr.retained_wells + or well_name not in kwr.segmented_wells + ): + del modified_deck[-1] + del modified_deck[-1] + return True + if len(tokens) > 2: + if not tokens[0].startswith("--"): + if dck.vicinity_specification: + well_name = tokens[0].replace("'", "") + if (well_name not in kwr.retained_wells and len(tokens) < 4) or ( + dck.original_to_output_i[int(tokens[0])] + * dck.original_to_output_j[int(tokens[1])] + * dck.original_to_output_k[int(tokens[2])] + == 0 + ): + return True + tokens[0] = str(dck.original_to_output_i[int(tokens[0])]) + tokens[1] = str(dck.original_to_output_j[int(tokens[1])]) + tokens[2] = str(dck.original_to_output_k[int(tokens[2])]) + modified_deck.append(" ".join(tokens)) + return True + tokens[0] = str(dck.original_to_output_i[int(tokens[0])]) + tokens[1] = str(dck.original_to_output_j[int(tokens[1])]) + tokens[2] = str(dck.original_to_output_k[int(tokens[2])]) + modified_deck.append(" ".join(tokens)) + return True + return True + if kwr.complump: + tokens = nrwo.split() + if tokens and tokens[0] == "/": + kwr.complump = False + if modified_deck[-1].split()[0] in kwr.completion_wells: + del modified_deck[-1] + del modified_deck[-1] + return True + if ( + len(tokens) > 1 + and not tokens[0].startswith("--") + and modified_deck[-1].split()[0] == "COMPLUMP" + and dck.vicinity_specification + ): + well_name = tokens[0].replace("'", "") + if ( + well_name not in kwr.retained_wells + or well_name not in kwr.segmented_wells + ): + del modified_deck[-1] + del modified_deck[-1] + return True + if len(tokens) > 2: + if not tokens[0].startswith("--"): + original_tokens = tokens.copy() + position_offset = 0 + for position, value in enumerate(original_tokens): + if "*" in value: + tokens.pop(position + position_offset) + repeat_count = int(value[0]) + for _ in range(repeat_count): + tokens.insert(position + position_offset, "1*") + position_offset += repeat_count - 1 + if dck.vicinity_specification: + well_name = tokens[0].replace("'", "") + for value, axis_name in zip( + tokens[1:5], ("i", "j", "k", "k"), strict=True + ): + if ( + "*" not in value + and getattr(dck, f"{axis_name}c")[int(value)] == 0 + ): + return True + if well_name not in kwr.retained_wells: + return True + for position, axis_name in zip( + range(1, 5), ("i", "j", "k", "k"), strict=True + ): + if "*" not in tokens[position]: + source_index = int(tokens[position]) + tokens[position] = str( + getattr(dck, f"{axis_name}c")[source_index] + ) + modified_deck.append(" ".join(tokens)) + return True + for position, axis_name in zip( + range(1, 5), ("i", "j", "k", "k"), strict=True + ): + if "*" not in tokens[position]: + source_index = int(tokens[position]) + tokens[position] = str( + getattr(dck, f"{axis_name}c")[source_index] + ) + modified_deck.append(" ".join(tokens)) + return True + return True + return False
+ + + +
+[docs] +def _handle_wells( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str, hv: bool +) -> bool: + """Update well-head grid indices and activate completion handlers.""" + if nrwo == "WELSPECS": + kwr.welspecs = True + modified_deck.append(nrwo) + return True + if kwr.welspecs: + tokens = nrwo.split() + if len(tokens) > 3 and not tokens[0].startswith("--"): + well_name = tokens[0].replace("'", "") + source_i = int(tokens[2]) + source_j = int(tokens[3]) + if dck.vicinity_specification: + if well_name not in kwr.retained_wells: + return True + if hv: + mapped_i = int(dck.original_to_output_i[source_i]) + if mapped_i == 0: + for offset in range(dck.output_nx): + lower_i = source_i - offset + upper_i = source_i + offset + if ( + lower_i >= 0 + and int(dck.original_to_output_i[lower_i]) > 0 + ): + mapped_i = int(dck.original_to_output_i[lower_i]) + break + if ( + upper_i < len(dck.original_to_output_i) + and int(dck.original_to_output_i[upper_i]) > 0 + ): + mapped_i = int(dck.original_to_output_i[upper_i]) + break + if mapped_i > 0: + tokens[2] = str(mapped_i) + mapped_j = int(dck.original_to_output_j[source_j]) + if mapped_j == 0: + for offset in range(dck.output_ny): + lower_j = source_j - offset + upper_j = source_j + offset + if ( + lower_j >= 0 + and int(dck.original_to_output_j[lower_j]) > 0 + ): + mapped_j = int(dck.original_to_output_j[lower_j]) + break + if ( + upper_j < len(dck.original_to_output_j) + and int(dck.original_to_output_j[upper_j]) > 0 + ): + mapped_j = int(dck.original_to_output_j[upper_j]) + break + if mapped_j > 0: + tokens[3] = str(mapped_j) + modified_deck.append(" ".join(tokens)) + return True + if ( + dck.original_to_output_i[source_i] + * dck.original_to_output_j[source_j] + == 0 + ): + tokens[2] = "1" + tokens[3] = "1" + modified_deck.append(" ".join(tokens)) + return True + tokens[2] = str(dck.original_to_output_i[source_i]) + tokens[3] = str(dck.original_to_output_j[source_j]) + modified_deck.append(" ".join(tokens)) + return True + if tokens and tokens[0] == "/": + kwr.welspecs = False + if nrwo == "COMPDAT": + kwr.compdat = True + modified_deck.append(nrwo) + return True + if nrwo == "COMPLUMP": + kwr.complump = True + modified_deck.append(nrwo) + return True + return False
+ + + +
+[docs] +def _handle_source( + dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str +) -> bool: + """Update SOURCE grid indices for the modified grid.""" + if nrwo == "SOURCE": + kwr.source = True + modified_deck.append(nrwo) + return True + if not kwr.source: + return False + tokens = nrwo.split() + if len(tokens) > 2 and not tokens[0].startswith("--"): + source_i = int(tokens[0]) + source_j = int(tokens[1]) + source_k = int(tokens[2]) + if dck.vicinity_specification and ( + dck.original_to_output_i[source_i] + * dck.original_to_output_j[source_j] + * dck.original_to_output_k[source_k] + == 0 + ): + return True + tokens[0] = str(dck.original_to_output_i[source_i]) + tokens[1] = str(dck.original_to_output_j[source_j]) + tokens[2] = str(dck.original_to_output_k[source_k]) + modified_deck.append(" ".join(tokens)) + return True + if tokens and tokens[0] == "/": + kwr.source = False + return False
+ + + +
+[docs] +def _scan_deck_file( + dck: ConfigViaDeck, file_path: str | Path +) -> tuple[list[str], bool, NDArray]: + """Scan a deck file for includes and directional multipliers. + + Parameters + ---------- + dck + Deck configuration providing the file encoding. + file_path + DATA or include file to scan. + + Returns + ------- + includes, has_main_multflt, multipliers + Resolved include paths, whether the main deck contains ``MULTFLT``, and + flags for ``MULTX``, ``MULTX-``, ``MULTY``, ``MULTY-``, ``MULTZ``, and + ``MULTZ-``.""" + path = Path(file_path) + includes: list[str] = [] + include_pending = False + maindeckmultflt = False + mults = np.array([False, False, False, False, False, False]) + is_main_deck = ".DATA" in str(path) + base_directory = path.resolve().parent + with path.open("r", encoding=dck.deck_encoding) as file_handle: + for csv_row in csv.reader(file_handle): + deck_line = str(csv_row)[2:-2].strip() + if include_pending: + include_path = deck_line.split("--", maxsplit=1)[0] + include_path = include_path.replace(" /", "").rstrip("/").strip() + include_path = include_path.strip("'\"") + resolved_include = Path(os.path.normpath(base_directory / include_path)) + if resolved_include.exists(): + includes.append(str(resolved_include)) + include_pending = False + continue + mults = _mark_multiplier_keyword(deck_line, mults) + if deck_line == "INCLUDE": + include_pending = True + if is_main_deck and deck_line == "MULTFLT": + maindeckmultflt = True + return includes, maindeckmultflt, mults
+ + + +
+[docs] +def _mark_multiplier_keyword(deck_line: str, mults: NDArray) -> NDArray: + """Set the corresponding flag if a multiplier keyword is found.""" + keywords = deck_line.split() + for i, multiplier in enumerate( + ["multx", "multx-", "multy", "multy-", "multz", "multz-"] + ): + keyword = multiplier.upper() + if deck_line == keyword or (len(keywords) > 1 and keywords[0] == keyword): + mults[i] = True + return mults + return mults
+ + + +
+[docs] +def find_multiplier_keywords(dck: ConfigViaDeck) -> tuple[bool, NDArray]: + """Find directional multiplier keywords in nested includes. + + At most three levels of included files are scanned. + + Parameters + ---------- + dck + Deck configuration identifying the input deck and encoding. + + Returns + ------- + has_main_multflt, multipliers + Whether the main deck contains ``MULTFLT`` and directional multiplier + flags in x, x-, y, y-, z, and z- order.""" + multipliers_values = np.array([False, False, False, False, False, False]) + maindeckmultflt = False + included_files, multflt, mults = _scan_deck_file(dck, f"{dck.input_deck_name}.DATA") + maindeckmultflt = maindeckmultflt or multflt + multipliers_values = multipliers_values | mults + first_level_includes: list[str] = [] + second_level_includes: list[str] = [] + for included_file in included_files: + incs, multflt, mults = _scan_deck_file(dck, included_file) + first_level_includes.extend(incs) + maindeckmultflt = maindeckmultflt or multflt + multipliers_values = multipliers_values | mults + for included_file in first_level_includes: + incs, multflt, mults = _scan_deck_file(dck, included_file) + second_level_includes.extend(incs) + maindeckmultflt = maindeckmultflt or multflt + multipliers_values = multipliers_values | mults + for included_file in second_level_includes: + incs, multflt, mults = _scan_deck_file(dck, included_file) + maindeckmultflt = maindeckmultflt or multflt + multipliers_values = multipliers_values | mults + return maindeckmultflt, multipliers_values
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/refinement.html b/docs/_modules/pycopm/utils/refinement.html new file mode 100644 index 0000000..a8b724e --- /dev/null +++ b/docs/_modules/pycopm/utils/refinement.html @@ -0,0 +1,1027 @@ + + + + + + + + + + pycopm.utils.refinement — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.refinement

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0912,R0913,R0914,R0915,C0302,R0917,R1702,R0916,R0911,E1102
+
+"""Refine a corner-point grid and its reservoir properties."""
+
+import argparse
+import sys
+from contextlib import nullcontext
+from dataclasses import dataclass
+
+import numpy as np
+from alive_progress import alive_bar
+from numpy.typing import NDArray
+
+from pycopm.config.config import ConfigViaDeck
+from pycopm.utils.files_writer import write_grid, write_property_inc
+from pycopm.utils.input_values import parse_axis_modifications
+
+
+
+[docs] +@dataclass(slots=True) +class RefinementMaps: + """Store axis refinement values and subdivision counts.""" + + #: Number of additional cells created from each original x interval. + x: NDArray + + #: Number of additional cells created from each original y interval. + y: NDArray + + #: Number of additional cells created from each original z interval. + z: NDArray + + #: Number of refined cells generated from each original cell, flattened in + #: ``(z, y, x)`` order. + refined_cell_counts: NDArray
+ + + +
+[docs] +def create_refinement_maps( + dck: ConfigViaDeck, cmdargs: argparse.Namespace +) -> RefinementMaps: + """Create axis refinement maps and update output dimensions. + + A refinement value of ``n`` divides an original interval into ``n + 1`` + intervals. + + Parameters + ---------- + dck + Deck configuration whose output dimensions are updated. + cmdargs + Command arguments containing ``refinement``, ``x_refinement``, + ``y_refinement``, and ``z_refinement``. + + Returns + ------- + RefinementMaps + Axis values and the number of subdivisions per original cell.""" + cijk, refs = parse_axis_modifications( + cmdargs.refinement, + [ + cmdargs.x_refinement, + cmdargs.y_refinement, + cmdargs.z_refinement, + ], + ) + values = [] + for direction_index, direction in enumerate(("x", "y", "z")): + original_size = getattr(dck, f"original_n{direction}") + if len(cijk) > 2: + refinement_values = np.full( + original_size, int(cijk[direction_index]), dtype=int + ) + elif len(refs[direction_index]) > 0: + refinement_values = np.asarray(refs[direction_index], dtype=int) + else: + refinement_values = np.zeros(original_size, dtype=int) + values.append(refinement_values) + setattr( + dck, + f"output_n{direction}", + original_size + int(np.sum(refinement_values)), + ) + x_repetitions = values[0] + 1 + y_repetitions = values[1] + 1 + z_repetitions = values[2] + 1 + refined_cell_counts = ( + ( + z_repetitions[:, None, None] + * y_repetitions[None, :, None] + * x_repetitions[None, None, :] + ) + .ravel() + .astype(float) + ) + return RefinementMaps( + x=values[0], y=values[1], z=values[2], refined_cell_counts=refined_cell_counts + )
+ + + +
+[docs] +def refine_properties( + dck: ConfigViaDeck, refinement: RefinementMaps, modified_deck: list[str] +) -> list[str]: + """Map reservoir properties onto the refined grid. + + Properties are copied to generated cells. ``PORV`` is divided equally among + them to preserve each original cell's pore volume. + + Parameters + ---------- + dck + Deck configuration containing source properties and output dimensions. + refinement + Refinement maps created by :func:`create_refinement_maps`. + modified_deck + Deck lines updated with generated property includes. + + Returns + ------- + generated_files + Names of the written include files.""" + generated_files = [] + number_values = dck.output_nx * dck.output_ny * dck.output_nz + property_names = ( + dck.props_keywords + + dck.regions_keywords + + ["porv"] + + dck.grids_keywords + + dck.solution_keywords + ) + show_progress = sys.stdout.isatty() + if show_progress: + bar_status = alive_bar(len(property_names), bar="fish") + else: + bar_status = nullcontext() + x_repetitions = np.asarray(refinement.x, dtype=np.intp) + 1 + y_repetitions = np.asarray(refinement.y, dtype=np.intp) + 1 + z_repetitions = np.asarray(refinement.z, dtype=np.intp) + 1 + with bar_status as bar_animation: + for property_name in property_names: + if show_progress: + bar_animation() + if property_name == "porv": + values = np.divide( + np.asarray(dck.init_file[property_name.upper()]), + refinement.refined_cell_counts, + ) + else: + values = np.zeros(dck.original_cell_count) + if property_name in dck.solution_keywords: + values[dck.original_active_cell_mask] = dck.restart_file[ + property_name.upper(), 0 + ] + else: + values[dck.original_active_cell_mask] = dck.init_file[ + property_name.upper() + ] + output_dtype = int if "num" in property_name else float + coarse_values = values.reshape( + dck.original_nz, dck.original_ny, dck.original_nx + ) + refined_values = ( + np.repeat( + np.repeat( + np.repeat(coarse_values, x_repetitions, axis=2), + y_repetitions, + axis=1, + ), + z_repetitions, + axis=0, + ) + .reshape(number_values) + .astype(output_dtype, copy=False) + ) + if property_name == "porv": + dck.output_actnum = (refined_values > 0).astype(int) + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") + write_property_inc( + dck, + property_name, + refined_values, + number_values, + modified_deck, + True, + ) + return generated_files
+ + + +
+[docs] +def create_coord_axis_map( + refinement_values: NDArray, +) -> tuple[NDArray, NDArray]: + """Create interpolation data for one COORD axis. + + Parameters + ---------- + refinement_values + Number of additional cells in each original interval. + + Returns + ------- + source_indices, fractions + Original intervals and relative positions of refined grid points.""" + interval_counts = refinement_values + 1 + source_indices = np.repeat( + np.arange(refinement_values.size, dtype=np.intp), + interval_counts, + ) + fraction_blocks = [ + np.arange(interval_count, dtype=float) / interval_count + for interval_count in interval_counts + ] + source_indices = np.concatenate( + ( + source_indices, + np.asarray([refinement_values.size - 1], dtype=np.intp), + ) + ) + fractions = np.concatenate( + ( + *fraction_blocks, + np.asarray([1.0]), + ) + ) + return source_indices, fractions
+ + + +
+[docs] +def create_zcorn_axis_map( + refinement_values: NDArray, +) -> tuple[NDArray, NDArray]: + """Create interpolation data for one ZCORN axis. + + Parameters + ---------- + refinement_values + Number of additional cells in each original interval. + + Returns + ------- + source_indices, fractions + Original intervals and relative corner positions in ZCORN order.""" + interval_counts = refinement_values + 1 + source_indices = np.repeat( + np.arange(refinement_values.size, dtype=np.intp), + 2 * interval_counts, + ) + fraction_blocks = [ + np.repeat( + np.arange(interval_count + 1, dtype=float) / interval_count, + 2, + )[1:-1] + for interval_count in interval_counts + ] + fractions = np.concatenate(fraction_blocks) + return source_indices, fractions
+ + + +
+[docs] +def refine_zcorn_surface( + source_surface: NDArray, + destination_surface: NDArray, + original_nx: int, + original_ny: int, + output_nx: int, + output_ny: int, + zcorn_x_indices: NDArray, + zcorn_y_indices: NDArray, + zcorn_x_fractions: NDArray, + zcorn_y_fractions: NDArray, +) -> None: + """Interpolate one ZCORN surface onto the refined horizontal grid. + + Parameters + ---------- + source_surface + Flattened input surface with ``4 * original_nx * original_ny`` values. + destination_surface + Preallocated output with ``4 * output_nx * output_ny`` values, modified + in place. + original_nx, original_ny + Original horizontal grid dimensions. + output_nx, output_ny + Refined horizontal grid dimensions. + zcorn_x_indices, zcorn_y_indices + Source interval indices for refined corners. + zcorn_x_fractions, zcorn_y_fractions + Relative interpolation positions within source intervals.""" + source_values = source_surface.reshape( + original_ny, + 2, + original_nx, + 2, + ) + x_start_values = source_values[:, :, zcorn_x_indices, 0] + x_value_difference = source_values[:, :, zcorn_x_indices, 1] - x_start_values + x_refined_values = ( + x_start_values + zcorn_x_fractions[None, None, :] * x_value_difference + ) + y_start_values = x_refined_values[zcorn_y_indices, 0, :] + y_value_difference = x_refined_values[zcorn_y_indices, 1, :] - y_start_values + destination_values = destination_surface.reshape( + 2 * output_ny, + 2 * output_nx, + ) + np.multiply( + y_value_difference, + zcorn_y_fractions[:, None], + out=destination_values, + ) + destination_values += y_start_values
+ + + +
+[docs] +def refine_grid(dck: ConfigViaDeck, refinement: RefinementMaps) -> None: + """Create and write the refined corner-point grid. + + ``COORD`` and ``ZCORN`` values are linearly interpolated along the refined + axes. + + Parameters + ---------- + dck + Deck configuration containing original geometry and grid dimensions. + refinement + Axis refinement maps.""" + original_zcorn = np.asarray(dck.egrid_file["ZCORN"], dtype=float) + original_coord = np.asarray(dck.egrid_file["COORD"], dtype=float) + x_refinement = np.asarray(refinement.x, dtype=np.intp) + y_refinement = np.asarray(refinement.y, dtype=np.intp) + z_refinement = np.asarray(refinement.z, dtype=np.intp) + + coord_x_indices, coord_x_fractions = create_coord_axis_map(x_refinement) + coord_y_indices, coord_y_fractions = create_coord_axis_map(y_refinement) + source_coord = original_coord.reshape( + dck.original_ny + 1, + dck.original_nx + 1, + 6, + ) + x_start_coord = source_coord[:, coord_x_indices, :] + x_coord_difference = source_coord[:, coord_x_indices + 1, :] - x_start_coord + x_refined_coord = ( + x_start_coord + coord_x_fractions[None, :, None] * x_coord_difference + ) + y_start_coord = x_refined_coord[coord_y_indices, :, :] + y_coord_difference = x_refined_coord[coord_y_indices + 1, :, :] - y_start_coord + refined_coord = ( + y_start_coord + coord_y_fractions[:, None, None] * y_coord_difference + ) + cr = refined_coord.ravel() + + zcorn_x_indices, zcorn_x_fractions = create_zcorn_axis_map(x_refinement) + zcorn_y_indices, zcorn_y_fractions = create_zcorn_axis_map(y_refinement) + source_surface_size = 4 * dck.original_nx * dck.original_ny + refined_surface_size = 4 * dck.output_nx * dck.output_ny + refined_zcorn_size = 8 * dck.output_nx * dck.output_ny * dck.output_nz + source_surfaces = original_zcorn.reshape( + 2 * dck.original_nz, + source_surface_size, + ) + zc = np.empty(refined_zcorn_size, dtype=float) + + refined_top_surface = np.empty( + refined_surface_size, + dtype=float, + ) + refined_bottom_surface = np.empty( + refined_surface_size, + dtype=float, + ) + output_index = 0 + + original_nx = dck.original_nx + original_ny = dck.original_ny + output_nx = dck.output_nx + output_ny = dck.output_ny + for layer_index, refinement_value in enumerate(z_refinement): + refinement_count = int(refinement_value) + 1 + refine_zcorn_surface( + source_surfaces[2 * layer_index], + refined_top_surface, + original_nx, + original_ny, + output_nx, + output_ny, + zcorn_x_indices, + zcorn_y_indices, + zcorn_x_fractions, + zcorn_y_fractions, + ) + refine_zcorn_surface( + source_surfaces[2 * layer_index + 1], + refined_bottom_surface, + original_nx, + original_ny, + output_nx, + output_ny, + zcorn_x_indices, + zcorn_y_indices, + zcorn_x_fractions, + zcorn_y_fractions, + ) + surface_difference = refined_bottom_surface - refined_top_surface + vertical_fractions = np.repeat( + np.arange( + refinement_count + 1, + dtype=float, + ) + / refinement_count, + 2, + )[1:-1] + + for vertical_fraction in vertical_fractions: + output_surface = zc[output_index : output_index + refined_surface_size] + if vertical_fraction == 0.0: + np.copyto(output_surface, refined_top_surface) + elif vertical_fraction == 1.0: + np.copyto(output_surface, refined_bottom_surface) + else: + np.multiply( + surface_difference, + vertical_fraction, + out=output_surface, + ) + output_surface += refined_top_surface + output_index += refined_surface_size + + write_grid(dck, cr, zc, False)
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/runs_executer.html b/docs/_modules/pycopm/utils/runs_executer.html new file mode 100644 index 0000000..af1e13f --- /dev/null +++ b/docs/_modules/pycopm/utils/runs_executer.html @@ -0,0 +1,717 @@ + + + + + + + + + + pycopm.utils.runs_executer — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.runs_executer

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0914
+
+"""Run TOML-based simulation studies and generate postprocessing plots."""
+
+import shlex
+import stat
+import subprocess
+import sys
+from pathlib import Path
+
+from mako.template import Template
+
+from pycopm.config.config import ConfigViaTOML
+from pycopm.utils.terminal import pycopm_info, pycopm_success
+
+
+
+[docs] +def run_simulations(cfg: ConfigViaTOML) -> None: + """Run the configured OPM Flow or ERT workflow. + + Job scripts are copied to the output project and made executable before a + single realization or ERT study is started. + + Parameters + ---------- + cfg + TOML configuration containing execution mode and commands.""" + project_path = Path(cfg.output_directory) + source_jobs = Path(cfg.resource_directory) / "jobs" + target_jobs = project_path / "jobs" + target_jobs.mkdir(parents=True, exist_ok=True) + subprocess.run( + ["cp", "-a", f"{source_jobs}/.", f"{target_jobs}/."], + check=True, + ) + for filename in ( + "PERMX_eval", + "PERMY_eval", + "PERMZ_eval", + "table_eval", + "time_eval", + "flow_eval", + ): + script_path = target_jobs / f"{filename}.py" + if script_path.exists(): + script_path.chmod(script_path.stat().st_mode | stat.S_IXUSR) + if cfg.execution_mode == "single-run": + subprocess.run( + ["ert", "test_run", "ert.ert"], + cwd=project_path, + check=True, + ) + simulation_path = ( + project_path / "output" / "simulations" / "realisation-0" / "iter-0" + ) + subprocess.run( + [ + *shlex.split(str(cfg.flow_command)), + str(simulation_path / f"{cfg.reference_case_name}_COARSER.DATA"), + f"--output-dir={simulation_path}", + ], + cwd=project_path, + check=True, + ) + elif cfg.execution_mode == "ert": + subprocess.run( + ["ert", *shlex.split(str(cfg.ert_arguments)), "ert.ert"], + cwd=project_path, + check=True, + ) + pycopm_success("the results have been written to ", str(project_path), [])
+ + + +
+[docs] +def generate_postprocessing_plots( + cfg: ConfigViaTOML, elapsed_seconds: float, number_tables: int +) -> None: + """Render and execute the postprocessing script. + + Parameters + ---------- + cfg + TOML configuration and plotting settings. ``let_parameters`` is sorted in + place before rendering. + elapsed_seconds + Elapsed preprocessing and simulation time. + number_tables + Number of generated saturation-function tables.""" + project_path = Path(cfg.output_directory) + simulations_path = project_path / "output" / "simulations" + ensemble_size = len(next(simulations_path.walk())[1]) + number_iterations = 1 + for realisation_index in range(ensemble_size): + realisation_path = simulations_path / f"realisation-{realisation_index}" + number_iterations = max( + number_iterations, len(next(realisation_path.walk())[1]) + ) + cfg.let_parameters = sorted(cfg.let_parameters, key=lambda item: item[0]) + template = Template( + filename=str( + Path(cfg.resource_directory) + / "template_scripts" + / "common" + / "plot_post.mako" + ) + ) + rendered_template = template.render( + output_directory=cfg.output_directory, + resource_directory=cfg.resource_directory, + let_parameters=cfg.let_parameters, + history_matching_end_date=cfg.history_matching_end_date, + reference_case_name=cfg.reference_case_name, + model_name=cfg.model_name, + observation_relative_errors=cfg.observation_relative_errors, + observation_minimum_errors=cfg.observation_minimum_errors, + cleanup_file_suffixes=cfg.cleanup_file_suffixes, + number_tables=number_tables, + elapsed_seconds=elapsed_seconds, + number_iterations=number_iterations, + ensemble_size=ensemble_size, + ) + plotting_path = project_path / "jobs" / "plotting.py" + plotting_path.write_text(rendered_template, encoding="utf8") + pycopm_info("running the postprocessing methods, please wait...") + subprocess.run( + [sys.executable, str(plotting_path)], + cwd=project_path, + check=True, + )
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/terminal.html b/docs/_modules/pycopm/utils/terminal.html new file mode 100644 index 0000000..b2b72fc --- /dev/null +++ b/docs/_modules/pycopm/utils/terminal.html @@ -0,0 +1,842 @@ + + + + + + + + + + pycopm.utils.terminal — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.terminal

+# SPDX-FileCopyrightText: 2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+
+"""Format command-line messages for pycopm.
+
+ANSI colors are applied when supported by the selected stream. The module
+provides consistent formatting for invalid, accepted, deprecated, and
+informational values, plus fatal errors, warnings, tips, progress messages, and
+generated-file reports."""
+
+import os
+import sys
+from typing import NoReturn
+
+ANSI_BOLD_RED = "1;31"
+ANSI_BOLD_YELLOW = "1;33"
+ANSI_BOLD_GREEN = "1;32"
+ANSI_BOLD_BLUE = "1;34"
+ANSI_BOLD_MAGENTA = "1;35"
+ANSI_YELLOW = "1;33"
+ANSI_GREEN = "1;32"
+ANSI_CYAN = "36"
+ANSI_RED = "31"
+ANSI_BLUE = "1;34"
+
+
+
+[docs] +def _supports_color(stream: object = sys.stderr) -> bool: + """Check whether an output stream supports ANSI colors. + + Parameters + ---------- + stream : object, optional + Output stream used to determine ANSI-color support. + + Returns + ------- + bool + Whether ANSI color output is enabled for the stream.""" + return ( + hasattr(stream, "isatty") + and stream.isatty() + and os.environ.get("NO_COLOR") is None + and os.environ.get("TERM") != "dumb" + )
+ + + +
+[docs] +def _colorize( + text: str, + code: str, + stream: object = sys.stderr, +) -> str: + """Wrap text in an ANSI color sequence when supported. + + Parameters + ---------- + text : str + Text to format. + code : str + ANSI Select Graphic Rendition code. + stream : object, optional + Output stream used to determine ANSI-color support. + + Returns + ------- + str + Colored text, or unchanged text when colors are disabled.""" + if not _supports_color(stream): + return text + return f"\033[{code}m{text}\033[0m"
+ + + +
+[docs] +def cli_warning_value(value: str) -> str: + """Format a deprecated CLI option or value. + + Parameters + ---------- + value : str + Value to inspect or format. + + Returns + ------- + str + Quoted value colored as a warning when supported.""" + return _colorize(repr(value), ANSI_YELLOW)
+ + + +
+[docs] +def cli_correct_value(value: str) -> str: + """Format a correct CLI option or value. + + Parameters + ---------- + value : str + Value to inspect or format. + + Returns + ------- + str + Quoted value colored as a valid alternative when supported.""" + return _colorize(repr(value), ANSI_GREEN)
+ + + +
+[docs] +def cli_error_value(value: str) -> str: + """Format an invalid CLI option or value. + + Parameters + ---------- + value : str + Value to inspect or format. + + Returns + ------- + str + Quoted value colored as invalid when supported.""" + return _colorize(repr(value), ANSI_RED)
+ + + +
+[docs] +def cli_info_value(value: str) -> str: + """Format an informational CLI option or value. + + Parameters + ---------- + value : str + Value to inspect or format. + + Returns + ------- + str + Quoted value colored as information when supported.""" + return _colorize(repr(value), ANSI_BLUE)
+ + + +
+[docs] +def pycopm_error(message: str) -> NoReturn: + """Raise a fatal command-line error. + + Parameters + ---------- + message : str + Human-readable message to display or append. + + Raises + ------ + SystemExit + Always raised with the formatted error message.""" + label = _colorize("error", ANSI_BOLD_RED) + raise SystemExit(f"{pycopm_name()}: {label}: {message}")
+ + + +
+[docs] +def pycopm_warning(message: str) -> None: + """Display a non-fatal command-line warning. + + Parameters + ---------- + message : str + Human-readable message to display or append.""" + label = _colorize("warning", ANSI_BOLD_YELLOW) + print(f"{pycopm_name()}: {label}: {message}", file=sys.stderr)
+ + + +
+[docs] +def pycopm_info(message: str) -> None: + """Display an informational command-line message. + + Parameters + ---------- + message : str + Human-readable message to display or append.""" + label = _colorize("info", ANSI_BOLD_BLUE, sys.stdout) + print(f"{pycopm_name()}: {label}: {message}")
+ + + +
+[docs] +def pycopm_tip(message: str) -> None: + """Display a command-line suggestion. + + Parameters + ---------- + message : str + Human-readable message to display or append.""" + label = _colorize("tip", ANSI_BOLD_MAGENTA, sys.stdout) + print(f"{pycopm_name(sys.stdout)}: {label}: {message}")
+ + + +
+[docs] +def pycopm_success(msg: str, output_dir: str, filenames: list[str]) -> None: + """Display the generated output location and filenames. + + Parameters + ---------- + msg : str + Optional success text printed before the output location. + output_dir : str + Directory containing the generated files. + filenames : list[str] + Generated filenames to report.""" + label = _colorize("success", ANSI_BOLD_GREEN, sys.stdout) + if not filenames: + print(f"{pycopm_name()}: {label}: {msg}{output_dir}") + elif len(filenames) == 1: + print(f"{pycopm_name()}: {label}: {msg}{output_dir}/{filenames[0]}") + elif len(filenames) <= 5: + print(f"{pycopm_name()}: {label}{msg}") + print(f" Output directory: {output_dir}") + print(f" Files ({len(filenames)}): {', '.join(filenames)}") + else: + print(f"{pycopm_name()}: {label}{msg}") + print(f" Output directory: {output_dir}") + print(f" Files ({len(filenames)}):") + for filename in filenames: + print(f" - {filename}")
+ + + +
+[docs] +def pycopm_name(stream: object = sys.stderr) -> str: + """Format the pycopm program name. + + Parameters + ---------- + stream : object, optional + Output stream used to determine ANSI-color support. + + Returns + ------- + str + Formatted program name.""" + characters = [("pycopm", "1")] + return "".join( + _colorize(character, color, stream) for character, color in characters + )
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/transformation.html b/docs/_modules/pycopm/utils/transformation.html new file mode 100644 index 0000000..775f186 --- /dev/null +++ b/docs/_modules/pycopm/utils/transformation.html @@ -0,0 +1,759 @@ + + + + + + + + + + pycopm.utils.transformation — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.transformation

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0912,R0913,R0914,R0915,C0302,R0917,R1702,R0916,R0911,E1102
+
+"""Transform corner-point grid coordinates and rewrite associated properties."""
+
+import sys
+from contextlib import nullcontext
+
+import numpy as np
+from alive_progress import alive_bar
+
+from pycopm.config.config import ConfigViaDeck
+from pycopm.utils.files_writer import write_grid, write_property_inc
+
+
+
+[docs] +def transform_grid(dck: ConfigViaDeck) -> None: + """Apply the configured transformation to the corner-point grid. + + Supported specifications are ``translate [x,y,z]``, ``scale [x,y,z]``, and + ``rotatexy``, ``rotatexz``, or ``rotateyz`` followed by an angle in degrees. + Rotations are performed about the coordinate-system origin. + + Parameters + ---------- + dck + Deck configuration containing ``grid_transformation`` and source geometry.""" + transformation = dck.grid_transformation.split() + transformation_name = transformation[0] + + original_zcorn = np.asarray(dck.egrid_file["ZCORN"], dtype=float) + original_coord = np.asarray(dck.egrid_file["COORD"], dtype=float) + transformed_coord = original_coord.reshape(-1, 2, 3).copy() + + if transformation_name in ("translate", "scale"): + transformation_values = np.fromstring( + transformation[1].strip("()[]"), sep=",", dtype=float + ) + + if transformation_name == "translate": + transformed_coord += transformation_values + zc = original_zcorn + transformation_values[2] + else: + transformed_coord *= transformation_values + zc = original_zcorn * transformation_values[2] + + else: + angle = np.deg2rad(float(transformation[1])) + cosine = np.cos(angle) + sine = np.sin(angle) + + if transformation_name == "rotatexy": + xy_values = transformed_coord[:, :, :2].copy() + transformed_coord[:, :, 0] = ( + xy_values[:, :, 0] * cosine - xy_values[:, :, 1] * sine + ) + transformed_coord[:, :, 1] = ( + xy_values[:, :, 1] * cosine + xy_values[:, :, 0] * sine + ) + zc = original_zcorn.copy() + + else: + coordinate_axis = 0 if transformation_name == "rotatexz" else 1 + corner_pairs = np.asarray(((0, 1), (2, 3), (4, 5), (6, 7))) + horizontal_coordinates = np.empty(original_zcorn.size, dtype=float) + horizontal_index = 0 + + for layer_index in range(dck.original_nz): + layer_coordinates = np.asarray( + [ + [ + dck.grid_model.xyz_from_ijk( + column_index, row_index, layer_index + )[coordinate_axis] + for column_index in range(dck.original_nx) + ] + for row_index in range(dck.original_ny) + ], + dtype=float, + ) + coordinate_values = ( + layer_coordinates[:, :, corner_pairs] + .transpose(2, 0, 1, 3) + .reshape(-1) + ) + next_index = horizontal_index + coordinate_values.size + horizontal_coordinates[horizontal_index:next_index] = coordinate_values + horizontal_index = next_index + + horizontal_coordinates = horizontal_coordinates.reshape( + original_zcorn.shape + ) + + if transformation_name == "rotatexz": + xz_values = transformed_coord[:, :, (0, 2)].copy() + transformed_coord[:, :, 0] = ( + xz_values[:, :, 0] * cosine + xz_values[:, :, 1] * sine + ) + transformed_coord[:, :, 2] = ( + xz_values[:, :, 1] * cosine - xz_values[:, :, 0] * sine + ) + zc = original_zcorn * cosine - horizontal_coordinates * sine + else: + yz_values = transformed_coord[:, :, (1, 2)].copy() + transformed_coord[:, :, 1] = ( + yz_values[:, :, 0] * cosine - yz_values[:, :, 1] * sine + ) + transformed_coord[:, :, 2] = ( + yz_values[:, :, 1] * cosine + yz_values[:, :, 0] * sine + ) + zc = original_zcorn * cosine + horizontal_coordinates * sine + + cr = transformed_coord.ravel() + write_grid(dck, cr, zc, False)
+ + + +
+[docs] +def transform_properties(dck: ConfigViaDeck, modified_deck: list[str]) -> list[str]: + """Rewrite reservoir properties for a transformed grid. + + Property values are unchanged because transformations modify only geometry. + + Parameters + ---------- + dck + Deck configuration containing source properties and output dimensions. + modified_deck + Deck lines updated with generated property includes. + + Returns + ------- + generated_files + Names of the written include files.""" + generated_files = [] + property_names = ( + dck.props_keywords + + dck.regions_keywords + + dck.grids_keywords + + dck.solution_keywords + + ["porv"] + ) + number_values = dck.output_nx * dck.output_ny * dck.output_nz + show_progress = sys.stdout.isatty() + if show_progress: + bar_progress = alive_bar(len(property_names), bar="fish") + else: + bar_progress = nullcontext() + with bar_progress as bar_animation: + for property_name in property_names: + if show_progress: + bar_animation() + values = np.zeros(dck.original_cell_count) + if property_name in dck.solution_keywords: + values[dck.original_active_cell_mask] = dck.restart_file[ + property_name.upper(), 0 + ] + elif property_name == "porv": + values = np.asarray(dck.init_file[property_name.upper()]) + else: + values[dck.original_active_cell_mask] = dck.init_file[ + property_name.upper() + ] + write_property_inc( + dck, + property_name, + values, + number_values, + modified_deck, + True, + ) + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") + return generated_files
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_modules/pycopm/utils/vicinity.html b/docs/_modules/pycopm/utils/vicinity.html new file mode 100644 index 0000000..4291a18 --- /dev/null +++ b/docs/_modules/pycopm/utils/vicinity.html @@ -0,0 +1,1773 @@ + + + + + + + + + + pycopm.utils.vicinity — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +

Source code for pycopm.utils.vicinity

+# SPDX-FileCopyrightText: 2024-2026 NORCE Research AS
+# SPDX-License-Identifier: GPL-3.0
+# pylint: disable=R0902,R0912,R0913,R0914,R0915,C0302,R0917,R1702,R0916,R0911,E1102
+
+"""Extract submodels and map pore volume from outside their boundaries."""
+
+import csv
+import sys
+from contextlib import nullcontext
+from dataclasses import dataclass, field
+from pathlib import Path
+
+import numpy as np
+from alive_progress import alive_bar
+from numpy.typing import NDArray
+from shapely import Polygon, contains_xy, prepare
+
+from pycopm.config.config import ConfigViaDeck
+from pycopm.utils.files_writer import write_grid, write_property_inc
+
+
+
+[docs] +@dataclass(slots=True) +class VicinityMaps: + """Store a vicinity selection, bounds, and pore-volume mappings. + + Global and per-layer bounds are one-based and inclusive. + """ + + #: Selected well name, or an empty string for polygon and region + #: selections. + selector: str + + #: Well-vicinity shape: ``box``, ``diamond``, or ``diamondxy``. ``None`` is + #: used for polygon and region selections. + shape: str | None + + #: Boolean mask identifying selected original-grid cells, flattened in + #: ``(z, y, x)`` order. + cell_mask: NDArray + + #: Minimum selected i index across all layers. + min_i: int + + #: Maximum selected i index across all layers. + max_i: int + + #: Minimum selected j index across all layers. + min_j: int + + #: Maximum selected j index across all layers. + max_j: int + + #: Minimum selected k index. + min_k: int + + #: Maximum selected k index. + max_k: int + + #: Minimum selected i index in each original layer. + layer_min_i: NDArray + + #: Maximum selected i index in each original layer. + layer_max_i: NDArray + + #: Minimum selected j index in each original layer. + layer_min_j: NDArray + + #: Maximum selected j index in each original layer. + layer_max_j: NDArray + + #: Total pore volume of selected active cells in each original layer. + layer_selected_porv: NDArray + + #: Total pore volume outside the selection in each original layer. + layer_external_porv: NDArray + + #: Zero-based completion coordinates in ``[i, j, k]`` order for the + #: selected well. + well_cells: list[list[int]] + + #: Zero-based output-grid indices of selected well cells. These cells are + #: excluded from boundary source remapping. + well_indices: list[int] = field(default_factory=list) + + #: Number of selected active cells in each output layer. + active_counts: NDArray = field(default_factory=lambda: np.array([], dtype=int)) + + #: Original-grid source index assigned to each output boundary cell during + #: pore-volume correction. Zero denotes no assigned source. + source_indices: NDArray = field(default_factory=lambda: np.array([], dtype=int))
+ + + +
+[docs] +@dataclass(slots=True) +class _BoundaryMapping: + """Store pore-volume mapping results for one submodel boundary.""" + + #: Unassigned pore volume collected along the boundary. + pore_volume: float + + #: Number of active cells receiving pore volume from the boundary. + active_count: int + + #: Distance from the geometric boundary to each receiving cell. + offsets: NDArray
+ + + +
+[docs] +def _submodel_index(dck: ConfigViaDeck, column: int, row: int, layer: int) -> int: + return column + row * dck.output_nx + layer * dck.output_nx * dck.output_ny
+ + + +
+[docs] +def _original_index(dck: ConfigViaDeck, column: int, row: int, layer: int) -> int: + return column + row * dck.original_nx + layer * dck.original_nx * dck.original_ny
+ + + +
+[docs] +def _add_or_collect_porv( + dck: ConfigViaDeck, submodel_index: int, pore_volume: float +) -> float: + if dck.pore_volume_correction in (1, 2): + dck.output_porv[submodel_index] += pore_volume + return 0.0 + return pore_volume
+ + + +
+[docs] +def create_vicinity_maps(dck: ConfigViaDeck) -> VicinityMaps: + """Select submodel cells and calculate their bounds. + + Selections can use region values, an xy polygon, or a well-centred ``box``, + ``diamond``, or ``diamondxy`` neighbourhood. + + Parameters + ---------- + dck + Deck configuration containing the vicinity specification and source grid. + + Returns + ------- + VicinityMaps + Selection mask, bounds, well cells, and per-layer pore-volume totals.""" + vicinity_options = dck.vicinity_specification.split() + selector = vicinity_options[0] + is_selector_well = False + shape: str | None = None + total_cells = dck.original_nx * dck.original_ny * dck.original_nz + cells_per_layer = dck.original_nx * dck.original_ny + well_cells = [] + if selector.upper() == "XYPOLYGON": + cell_centers = np.empty((total_cells, 3), dtype=float) + for global_index in range(total_cells): + layer_index, layer_offset = divmod(global_index, cells_per_layer) + row_index, column_index = divmod(layer_offset, dck.original_nx) + cell_coordinates = np.asarray( + dck.grid_model.xyz_from_ijk(column_index, row_index, layer_index, True), + dtype=float, + ) + cell_centers[global_index] = np.mean(cell_coordinates, axis=1) + polygon_coordinates = np.asarray( + [ + ( + float(coordinate.split(",")[0][1:]), + float(coordinate.split(",")[1][:-1]), + ) + for coordinate in vicinity_options[1:] + ], + dtype=float, + ) + grid_minimum = np.minimum( + np.min(cell_centers[:, :2], axis=0), + np.min(polygon_coordinates, axis=0), + ) + grid_maximum = np.maximum( + np.max(cell_centers[:, :2], axis=0), + np.max(polygon_coordinates, axis=0), + ) + coordinate_range = grid_maximum - grid_minimum + normalized_centers = (cell_centers[:, :2] - grid_minimum) / coordinate_range + normalized_polygon = (polygon_coordinates - grid_minimum) / coordinate_range + polygon = Polygon(normalized_polygon) + prepare(polygon) + cell_mask = contains_xy( + polygon, normalized_centers[:, 0], normalized_centers[:, 1] + ) + elif len(vicinity_options) > 2: + is_selector_well = True + shape = vicinity_options[1].lower() + well_cells = _get_well_completions_for_vicinity(dck, selector) + cell_mask = np.zeros(total_cells, dtype=bool) + well_locations = np.asarray(well_cells, dtype=np.intp).reshape(-1, 3) + if shape == "diamond": + interval = int(vicinity_options[2]) + offset_range = np.arange(-interval, interval + 1, dtype=np.intp) + layer_offsets, row_offsets, column_offsets = np.meshgrid( + offset_range, offset_range, offset_range, indexing="ij" + ) + offset_mask = ( + (np.abs(row_offsets - column_offsets - layer_offsets) <= interval) + & (np.abs(column_offsets + row_offsets - layer_offsets) <= interval) + & (np.abs(row_offsets - column_offsets + layer_offsets) <= interval) + & (np.abs(column_offsets + row_offsets + layer_offsets) <= interval) + ) + vicinity_offsets = np.column_stack( + ( + column_offsets[offset_mask], + row_offsets[offset_mask], + layer_offsets[offset_mask], + ) + ) + for well_location in well_locations: + selected_locations = well_location + vicinity_offsets + location_mask = ( + (selected_locations[:, 0] >= 0) + & (selected_locations[:, 0] < dck.original_nx) + & (selected_locations[:, 1] >= 0) + & (selected_locations[:, 1] < dck.original_ny) + & (selected_locations[:, 2] >= 0) + & (selected_locations[:, 2] < dck.original_nz) + ) + selected_locations = selected_locations[location_mask] + global_indices = ( + selected_locations[:, 0] + + selected_locations[:, 1] * dck.original_nx + + selected_locations[:, 2] * cells_per_layer + ) + cell_mask[global_indices] = True + elif shape == "diamondxy": + interval = int(vicinity_options[2]) + offset_range = np.arange(-interval, interval + 1, dtype=np.intp) + row_offsets_xy, column_offsets_xy = np.meshgrid( + offset_range, offset_range, indexing="ij" + ) + offset_mask = (np.abs(row_offsets_xy - column_offsets_xy) <= interval) & ( + np.abs(column_offsets_xy + row_offsets_xy) <= interval + ) + horizontal_offsets = np.column_stack( + (column_offsets_xy[offset_mask], row_offsets_xy[offset_mask]) + ) + all_layer_offsets = ( + np.arange(dck.original_nz, dtype=np.intp)[:, None] * cells_per_layer + ) + for well_location in well_locations: + selected_columns = well_location[0] + horizontal_offsets[:, 0] + selected_rows = well_location[1] + horizontal_offsets[:, 1] + location_mask = ( + (selected_columns >= 0) + & (selected_columns < dck.original_nx) + & (selected_rows >= 0) + & (selected_rows < dck.original_ny) + ) + horizontal_indices = ( + selected_columns[location_mask] + + selected_rows[location_mask] * dck.original_nx + ) + global_indices = ( + all_layer_offsets + horizontal_indices[None, :] + ).ravel() + cell_mask[global_indices] = True + else: + intervals = np.asarray( + [ + ( + int(interval.split(",")[0][1:]), + int(interval.split(",")[1][:-1]), + ) + for interval in vicinity_options[2:] + ], + dtype=int, + ) + if intervals.shape != (3, 2): + raise ValueError( + "The vicinity intervals must define x, y, and z ranges." + ) + column_offsets = np.arange( + intervals[0, 0], intervals[0, 1] + 1, dtype=np.intp + ) + row_offsets = np.arange(intervals[1, 0], intervals[1, 1] + 1, dtype=np.intp) + layer_offsets = np.arange( + intervals[2, 0], intervals[2, 1] + 1, dtype=np.intp + ) + layer_offsets, row_offsets, column_offsets = np.meshgrid( + layer_offsets, row_offsets, column_offsets, indexing="ij" + ) + vicinity_offsets = np.column_stack( + ( + column_offsets.ravel(), + row_offsets.ravel(), + layer_offsets.ravel(), + ) + ) + for well_location in well_locations: + selected_locations = well_location + vicinity_offsets + location_mask = ( + (selected_locations[:, 0] >= 0) + & (selected_locations[:, 0] < dck.original_nx) + & (selected_locations[:, 1] >= 0) + & (selected_locations[:, 1] < dck.original_ny) + & (selected_locations[:, 2] >= 0) + & (selected_locations[:, 2] < dck.original_nz) + ) + selected_locations = selected_locations[location_mask] + global_indices = ( + selected_locations[:, 0] + + selected_locations[:, 1] * dck.original_nx + + selected_locations[:, 2] * cells_per_layer + ) + cell_mask[global_indices] = True + else: + selected_values = np.asarray( + [int(value) for value in vicinity_options[1].split(",")] + ) + keyword = selector.upper() + cell_mask = np.zeros(total_cells, dtype=bool) + active_property_values = np.asarray(dck.init_file[keyword]) + cell_mask[dck.original_active_cell_mask] = np.isin( + active_property_values, selected_values + ) + selected_active_cells = np.asarray(cell_mask, dtype=bool) & ( + np.asarray(dck.original_porv) > 0 + ) + selected_indices = np.flatnonzero(selected_active_cells) + layer_indices, layer_offsets = np.divmod(selected_indices, cells_per_layer) + row_indices, column_indices = np.divmod(layer_offsets, dck.original_nx) + min_i = int(np.min(column_indices)) + 1 + max_i = int(np.max(column_indices)) + 1 + min_j = int(np.min(row_indices)) + 1 + max_j = int(np.max(row_indices)) + 1 + min_k = int(np.min(layer_indices)) + 1 + max_k = int(np.max(layer_indices)) + 1 + layer_min_i = np.full(dck.original_nz, dck.original_nx, dtype=int) + layer_max_i = np.ones(dck.original_nz, dtype=int) + layer_min_j = np.full(dck.original_nz, dck.original_ny, dtype=int) + layer_max_j = np.ones(dck.original_nz, dtype=int) + np.minimum.at(layer_min_i, layer_indices, column_indices + 1) + np.maximum.at(layer_max_i, layer_indices, column_indices + 1) + np.minimum.at(layer_min_j, layer_indices, row_indices + 1) + np.maximum.at(layer_max_j, layer_indices, row_indices + 1) + pore_volumes = np.asarray(dck.original_porv, dtype=float) + layer_selected_porv = np.bincount( + layer_indices, + weights=pore_volumes[selected_indices], + minlength=dck.original_nz, + ) + all_layer_indices = np.repeat(np.arange(dck.original_nz), cells_per_layer) + layer_external_porv = np.bincount( + all_layer_indices, + weights=pore_volumes * ~selected_active_cells, + minlength=dck.original_nz, + ) + return VicinityMaps( + selector=selector if is_selector_well else "", + shape=shape, + cell_mask=np.asarray(cell_mask, dtype=bool), + min_i=min_i, + max_i=max_i, + min_j=min_j, + max_j=max_j, + min_k=min_k, + max_k=max_k, + layer_min_i=layer_min_i, + layer_max_i=layer_max_i, + layer_min_j=layer_min_j, + layer_max_j=layer_max_j, + layer_selected_porv=layer_selected_porv, + layer_external_porv=layer_external_porv, + well_cells=well_cells, + )
+ + + +
+[docs] +def map_vicinity_properties( + dck: ConfigViaDeck, vicinity: VicinityMaps, modified_deck: list[str] +) -> list[str]: + """Map reservoir properties into the submodel bounding box. + + Cells inside the bounding box but outside the selection are written as + inactive. The function updates output pore volume and active cells. + + Parameters + ---------- + dck + Deck configuration containing source properties and output dimensions. + vicinity + Selection and bounds created by :func:`create_vicinity_maps`. + modified_deck + Deck lines updated with generated property includes. + + Returns + ------- + generated_files + Names of the written include files.""" + generated_files = [] + submodel_cells = dck.output_nx * dck.output_ny * dck.output_nz + dck.original_active_cell_mask = np.asarray(dck.original_porv) > 0 + vicinity.active_counts = np.zeros(dck.output_nz) + vicinity.source_indices = np.zeros(submodel_cells, dtype=int) + property_names = ( + ["porv"] + + dck.solution_keywords + + dck.props_keywords + + dck.regions_keywords + + dck.grids_keywords + ) + column_indices = np.arange(vicinity.min_i - 1, vicinity.max_i, dtype=np.intp) + row_indices = np.arange(vicinity.min_j - 1, vicinity.max_j, dtype=np.intp) + layer_indices = np.arange(vicinity.min_k - 1, vicinity.max_k, dtype=np.intp) + selected_global_indices = ( + column_indices[None, None, :] + + row_indices[None, :, None] * dck.original_nx + + layer_indices[:, None, None] * dck.original_nx * dck.original_ny + ).ravel() + selected_layer_indices = np.broadcast_to( + np.arange(dck.output_nz, dtype=np.intp)[:, None, None], + (dck.output_nz, dck.output_ny, dck.output_nx), + ).ravel() + selected_cell_mask = ( + dck.original_active_cell_mask[selected_global_indices] + & np.asarray(vicinity.cell_mask, dtype=bool)[selected_global_indices] + ) + selected_output_indices = np.flatnonzero(selected_cell_mask) + effective_global_indices = selected_global_indices[selected_cell_mask] + active_global_indices = np.flatnonzero(dck.original_active_cell_mask) + active_source_indices = np.searchsorted( + active_global_indices, effective_global_indices + ) + vicinity.active_counts[:] = np.bincount( + selected_layer_indices[selected_cell_mask], + minlength=dck.output_nz, + ) + solution_keywords = set(dck.solution_keywords) + show_progress = sys.stdout.isatty() + if show_progress: + bar_ctx = alive_bar(len(property_names), bar="fish") + else: + bar_ctx = nullcontext() + with bar_ctx as bar_animation: + for property_name in property_names: + if show_progress: + bar_animation() + property_dtype = int if "num" in property_name else float + values_c = np.zeros(submodel_cells, dtype=property_dtype) + if property_name == "porv": + source_values = np.asarray(dck.original_porv) + values_c[selected_output_indices] = source_values[ + effective_global_indices + ] + dck.output_porv = values_c + elif property_name in solution_keywords: + source_values = np.asarray(dck.restart_file[property_name.upper(), 0]) + values_c[selected_output_indices] = source_values[active_source_indices] + else: + source_values = np.asarray(dck.init_file[property_name.upper()]) + values_c[selected_output_indices] = source_values[active_source_indices] + write_property_inc( + dck, + property_name, + values_c, + submodel_cells, + modified_deck, + True, + ) + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") + dck.output_actnum = (np.asarray(dck.output_porv) > 0).astype(int) + return generated_files
+ + + +
+[docs] +def extract_vicinity_grid(dck: ConfigViaDeck, vicinity: VicinityMaps) -> None: + """Extract and write the selected corner-point subgrid. + + Parameters + ---------- + dck + Deck configuration containing source geometry and axis mappings. + vicinity + Inclusive bounds of the selected submodel.""" + original_zcorn = np.asarray(dck.egrid_file["ZCORN"]) + original_coord = np.asarray(dck.egrid_file["COORD"]) + source_coord = original_coord.reshape(dck.original_ny + 1, dck.original_nx + 1, 6) + cr = source_coord[ + vicinity.min_j - 1 : vicinity.max_j + 1, + vicinity.min_i - 1 : vicinity.max_i + 1, + :, + ].ravel() + selected_columns = np.asarray(dck.original_to_output_i[1 : dck.original_nx + 1]) > 0 + selected_rows = np.asarray(dck.original_to_output_j[1 : dck.original_ny + 1]) > 0 + selected_layers = np.asarray(dck.original_to_output_k[1 : dck.original_nz + 1]) > 0 + doubled_column_indices = np.flatnonzero(np.repeat(selected_columns, 2)) + doubled_row_indices = np.flatnonzero(np.repeat(selected_rows, 2)) + surface_indices = np.flatnonzero(np.repeat(selected_layers, 2)) + source_zcorn = original_zcorn.reshape( + 2 * dck.original_nz, + 2 * dck.original_ny, + 2 * dck.original_nx, + ) + zc = source_zcorn[ + np.ix_( + surface_indices, + doubled_row_indices, + doubled_column_indices, + ) + ].ravel() + write_grid(dck, cr, zc, False)
+ + + +
+[docs] +def _map_south_boundary( + dck: ConfigViaDeck, + vicinity: VicinityMaps, + layer_index: int, + original_layer: int, + column_offset: int, + row_offset: int, + trailing_columns: int, +) -> _BoundaryMapping: + offsets = np.zeros(dck.output_nx, dtype=int) + collected_porv = 0.0 + active_count = 0 + width = dck.output_nx - column_offset - trailing_columns + south_rows = int(vicinity.layer_min_j[original_layer]) - 1 + original_column_start = int(vicinity.layer_min_i[original_layer]) - 1 + pore_volume_grid = np.asarray(dck.original_porv).reshape( + dck.original_nz, dck.original_ny, dck.original_nx + ) + for local_column in range(width): + submodel_column = local_column + column_offset + submodel_cell = _submodel_index(dck, submodel_column, row_offset, layer_index) + original_column = local_column + original_column_start + boundary_cell = _original_index( + dck, original_column, max(south_rows - 1, 0), original_layer + ) + boundary_porv = float( + np.sum(pore_volume_grid[original_layer, :south_rows, original_column]) + ) + if dck.output_actnum[submodel_cell] > 0: + if submodel_cell not in vicinity.well_indices: + vicinity.source_indices[submodel_cell] = boundary_cell + dck.original_nx + active_count += 1 + collected_porv += _add_or_collect_porv(dck, submodel_cell, boundary_porv) + continue + interior_cell = boundary_cell + 1 + if ( + interior_cell >= dck.original_porv.size + or dck.original_porv[interior_cell] <= 0 + ): + collected_porv += boundary_porv + continue + for inward_offset in range(dck.output_ny - 1 - row_offset): + submodel_cell = _submodel_index( + dck, + submodel_column, + inward_offset + 1 + row_offset, + layer_index, + ) + original_row = inward_offset + south_rows + original_cell = _original_index( + dck, original_column, original_row, original_layer + ) + boundary_porv += 0.5 * dck.original_porv[original_cell] + if dck.output_actnum[submodel_cell] > 0: + if submodel_cell not in vicinity.well_indices: + vicinity.source_indices[submodel_cell] = original_cell + offsets[local_column] = inward_offset + 1 + active_count += 1 + collected_porv += _add_or_collect_porv( + dck, submodel_cell, boundary_porv + ) + break + if inward_offset == dck.output_ny - 2 - row_offset: + collected_porv += boundary_porv + return _BoundaryMapping(collected_porv, active_count, offsets)
+ + + +
+[docs] +def _map_north_boundary( + dck: ConfigViaDeck, + vicinity: VicinityMaps, + layer_index: int, + original_layer: int, + column_offset: int, + trailing_rows: int, + trailing_columns: int, +) -> _BoundaryMapping: + offsets = np.zeros(dck.output_nx, dtype=int) + collected_porv = 0.0 + active_count = 0 + width = dck.output_nx - column_offset - trailing_columns + north_start = int(vicinity.layer_max_j[original_layer]) + original_column_start = int(vicinity.layer_min_i[original_layer]) - 1 + pore_volume_grid = np.asarray(dck.original_porv).reshape( + dck.original_nz, dck.original_ny, dck.original_nx + ) + for local_column in range(width): + submodel_column = local_column + column_offset + submodel_cell = _submodel_index( + dck, submodel_column, dck.output_ny - 1 - trailing_rows, layer_index + ) + original_column = local_column + original_column_start + boundary_porv = float( + np.sum(pore_volume_grid[original_layer, north_start:, original_column]) + ) + interior_cell = _original_index( + dck, original_column, north_start - 1, original_layer + ) + if dck.output_actnum[submodel_cell] > 0: + if trailing_rows == 0 and submodel_cell not in vicinity.well_indices: + vicinity.source_indices[submodel_cell] = interior_cell + active_count += 1 + collected_porv += _add_or_collect_porv(dck, submodel_cell, boundary_porv) + continue + if dck.original_porv[interior_cell] <= 0: + collected_porv += boundary_porv + continue + for inward_offset in range(dck.output_ny - 1 - trailing_rows): + submodel_row = dck.output_ny - 2 - inward_offset - trailing_rows + submodel_cell = _submodel_index( + dck, submodel_column, submodel_row, layer_index + ) + original_row = north_start - inward_offset - 1 + original_cell = _original_index( + dck, original_column, original_row, original_layer + ) + boundary_porv += 0.5 * dck.original_porv[original_cell] + if dck.output_actnum[submodel_cell] > 0: + if submodel_cell not in vicinity.well_indices: + vicinity.source_indices[submodel_cell] = ( + original_cell - dck.original_nx + ) + offsets[local_column] = inward_offset + 1 + active_count += 1 + collected_porv += _add_or_collect_porv( + dck, submodel_cell, boundary_porv + ) + break + if inward_offset == dck.output_ny - 2 - trailing_rows: + collected_porv += boundary_porv + return _BoundaryMapping(collected_porv, active_count, offsets)
+ + + +
+[docs] +def _map_east_boundary( + dck: ConfigViaDeck, + vicinity: VicinityMaps, + layer_index: int, + original_layer: int, + column_offset: int, + row_offset: int, + trailing_rows: int, +) -> _BoundaryMapping: + offsets = np.zeros(dck.output_ny, dtype=int) + collected_porv = 0.0 + active_count = 0 + height = dck.output_ny - row_offset - trailing_rows + east_columns = int(vicinity.layer_min_i[original_layer]) - 1 + original_row_start = int(vicinity.layer_min_j[original_layer]) - 1 + pore_volume_grid = np.asarray(dck.original_porv).reshape( + dck.original_nz, dck.original_ny, dck.original_nx + ) + for local_row in range(height): + submodel_row = local_row + row_offset + submodel_cell = _submodel_index(dck, column_offset, submodel_row, layer_index) + original_row = local_row + original_row_start + boundary_porv = float( + np.sum(pore_volume_grid[original_layer, original_row, :east_columns]) + ) + interior_cell = _original_index( + dck, max(east_columns - 1, 0), original_row, original_layer + ) + if dck.output_actnum[submodel_cell] > 0: + if submodel_cell not in vicinity.well_indices: + vicinity.source_indices[submodel_cell] = interior_cell + 1 + active_count += 1 + collected_porv += _add_or_collect_porv(dck, submodel_cell, boundary_porv) + continue + adjacent_cell = interior_cell + 1 + if ( + adjacent_cell >= dck.original_porv.size + or dck.original_porv[adjacent_cell] <= 0 + ): + collected_porv += boundary_porv + continue + for inward_offset in range(dck.output_nx - 1 - column_offset): + submodel_cell = _submodel_index( + dck, + inward_offset + column_offset + 1, + submodel_row, + layer_index, + ) + original_column = inward_offset + east_columns + original_cell = _original_index( + dck, original_column, original_row, original_layer + ) + boundary_porv += 0.5 * dck.original_porv[original_cell] + if dck.output_actnum[submodel_cell] > 0: + if submodel_cell not in vicinity.well_indices: + vicinity.source_indices[submodel_cell] = original_cell + 1 + offsets[local_row] = inward_offset + 1 + active_count += 1 + collected_porv += _add_or_collect_porv( + dck, submodel_cell, boundary_porv + ) + break + if inward_offset == dck.output_nx - 2 - column_offset: + collected_porv += boundary_porv + return _BoundaryMapping(collected_porv, active_count, offsets)
+ + + +
+[docs] +def _get_well_completions_for_vicinity(dck: ConfigViaDeck, optvic) -> list: + """Collect zero-based completions for a selected well. + + Parameters + ---------- + dck + Deck configuration identifying the source DATA file. + optvic + Well name from the vicinity specification. + + Returns + ------- + list[list[int]] + Completion coordinates in ``[i, j, k]`` order.""" + in_compdat = False + deck_path = Path(f"{dck.input_deck_name}.DATA") + wvicinity = [] + with deck_path.open("r", encoding=dck.deck_encoding) as deck_file: + for row in csv.reader(deck_file): + parsed_line = str(row)[2:-2].strip() + if parsed_line == "COMPDAT": + in_compdat = True + continue + if not in_compdat: + continue + tokens = parsed_line.split() + if not tokens: + continue + if tokens[0] == "/": + in_compdat = False + continue + if tokens[0].startswith("--"): + continue + well_name = tokens[0].replace("'", "") + if well_name != optvic or len(tokens) <= 4: + continue + source_i = int(tokens[1]) + source_j = int(tokens[2]) + source_k1 = int(tokens[3]) + source_k2 = int(tokens[4]) + for source_k in range(source_k1, source_k2 + 1): + wvicinity.append([source_i - 1, source_j - 1, source_k - 1]) + return wvicinity
+ + + +
+[docs] +def _map_west_boundary( + dck: ConfigViaDeck, + vicinity: VicinityMaps, + layer_index: int, + original_layer: int, + row_offset: int, + trailing_rows: int, + trailing_columns: int, +) -> _BoundaryMapping: + offsets = np.zeros(dck.output_ny, dtype=int) + collected_porv = 0.0 + active_count = 0 + height = dck.output_ny - row_offset - trailing_rows + west_start = int(vicinity.layer_max_i[original_layer]) + original_row_start = int(vicinity.layer_min_j[original_layer]) - 1 + pore_volume_grid = np.asarray(dck.original_porv).reshape( + dck.original_nz, dck.original_ny, dck.original_nx + ) + for local_row in range(height): + submodel_row = local_row + row_offset + submodel_cell = _submodel_index( + dck, + dck.output_nx - 1 - trailing_columns, + submodel_row, + layer_index, + ) + original_row = local_row + original_row_start + boundary_porv = float( + np.sum(pore_volume_grid[original_layer, original_row, west_start:]) + ) + interior_cell = _original_index( + dck, west_start - 1, original_row, original_layer + ) + if dck.output_actnum[submodel_cell] > 0: + if trailing_columns == 0 and submodel_cell not in vicinity.well_indices: + vicinity.source_indices[submodel_cell] = interior_cell + active_count += 1 + collected_porv += _add_or_collect_porv(dck, submodel_cell, boundary_porv) + continue + if dck.original_porv[interior_cell] <= 0: + collected_porv += boundary_porv + continue + for inward_offset in range(dck.output_nx - 1 - trailing_columns): + submodel_column = dck.output_nx - 2 - inward_offset - trailing_columns + submodel_cell = _submodel_index( + dck, submodel_column, submodel_row, layer_index + ) + original_column = west_start - inward_offset - 1 + original_cell = _original_index( + dck, original_column, original_row, original_layer + ) + boundary_porv += 0.5 * dck.original_porv[original_cell] + if dck.output_actnum[submodel_cell] > 0: + if submodel_cell not in vicinity.well_indices: + vicinity.source_indices[submodel_cell] = original_cell - 1 + offsets[local_row] = inward_offset + 1 + active_count += 1 + collected_porv += _add_or_collect_porv( + dck, submodel_cell, boundary_porv + ) + break + if inward_offset == dck.output_nx - 2 - trailing_columns: + collected_porv += boundary_porv + return _BoundaryMapping(collected_porv, active_count, offsets)
+ + + +
+[docs] +def _corner_pore_volumes( + dck: ConfigViaDeck, vicinity: VicinityMaps, original_layer: int +) -> tuple[float, float, float, float]: + """Calculate excluded pore volume in the four layer corners. + + Returns + ------- + southwest, southeast, northwest, northeast + Corner pore-volume totals for the selected original layer.""" + pore_volume_grid = np.asarray(dck.original_porv).reshape( + dck.original_nz, dck.original_ny, dck.original_nx + ) + minimum_column = int(vicinity.layer_min_i[original_layer]) - 1 + maximum_column = int(vicinity.layer_max_i[original_layer]) + minimum_row = int(vicinity.layer_min_j[original_layer]) - 1 + maximum_row = int(vicinity.layer_max_j[original_layer]) + layer_porv = pore_volume_grid[original_layer] + return ( + float(np.sum(layer_porv[:minimum_row, :minimum_column])), + float(np.sum(layer_porv[:minimum_row, maximum_column:])), + float(np.sum(layer_porv[maximum_row:, :minimum_column])), + float(np.sum(layer_porv[maximum_row:, maximum_column:])), + )
+ + + +
+[docs] +def _apply_layer_pore_volume_correction( + dck: ConfigViaDeck, + vicinity: VicinityMaps, + layer_index: int, + column_offset: int, + row_offset: int, + trailing_columns: int, + trailing_rows: int, + south: _BoundaryMapping, + north: _BoundaryMapping, + east: _BoundaryMapping, + west: _BoundaryMapping, + corner_porv: tuple[float, float, float, float], +) -> None: + layer_start = layer_index * dck.output_nx * dck.output_ny + layer_end = layer_start + dck.output_nx * dck.output_ny + boundary_indices = ( + np.flatnonzero(vicinity.source_indices[layer_start:layer_end] != 0) + + layer_start + ) + boundary_count = int(boundary_indices.size) + if dck.pore_volume_correction == 3: + if vicinity.shape != "diamond" and boundary_count > 0: + dck.output_porv[boundary_indices] += ( + vicinity.layer_external_porv[layer_index] / boundary_count + ) + return + if dck.pore_volume_correction == 4: + if vicinity.shape != "diamond" and vicinity.active_counts[layer_index] > 0: + active_indices = ( + np.flatnonzero(dck.output_actnum[layer_start:layer_end] > 0) + + layer_start + ) + dck.output_porv[active_indices] += ( + vicinity.layer_external_porv[layer_index] + / vicinity.active_counts[layer_index] + ) + return + southwest_porv, southeast_porv, northwest_porv, northeast_porv = corner_porv + width = dck.output_nx - column_offset - trailing_columns + for local_column in range(width): + south_cell = _submodel_index( + dck, + local_column + column_offset, + south.offsets[local_column] + row_offset, + layer_index, + ) + if dck.output_actnum[south_cell] > 0 and south.active_count > 0: + increment = south.pore_volume / south.active_count + if dck.pore_volume_correction == 1: + if south.active_count + east.active_count > 0: + increment += southwest_porv / ( + south.active_count + east.active_count + ) + if south.active_count + west.active_count > 0: + increment += southeast_porv / ( + south.active_count + west.active_count + ) + dck.output_porv[south_cell] += increment + north_cell = _submodel_index( + dck, + local_column + column_offset, + dck.output_ny - 1 - north.offsets[local_column] - trailing_rows, + layer_index, + ) + if dck.output_actnum[north_cell] > 0 and north.active_count > 0: + increment = north.pore_volume / north.active_count + if dck.pore_volume_correction == 1: + if north.active_count + east.active_count > 0: + increment += northwest_porv / ( + north.active_count + east.active_count + ) + if north.active_count + west.active_count > 0: + increment += northeast_porv / ( + north.active_count + west.active_count + ) + dck.output_porv[north_cell] += increment + height = dck.output_ny - row_offset - trailing_rows + for local_row in range(height): + east_cell = _submodel_index( + dck, + east.offsets[local_row] + column_offset, + local_row + row_offset, + layer_index, + ) + if dck.output_actnum[east_cell] > 0 and east.active_count > 0: + increment = east.pore_volume / east.active_count + if dck.pore_volume_correction == 1: + if east.active_count + south.active_count > 0: + increment += southwest_porv / ( + east.active_count + south.active_count + ) + if east.active_count + north.active_count > 0: + increment += northwest_porv / ( + east.active_count + north.active_count + ) + dck.output_porv[east_cell] += increment + west_cell = _submodel_index( + dck, + dck.output_nx - 1 - west.offsets[local_row] - trailing_columns, + local_row + row_offset, + layer_index, + ) + if dck.output_actnum[west_cell] > 0 and west.active_count > 0: + increment = west.pore_volume / west.active_count + if dck.pore_volume_correction == 1: + if west.active_count + south.active_count > 0: + increment += southeast_porv / ( + west.active_count + south.active_count + ) + if west.active_count + north.active_count > 0: + increment += northeast_porv / ( + west.active_count + north.active_count + ) + dck.output_porv[west_cell] += increment + if dck.pore_volume_correction == 2: + for corner_column, corner_row, corner_value in ( + (0, 0, southwest_porv), + (dck.output_nx - 1, 0, southeast_porv), + (0, dck.output_ny - 1, northwest_porv), + (dck.output_nx - 1, dck.output_ny - 1, northeast_porv), + ): + if corner_value != 0: + nearest_index = _find_nearest_active_corner_cell( + dck, layer_index, corner_column, corner_row + ) + dck.output_porv[nearest_index] += corner_value + expected_porv = ( + vicinity.layer_external_porv[layer_index] + + vicinity.layer_selected_porv[layer_index] + ) + mapped_porv = float(np.sum(dck.output_porv[layer_start:layer_end])) + if mapped_porv < expected_porv and boundary_count > 0: + dck.output_porv[boundary_indices] += ( + expected_porv - mapped_porv + ) / boundary_count
+ + + +
+[docs] +def _find_nearest_active_corner_cell( + dck: ConfigViaDeck, layer_index: int, corner_i: int, corner_j: int +) -> int: + """Return the nearest active cell to a corner in a layer.""" + layer_start = layer_index * dck.output_nx * dck.output_ny + active_cells = np.asarray(dck.output_actnum)[ + layer_start : layer_start + dck.output_nx * dck.output_ny + ].reshape(dck.output_ny, dck.output_nx) + active_j, active_i = np.nonzero(active_cells > 0) + if active_i.size == 0: + raise ValueError(f"No active cells found in submodel layer {layer_index}") + distances = np.abs(active_i - corner_i) + np.abs(active_j - corner_j) + nearest = np.lexsort((active_i, active_j, distances))[0] + return int(layer_start + active_i[nearest] + active_j[nearest] * dck.output_nx)
+ + + +
+[docs] +def _distribute_vertical_pore_volume( + dck: ConfigViaDeck, vicinity: VicinityMaps +) -> None: + """Distribute pore volume excluded above and below the submodel. + + Parameters + ---------- + dck + Deck configuration whose ``output_porv`` is updated. + vicinity + Selection bounds and correction settings.""" + if vicinity.shape == "diamond" or dck.pore_volume_correction in (3, 4): + return + cells_per_original_layer = dck.original_nx * dck.original_ny + submodel_porv = dck.output_porv.reshape(dck.output_nz, dck.output_ny, dck.output_nx) + active_columns = np.any(submodel_porv > 0, axis=0) + column_rows, column_columns = np.nonzero(active_columns) + if column_rows.size == 0: + return + if vicinity.min_k > 1: + lower_end = (vicinity.min_k - 1) * cells_per_original_layer + lower_porv = float(np.sum(dck.original_porv[:lower_end])) + first_layers = np.argmax(submodel_porv > 0, axis=0) + lower_indices = ( + first_layers[column_rows, column_columns] * dck.output_nx * dck.output_ny + + column_rows * dck.output_nx + + column_columns + ) + dck.output_porv[lower_indices] += lower_porv / lower_indices.size + if vicinity.max_k < dck.original_nz: + upper_start = vicinity.max_k * cells_per_original_layer + upper_porv = float(np.sum(dck.original_porv[upper_start:])) + last_layers = dck.output_nz - 1 - np.argmax(submodel_porv[::-1] > 0, axis=0) + upper_indices = ( + last_layers[column_rows, column_columns] * dck.output_nx * dck.output_ny + + column_rows * dck.output_nx + + column_columns + ) + dck.output_porv[upper_indices] += upper_porv / upper_indices.size
+ + + +
+[docs] +def apply_boundary_pore_volume_correction( + dck: ConfigViaDeck, vicinity: VicinityMaps +) -> None: + """Map pore volume excluded from the submodel onto active cells. + + The correction strategy is selected by ``dck.pore_volume_correction``. Depending + on the chosen method, excluded pore volume is assigned to corresponding + boundary cells, nearest corner cells, all boundary cells, or all active cells. + + Parameters + ---------- + dck + Deck configuration whose ``output_porv`` is updated. + vicinity + Selection bounds and pore-volume mapping arrays.""" + if ( + vicinity.shape in ("diamond", "diamondxy") + and int(dck.vicinity_specification.split()[2]) > 0 + ): + for well_location in vicinity.well_cells: + vicinity.well_indices.append( + well_location[0] + - vicinity.min_i + + (well_location[1] - vicinity.min_j + 1) * dck.output_nx + + (well_location[2] - vicinity.min_k + 1) + * dck.output_nx + * dck.output_ny + + 1 + ) + original_porv = dck.output_porv.copy() if dck.pore_volume_correction == 0 else None + for layer_index in range(dck.output_nz): + original_layer = layer_index + vicinity.min_k - 1 + row_offset = int(vicinity.layer_min_j[original_layer]) - vicinity.min_j + column_offset = int(vicinity.layer_min_i[original_layer]) - vicinity.min_i + trailing_rows = vicinity.max_j - int(vicinity.layer_max_j[original_layer]) + trailing_columns = vicinity.max_i - int(vicinity.layer_max_i[original_layer]) + corner_porv = _corner_pore_volumes(dck, vicinity, original_layer) + south = _map_south_boundary( + dck, + vicinity, + layer_index, + original_layer, + column_offset, + row_offset, + trailing_columns, + ) + north = _map_north_boundary( + dck, + vicinity, + layer_index, + original_layer, + column_offset, + trailing_rows, + trailing_columns, + ) + east = _map_east_boundary( + dck, + vicinity, + layer_index, + original_layer, + column_offset, + row_offset, + trailing_rows, + ) + west = _map_west_boundary( + dck, + vicinity, + layer_index, + original_layer, + row_offset, + trailing_rows, + trailing_columns, + ) + _apply_layer_pore_volume_correction( + dck, + vicinity, + layer_index, + column_offset, + row_offset, + trailing_columns, + trailing_rows, + south, + north, + east, + west, + corner_porv, + ) + _distribute_vertical_pore_volume(dck, vicinity) + mapped_porv = float(np.sum(dck.output_porv)) + expected_porv = float( + np.sum(vicinity.layer_external_porv) + np.sum(vicinity.layer_selected_porv) + ) + if dck.pore_volume_correction == 0 and original_porv is not None: + dck.output_porv = original_porv + elif dck.pore_volume_correction in (1, 2, 3): + correction_indices = np.flatnonzero(vicinity.source_indices != 0) + if correction_indices.size > 0: + dck.output_porv[correction_indices] += ( + expected_porv - mapped_porv + ) / correction_indices.size + else: + correction_frequency = int(np.sum(vicinity.active_counts)) + active_indices = np.flatnonzero(dck.output_actnum > 0) + if correction_frequency > 0: + dck.output_porv[active_indices] += ( + expected_porv - mapped_porv + ) / correction_frequency
+ +
+ +
+ + + + + +
+ +
+
+
+ +
+ + + + +
+ + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/_sources/about.rst.txt b/docs/_sources/about.rst.txt deleted file mode 100644 index ca75d25..0000000 --- a/docs/_sources/about.rst.txt +++ /dev/null @@ -1,12 +0,0 @@ -============ -About pycopm -============ - -.. image:: ./figs/pycopm.png - -The **pycopm** tool is being funded by the `Center for Sustainable Subsurface Resources (CSSR) `_ [project no. 331841] -and by `Expansion of Resources for CO2 Storage on the Horda Platform (ExpReCCS) `_ [project no. 336294]. - -Contributions are more than welcome using the fork and pull request approach. - -For a new feature, please request this raising an issue. diff --git a/docs/_sources/api.rst.txt b/docs/_sources/api.rst.txt index 9840f1f..d585672 100644 --- a/docs/_sources/api.rst.txt +++ b/docs/_sources/api.rst.txt @@ -1,7 +1,7 @@ -================= -pycopm Python API -================= +.. _api-reference: +Python API +========== The main script for the **pycopm** executable is located in the core folder. The different jobs called by ERT are located in the jobs folder. The reference_simulation folder contains the generated files after running Flow in the norne and drogon case in the opm-test folder. @@ -11,6 +11,9 @@ grid refinement, grid coarsening, submodels, and transformations. .. figure:: figs/contents.png - Files in the pyocpm package. + Files in the pycopm package. -.. include:: modules.rst +.. toctree:: + :maxdepth: 2 + + api/modules diff --git a/docs/_sources/modules.rst.txt b/docs/_sources/api/modules.rst.txt similarity index 100% rename from docs/_sources/modules.rst.txt rename to docs/_sources/api/modules.rst.txt diff --git a/docs/_sources/pycopm.core.pycopm.rst.txt b/docs/_sources/api/pycopm.core.pycopm.rst.txt similarity index 100% rename from docs/_sources/pycopm.core.pycopm.rst.txt rename to docs/_sources/api/pycopm.core.pycopm.rst.txt diff --git a/docs/_sources/pycopm.core.rst.txt b/docs/_sources/api/pycopm.core.rst.txt similarity index 100% rename from docs/_sources/pycopm.core.rst.txt rename to docs/_sources/api/pycopm.core.rst.txt diff --git a/docs/_sources/pycopm.rst.txt b/docs/_sources/api/pycopm.rst.txt similarity index 100% rename from docs/_sources/pycopm.rst.txt rename to docs/_sources/api/pycopm.rst.txt diff --git a/docs/_sources/pycopm.utils.coarsening.rst.txt b/docs/_sources/api/pycopm.utils.coarsening.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.coarsening.rst.txt rename to docs/_sources/api/pycopm.utils.coarsening.rst.txt diff --git a/docs/_sources/pycopm.utils.files_writer.rst.txt b/docs/_sources/api/pycopm.utils.files_writer.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.files_writer.rst.txt rename to docs/_sources/api/pycopm.utils.files_writer.rst.txt diff --git a/docs/_sources/pycopm.utils.generate_decks.rst.txt b/docs/_sources/api/pycopm.utils.generate_decks.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.generate_decks.rst.txt rename to docs/_sources/api/pycopm.utils.generate_decks.rst.txt diff --git a/docs/_sources/pycopm.utils.input_values.rst.txt b/docs/_sources/api/pycopm.utils.input_values.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.input_values.rst.txt rename to docs/_sources/api/pycopm.utils.input_values.rst.txt diff --git a/docs/_sources/pycopm.utils.parser_deck.rst.txt b/docs/_sources/api/pycopm.utils.parser_deck.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.parser_deck.rst.txt rename to docs/_sources/api/pycopm.utils.parser_deck.rst.txt diff --git a/docs/_sources/pycopm.utils.refinement.rst.txt b/docs/_sources/api/pycopm.utils.refinement.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.refinement.rst.txt rename to docs/_sources/api/pycopm.utils.refinement.rst.txt diff --git a/docs/_sources/pycopm.utils.rst.txt b/docs/_sources/api/pycopm.utils.rst.txt similarity index 95% rename from docs/_sources/pycopm.utils.rst.txt rename to docs/_sources/api/pycopm.utils.rst.txt index 32cd074..47fcac7 100644 --- a/docs/_sources/pycopm.utils.rst.txt +++ b/docs/_sources/api/pycopm.utils.rst.txt @@ -14,6 +14,7 @@ Submodules pycopm.utils.parser_deck pycopm.utils.refinement pycopm.utils.runs_executer + pycopm.utils.terminal pycopm.utils.transformation pycopm.utils.vicinity diff --git a/docs/_sources/pycopm.utils.runs_executer.rst.txt b/docs/_sources/api/pycopm.utils.runs_executer.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.runs_executer.rst.txt rename to docs/_sources/api/pycopm.utils.runs_executer.rst.txt diff --git a/docs/_sources/api/pycopm.utils.terminal.rst.txt b/docs/_sources/api/pycopm.utils.terminal.rst.txt new file mode 100644 index 0000000..54f8943 --- /dev/null +++ b/docs/_sources/api/pycopm.utils.terminal.rst.txt @@ -0,0 +1,8 @@ +pycopm.utils.terminal module +============================ + +.. automodule:: pycopm.utils.terminal + :members: + :private-members: + :show-inheritance: + :undoc-members: diff --git a/docs/_sources/pycopm.utils.transformation.rst.txt b/docs/_sources/api/pycopm.utils.transformation.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.transformation.rst.txt rename to docs/_sources/api/pycopm.utils.transformation.rst.txt diff --git a/docs/_sources/pycopm.utils.vicinity.rst.txt b/docs/_sources/api/pycopm.utils.vicinity.rst.txt similarity index 100% rename from docs/_sources/pycopm.utils.vicinity.rst.txt rename to docs/_sources/api/pycopm.utils.vicinity.rst.txt diff --git a/docs/_sources/command-line.rst.txt b/docs/_sources/command-line.rst.txt new file mode 100644 index 0000000..05dd4dc --- /dev/null +++ b/docs/_sources/command-line.rst.txt @@ -0,0 +1,43 @@ +.. _command-line-reference: + +Command-line reference +====================== + +A **pycopm** command selects a generic OPM Flow ``.DATA`` deck or a legacy TOML +configuration: + +.. code-block:: console + + pycopm -i INPUT [OPTIONS] + +The reference is organized by task. On each child page, every flag is an +explicit subsection heading, so the flags appear in the right-hand page table +of contents. The Sphinx ``option`` directives remain in place for stable +cross-references. + +.. note:: + + Generic deck input is the actively developed workflow. All options apply to + ``.DATA`` decks. Only ``-i``, ``-o``, ``-f``, and ``-precision`` apply to + legacy TOML files. + +.. toctree:: + :maxdepth: 1 + + options/workflow + options/grid + options/aggregation + options/submodels-transformations + options/output + options/compatibility + +Common commands +--------------- + +.. code-block:: console + + pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output + pycopm -i MODEL.DATA -z 1:10,11:15 -a min,mode -p 1 -m all + pycopm -i MODEL.DATA -v 'WELLN box [-1,1] [-2,2] [0,3]' -p 1 -m all + pycopm -i MODEL.DATA -d 'rotatexy 45' -m all + pycopm -i MODEL6.DATA -z 1:4 -dual 'poro <= 0.1, vertical TF = 0' -m all diff --git a/docs/_sources/configuration/complete-reference.rst.txt b/docs/_sources/configuration/complete-reference.rst.txt new file mode 100644 index 0000000..60872d9 --- /dev/null +++ b/docs/_sources/configuration/complete-reference.rst.txt @@ -0,0 +1,141 @@ +.. _legacy-configuration-complete: + +Complete legacy configuration reference +======================================= + +This page preserves the complete original TOML documentation, including the full arrays, parameter explanations, ERT guidance, figures, and tips. + +.. Note:: + The configuration files allow to set the integrated studies (coarsening and history matching) + only for the drogon and norne model. To use **pycopm** in any given OPM Flow geological model + to generate modified files (coarsening, refinement, submodels, and transformations), this can + be achieved without a configuration file, but setting the parameters via command lines (see + the :doc:`../command-line` or run `pycopm -h` for the definition of the argument options, as well as + the examples in :doc:`../examples`.) + + +Here we use as an example one of the configuration files used in the tests +(see `input.toml `_). +The first input parameter is: + +.. code-block:: python + :linenos: + + # Set mpirun, the full path to the flow executable, and simulator flags (except --output-dir) + flow_command = "flow --newton-min-iterations=1" + +If **flow** is not in your path, then write the full path to the executable, as well as adding mpirun +if this is supported in your machine (e.g., flow_command = "mpirun -np 8 /Users/dmar/Github/opm/build/opm-simulators/bin/flow -\-newton-min-iterations=1"). + +The next entries define the following parameters: + +.. code-block:: python + :linenos: + :lineno-start: 4 + + # Set the model parameters + model_name = "norne" # Geological model (norne or drogon) + execution_mode = "single-run" # Mode to run (single-run, files, or ert) + x_coarsening = [0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0] # Array of x-coarsening + y_coarsening = [0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0] # Array of y-coarsening + z_coarsening = [0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0] # Array of z-coarsening + ensemble_size = 2 # Number of ensembles + max_parallel_realizations = 2 # Maximum number of ensembles running in parallel + max_realization_runtime_seconds = 600 # Maximum runtime in seconds of a realization. A value of 0 means unlimited runtime. + min_successful_realizations = 2 # Minimum number of realizations that must have succeeded for the simulation to be regarded as a success. + random_seed = 7 # Set a specific seed for reproducibility. A value of 0 means no seed. + saturation_function_method = 0 # Select which coarser deck to use: 0 -> default one or 1 -> LET sat functions + satnum_generation_method = 1 # For norne: for the LET coarser deck, select: 0 -> SATNUM=1, 1 -> SATNUM is computed from Sandve et al 2022, 2 -> #SATNUM=#Cells. + pore_volume_correction = 0 # 0 -> no corrections for lost porv, 1 -> correct it on all cells + initialization_method = 0 # Initialization 0 -> Equil 1->INIT from fine-scale + observation_relative_errors = [0.1,0.1,0.1] # Error WWPR, WOPR, and WGPR + observation_minimum_errors = [100,100,100000] # Minimum error of WWPR, WOPR, and WGPR + history_matching_end_date = 2005-03-01 # Last date to HM + cleanup_file_suffixes = ["PRT"] # Delete files with this suffix to save storage + +The single-run mode results in Flow running only one simulation and the information shown in +the terminal is the one from the Flow executable instead of the ERT one. The ert option calls the ERT +executable and the command values are given after in the same input file which are discussed later. The +files option only writes the needed input files for ERT. + +The x-, y, and z-coarsening inputs define which pillars are removed (value 2) in the x, y, and z direction respectively. + +.. figure:: ../figs/index.png +.. figure:: ../figs/index_plopm.png + + I, J, and K cell index in the standard Norne data set (top figures from `ResInsight `_ and bottom figures from `plopm `_, see/run `docs_configs_views.sh `_). + +Values of 0 do not remove the pilar. + +.. tip:: + The 0 values are used to keep pillars in order to honor the main Norne shape. Then from the + example provided here, change the 2's to 0's to add back the pillars. + +The remaining lines define the number of ensembles, maximum number of ensembles running in parallel, the maximum runtime in seconds to stop a realization +(0 means unlimited time), the minimum number of realizations for the simulation to be regarded as a success, +the random seed for reproducibility (0 means no seed), to use the saturation +functions from opm-tests norne or to use the LET saturation functions, defining if each of the coarser cells is +considered as only one region, regions as in Sandve et al 2022 or as a different SATNUM region, and the cporv entry +sets if the ntg and poro properties on the cells on the boundary are modified to account for lost PV respect to the +default simulation or if the porosity of all cells is modified to match this value. The last entry set the type of files to +delete after each realization is completed to save memory. + +If the ert option is activated, then the following input: + +.. code-block:: python + :linenos: + :lineno-start: 25 + + # Set the command line for the ert executable (gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda) and flags + ert_arguments = "es_mda --weights 1" + +sets the type of ert option and command flags. Currently, the options supported are gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda. +Confer to the `ERT `_ documentation for a full description of these options. + +The LET saturation function parameters for each of the coarsened cells are given in the following entry: + +.. code-block:: python + :linenos: + :lineno-start: 28 + + # Properties LET saturation functions: name, value, use dist in hm?, dist, distpara, distpara + let_parameters = [["lw", 3, 1, "UNIFORM", 1.1, 5], + ["ew", 1, 1, "UNIFORM", -1, 2], + ["tw", 3, 1, "UNIFORM", 1.1, 5], + ["lo", 3, 1, "UNIFORM", 1.1, 5], + ["eo", 1, 1, "UNIFORM", -1, 2], + ["to", 3, 1, "UNIFORM", 1.1, 5], + ["lg", 3, 1, "UNIFORM", 1.1, 5], + ["eg", 1, 1, "UNIFORM", -1, 2], + ["tg", 3, 1, "UNIFORM", 1.1, 5], + ["log", 3, 1, "UNIFORM", 1.1, 5], + ["eog", 1, 1, "UNIFORM", -1, 2], + ["tog", 1, 1, "UNIFORM", 1.1, 5], + ["lmlto", 1.5, 1, "UNIFORM", 1, 2], + ["emlto", 1, 1, "UNIFORM", 0.9, 2.1], + ["tmlto", 1.5, 1, "UNIFORM", 1, 2], + ["lmltg", 1.5, 1, "UNIFORM", 1, 2], + ["emltg", 1, 1, "UNIFORM", 0.9, 2.1], + ["tmltg", 1.5, 1, "UNIFORM", 1, 2]] + +The 'use dist in hm?' defines if the property will be history match (1) or the provided value in the second entry will be used (0). + +Finally, we set if the permeabilities will be considered for the hm: + +.. code-block:: python + :linenos: + :lineno-start: 48 + + # Coarser rock properties: name, use dist in hm?, coarsing approach (max or mean) + rock_property_settings = [["PERMX", 1, "max"], + ["PERMY", 0, "max"], + ["PERMZ", 0, "max"]] + +If option 1 is selected, then the distributions are UNIFORM with interval values of [perm_min, perm_max], corresponding to the +minimum and maximum values in each of the coarser cells. + +.. tip:: + By setting the "mode = 'files'" in the toml configuration file, only the needed files to run a history matching using + ERT are generated. Then one can inspect those files and do additional modifications before running the history matching by + calling directly the ert executable. If you are not familiar with the format of the files to use ERT, visit + `the ERT documentation `_. diff --git a/docs/_sources/configuration_file.rst.txt b/docs/_sources/configuration_file.rst.txt index 509f3d0..f7ff1f5 100644 --- a/docs/_sources/configuration_file.rst.txt +++ b/docs/_sources/configuration_file.rst.txt @@ -1,137 +1,17 @@ -================== -Configuration file -================== -.. Note:: - The configuration files allow to set the integrated studies (coarsening and history matching) - only for the drogon and norne model. To use **pycopm** in any given OPM Flow geological model - to generate modified files (coarsening, refinement, submodels, and transformations), this can - be achieved without a configuration file, but setting the parameters via command lines (see - the :ref:`overview` or run `pycopm -h` for the definition of the argument options, as well as - the examples in :ref:`generic`.) +.. _configuration-file: +Legacy TOML configuration +========================= -Here we use as an example one of the configuration files used in the tests -(see `input.toml `_). -The first input parameter is: +TOML configuration files define integrated coarsening and history-matching studies for the predefined Norne and Drogon models. -.. code-block:: python - :linenos: +.. warning:: - # Set mpirun, the full path to the flow executable, and simulator flags (except --output-dir) - flow_command = "flow --newton-min-iterations=1" - -If **flow** is not in your path, then write the full path to the executable, as well as adding mpirun -if this is supported in your machine (e.g., flow_command = "mpirun -np 8 /Users/dmar/Github/opm/build/opm-simulators/bin/flow -\-newton-min-iterations=1"). + This workflow is retained for legacy, model-specific studies. Current development targets generic OPM Flow deck input. New users should start with :doc:`tutorial` and :doc:`command-line`. -The next entries define the following parameters: +Only ``-i``, ``-o``, ``-f``, and ``-precision`` apply on the command line when a TOML file is selected. -.. code-block:: python - :linenos: - :lineno-start: 4 +.. toctree:: + :maxdepth: 1 - # Set the model parameters - model_name = "norne" # Geological model (norne or drogon) - execution_mode = "single-run" # Mode to run (single-run, files, or ert) - x_coarsening = [0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0] # Array of x-coarsening - y_coarsening = [0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0] # Array of y-coarsening - z_coarsening = [0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0] # Array of z-coarsening - ensemble_size = 2 # Number of ensembles - max_parallel_realizations = 2 # Maximum number of ensembles running in parallel - max_realization_runtime_seconds = 600 # Maximum runtime in seconds of a realization. A value of 0 means unlimited runtime. - min_successful_realizations = 2 # Minimum number of realizations that must have succeeded for the simulation to be regarded as a success. - random_seed = 7 # Set a specific seed for reproducibility. A value of 0 means no seed. - saturation_function_method = 0 # Select which coarser deck to use: 0 -> default one or 1 -> LET sat functions - satnum_generation_method = 1 # For norne: for the LET coarser deck, select: 0 -> SATNUM=1, 1 -> SATNUM is computed from Sandve et al 2022, 2 -> #SATNUM=#Cells. - pore_volume_correction = 0 # 0 -> no corrections for lost porv, 1 -> correct it on all cells - initialization_method = 0 # Initialization 0 -> Equil 1->INIT from fine-scale - observation_relative_errors = [0.1,0.1,0.1] # Error WWPR, WOPR, and WGPR - observation_minimum_errors = [100,100,100000] # Minimum error of WWPR, WOPR, and WGPR - history_matching_end_date = 2005-03-01 # Last date to HM - cleanup_file_suffixes = ["PRT"] # Delete files with this suffix to save storage - -The single-run mode results in Flow running only one simulation and the information shown in -the terminal is the one from the Flow executable instead of the ERT one. The ert option calls the ERT -executable and the command values are given after in the same input file which are discussed later. The -files option only writes the needed input files for ERT. - -The x-, y, and z-coarsening inputs define which pillars are removed (value 2) in the x, y, and z direction respectively. - -.. figure:: figs/index.png -.. figure:: figs/index_plopm.png - - I, J, and K cell index in the standard Norne data set (top figures from `ResInsight `_ and bottom figures from `plopm `_, see/run `docs_configs_views.sh `_). - -Values of 0 do not remove the pilar. - -.. tip:: - The 0 values are used to keep pillars in order to honor the main Norne shape. Then from the - example provided here, change the 2's to 0's to add back the pillars. - -The remaining lines define the number of ensembles, maximum number of ensembles running in parallel, the maximum runtime in seconds to stop a realization -(0 means unlimited time), the minimum number of realizations for the simulation to be regarded as a success, -the random seed for reproducibility (0 means no seed), to use the saturation -functions from opm-tests norne or to use the LET saturation functions, defining if each of the coarser cells is -considered as only one region, regions as in Sandve et al 2022 or as a different SATNUM region, and the cporv entry -sets if the ntg and poro properties on the cells on the boundary are modified to account for lost PV respect to the -default simulation or if the porosity of all cells is modified to match this value. The last entry set the type of files to -delete after each realization is completed to save memory. - -If the ert option is activated, then the following input: - -.. code-block:: python - :linenos: - :lineno-start: 25 - - # Set the command line for the ert executable (gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda) and flags - ert_arguments = "es_mda --weights 1" - -sets the type of ert option and command flags. Currently, the options supported are gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda. -Confer to the `ERT `_ documentation for a full description of these options. - -The LET saturation function parameters for each of the coarsened cells are given in the following entry: - -.. code-block:: python - :linenos: - :lineno-start: 28 - - # Properties LET saturation functions: name, value, use dist in hm?, dist, distpara, distpara - let_parameters = [["lw", 3, 1, "UNIFORM", 1.1, 5], - ["ew", 1, 1, "UNIFORM", -1, 2], - ["tw", 3, 1, "UNIFORM", 1.1, 5], - ["lo", 3, 1, "UNIFORM", 1.1, 5], - ["eo", 1, 1, "UNIFORM", -1, 2], - ["to", 3, 1, "UNIFORM", 1.1, 5], - ["lg", 3, 1, "UNIFORM", 1.1, 5], - ["eg", 1, 1, "UNIFORM", -1, 2], - ["tg", 3, 1, "UNIFORM", 1.1, 5], - ["log", 3, 1, "UNIFORM", 1.1, 5], - ["eog", 1, 1, "UNIFORM", -1, 2], - ["tog", 1, 1, "UNIFORM", 1.1, 5], - ["lmlto", 1.5, 1, "UNIFORM", 1, 2], - ["emlto", 1, 1, "UNIFORM", 0.9, 2.1], - ["tmlto", 1.5, 1, "UNIFORM", 1, 2], - ["lmltg", 1.5, 1, "UNIFORM", 1, 2], - ["emltg", 1, 1, "UNIFORM", 0.9, 2.1], - ["tmltg", 1.5, 1, "UNIFORM", 1, 2]] - -The 'use dist in hm?' defines if the property will be history match (1) or the provided value in the second entry will be used (0). - -Finally, we set if the permeabilities will be considered for the hm: - -.. code-block:: python - :linenos: - :lineno-start: 48 - - # Coarser rock properties: name, use dist in hm?, coarsing approach (max or mean) - rock_property_settings = [["PERMX", 1, "max"], - ["PERMY", 0, "max"], - ["PERMZ", 0, "max"]] - -If option 1 is selected, then the distributions are UNIFORM with interval values of [perm_min, perm_max], corresponding to the -minimum and maximum values in each of the coarser cells. - -.. tip:: - By setting the "mode = 'files'" in the toml configuration file, only the needed files to run a history matching using - ERT are generated. Then one can inspect those files and do additional modifications before running the history matching by - calling directly the ert executable. If you are not familiar with the format of the files to use ERT, visit - `the ERT documentation `_. + configuration/complete-reference diff --git a/docs/_sources/examples.rst.txt b/docs/_sources/examples.rst.txt index 84c19e2..b586128 100644 --- a/docs/_sources/examples.rst.txt +++ b/docs/_sources/examples.rst.txt @@ -1,6 +1,16 @@ -******** +.. _examples: + Examples -******** +======== + +Deck-based examples are the recommended and actively developed path. The legacy configuration example is retained for existing Norne and Drogon studies. + +.. warning:: + + For new applications, start with :doc:`tutorial/hello-world` and the deck-based examples. TOML workflows are legacy and model-specific. + +Visualization and reproducibility +--------------------------------- For additional examples demonstrating the applicability of **pycopm**, see the `tests `_. @@ -34,299 +44,76 @@ For additional examples demonstrating the applicability of **pycopm**, see the ` . ./tests/scripts/name_of_script.sh -======================= -Via configuration files -======================= - -The `examples `_ folder contains configuration files -to perform HM studies in drogon and norne using `ERT `_. For example, by executing inside the `example folder for drogon `_: - -.. code-block:: bash - - # From inside the main pycopm folder - cd examples/configurations/drogon - pycopm -i input.toml -o drogon_coarser - -The following are the drogon model from `opm-tests `_ and coarsened model generated using **pycopm** using ResInsight for the visualization: - -.. figure:: figs/drogon_coarser.png -.. figure:: figs/drogon_coarser_plopm.png - - Initial oil saturation from the input (left) and coarsened (right) models (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_config_drogon.sh `_). - -.. note:: - For Drogon, a scored is printed after the run to compared the error to the results available at - https://webviz-subsurface-example.azurewebsites.net/history-match. While `input.toml `_ - only runs one HM iteration with two ensemble members that is used in testing **pycopm**, `hm.toml `_ runs a history matching - with a better score (i.e., less error compare to the observation data). This configuration file is also an example of how to use mpi to run Flow built from source - (set the flow path to your flow location; if you do not have mpi, you can remove it and still run the example). - - -.. _generic: - -================== -Via OPM Flow decks -================== +Deck workflow scope +------------------- The current development of **pycopm** focuses on creating tailored models (grid refinement, grid coarsening, submodels, and transformations) by using input decks. While in the Hello world example these four different options are demonstrated, for the latter examples the focus is on the grid coarsening functionality, and the -SPE10 also shows the submodel functionality. - -Hello world ------------ -For the `HELLO_WORLD.DATA `_ deck, by executing: - -.. code-block:: bash - - # From inside the main pycopm folder - cd examples/decks - pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output - -.. note:: - - If the folder to flow is not added to your path, then pass the full path to the flow executable using the flag **-f /path/to/flow**. - -Using `plopm `_, then we can visualize the generated files in the output folder: - -.. figure:: figs/hello_world_1.png - - Dry run from the input cloned deck (left) and (right) coarsened model. Adding the flag **-p 1** would add the remove pore volume to the neighbouring cells (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). - -As mentioned above, if you do not have ResInsight, then to visualize the results in ParaView run - -.. code-block:: bash - - flow HELLO_WORLD.DATA --enable-vtk-output=true - flow HELLO_WORLD_PYCOPM.DATA --enable-vtk-output=true - -To make active the coarsened cell where there is only one active cell, this can be achieved by: - -.. code-block:: bash - - pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -a max - -.. figure:: figs/hello_world_2.png - - Dry run from the input cloned deck (left) and (right) coarsened model. The region numbers by default are given by the mode, e.g., use the flag **-n max** to keep the maximum integer (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). - -As described in the :doc:`theory <./theory>`, **pycopm** can be not only used for grid coarsening, but also to apply grid refinements, submodels, and transformations. -Then, with the following commands first we substract a submodel around the isolated grid cell proyecting the outside pore volume on the boundaries, after -we apply a grid refinement on the cells in the middle x and y location, and finally we rotate the model 45 degrees. - -.. code-block:: bash - - pycopm -i HELLO_WORLD.DATA -v 'xypolygon [4,8.5] [4,16.5] [11.5,16.5] [11.5,8.5] [4,8.5]' -p 1 -m all - pycopm -i HELLO_WORLD_PYCOPM.DATA -rx 0,0,0,2,0,0,0 -ry 0,0,0,2,0,0,0 -m all - pycopm -i HELLO_WORLD_PYCOPM_PYCOPM.DATA -d 'rotatexy 45' -m all - -.. figure:: figs/hello_world_3.png - - Extracted region with the projected pore volumes (bottom left), refinement around the center cells (top right), and rotation (bottom right). - The text in the legends highlight that the pore volume is conserved (35.58) and the number of active cells is reduced from 351 to 25 in the - submodel and after increased to 41 due to the grid refinement (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). - -.. note:: - - To write the cell values for the SOLUTION section instead of using the EQUIL keyword, this can be achieved by the flag **-explicit 1**; the - only requirement is that the EQUIL keyword needs to be in the main input DATA file and no via INCLUDE files. - -Smeaheia --------- - -By downloading the `Smeaheia simulation model `_ (dataset part Simulation models), -then: - -.. code-block:: bash - - # From the download folders - cd Simulation_Models/data - pycopm -c 5,4,1 -a min -m all -i Statoil_Feasibility_sim_model_with_depletion_KROSS_INJ_SECTOR_20.DATA -o . - -will generate a coarser model five times in the x direction and four in the y direction, where the coarse cell is -made inactive if at least one cell is inactive (**-a min**). - -We use our `plopm `_ friend to generate PNG figures: - -.. code-block:: bash - - plopm -i 'STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PREP_PYCOPM_DRYRUN STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PYCOPM' -s ,,1 -v poro -subfigs 1,2 -save smeaheia -t 'Smeaheia Coarsened Smeaheia' -delax 1 -xunits km -xformat .0f -yunits km -yformat .0f -d 5,4.5 -suptitle 0 -c cet_rainbow_bgyrm_35_85_c69 -cbsfax 0.2,0.95,0.6,0.02 -cformat .2f - -.. figure:: figs/smeaheia.png - - Top view of porosity values for the (left) original and (right) coarsened model (figures using plopm, see/run `docs_via_deck_smeaheia.sh `_). - -.. _drogon: - -Drogon ------- - -.. note:: - In the current implementation of the **pycopm** tool, the handling of properties that requires definitions of i,j,k indices - (e.g., FAULTS, WELLSPECS) are assumed to be defined in the main .DATA deck. Then, in order to use **pycopm** for simulation models - where these properties are define via include files, replace those includes in the .DATA deck with the actual content of the include files. - Here are some relevant keywords per deck section that need to be in the main input deck and not via include files: - - SECTION GRID: MAPAXES, FAULTS, MULTREGT (other keywords like MULTZ, NTG, or definitions/operations for perms and poro can be in included files since - permx, permy, permz, poro, porv, multx, multy, multz are read from the .INIT file) - - SECTION PROPS: EQUALS, COPY, ADD, and MULTIPLY since this involve i,j,k indices and are applied to properties such as saturation functions parameters that - are still given in the same input format in the generated deck. In addition, SWATINIT if used in the deck, is read from the .INIT file and output for the - modified deck in a new file, then one might need to give the right include path to this special case. - - SECTION SCHEDULE: All keywords in this section must be in the input deck and no via include viles. - -Following the note above, then by downloading the `DROGON model `_, adding the `MAPAXES `_ -to the deck, replacing the lines in `DROGON_HIST.DATA `_ for the FAULTS (L127-128) and SCHEDULE (L242-243) with the actual content of those include files, then by executing: - -.. code-block:: bash - - pycopm -i DROGON_HIST.DATA -c 1,1,3 -p 1 -q 1 -l C1 - pycopm -i DROGON_HIST_PYCOPM.DATA -c 1,3,1 -p 1 -q 1 -j 2.5 -l C2 -m all - -this would generate the following coarsened model: - -.. figure:: figs/drogon_generic.png -.. figure:: figs/drogon_generic_plopm.png - - Note that the total pore volume is conserved for the coarsened model (right). The properties of the standard model (left) can be visualized using the DROGON_HIST_PREP_PYCOPM_DRYRUN generated files (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_deck_drogon.sh `_). - -Here, we first coarse in the z direction, which reduces the number of cells from 31 to 11, and after we coarse in the y direction. -After trial and error, the jump (**-j**) is set to 2.5 to avoid generated connections across the faults. For geological models with a lot of -inactive cells and faults, this divide and conquer apporach is recommended, i.e., coarsening first in the z direction and after coarsening -in the x and y directions. Also, we add labels (**-l**) C1 and C2 to differentiate between the coarse include files. In addition, we use the -flags **-p 1 -q 1** to add the remove pore volume to the closest coarser cells and to redistribute the pore volume in the locations with -gas and oil, this results in the coarsened model having the same total pore volume, field gas in place, and practically same oil and water in -place as the input model. - -.. note:: - Add to the generated deck the removed include files in the grid section related to the region operations (e.g., - ../include/grid/drogon.multregt for this case). - -Now, we also show a two times coarsened model in all directions (referring to the previous comment about divide and conquer, for the Drogon model -it seems still ok to do a two times coarsening in one go): - -.. code-block:: bash - - pycopm -i DROGON_HIST.DATA -c 2,2,2 -p 1 -q 1 -j 4 -w DROGON_2TIMES_COARSER -m all - -Here, we use the **-w** flag to give a specific name to the generated coarsened deck, as well as using a higher value of **-j** to avoid generated connections across the faults. - -.. tip:: - To use a different approach from the default ones (see the :doc:`theory <./theory>`) to coarse one of the properties (e.g., permeabilities), this can - be achieve by the **-s** flag, e.g., **-s pvmean** to coarse the permeabilities using a pv-weighted mean. In addition, one could add a different label - **-l pvweightedperms** to identify the generated .INC files with the permeabilities, and rename these files in order to be used in the coarserned model with the rest - of the properties using the default aproaches or a combination of them (e.g., **-s max -l maxpermz** and keep the maximum values of permz). - -If we run these three models using OPM Flow: - -.. code-block:: bash - - flow DROGON_HIST.DATA - flow DROGON_HIST_PYCOPM_PYCOPM - flow DROGON_2TIMES_COARSER - -then we can compare the summary vectors. To this end, we use our good old friend `plopm `_: - -.. code-block:: bash - - plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v 'FOIP,FOPR,TCPU' -tunits y -f 14 -subfigs 2,2 -delax 1 -loc empty,empty,empty,center -d 10,5 -xformat '.1f' -xlnum 6 -ylabel 'sm$^3$ sm$^3$/day seconds' -t 'Field oil in place Field oil production rate Simulation time' -labels 'DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER' -save drogon_pycopm_comparison -yformat '.2e,.0f,.0f' - -.. figure:: figs/drogon_pycopm_comparison.png - - Note that the coarsened models have the same initial field oil in place as the input model. It seems the coarsened properties (e.g., permeabilities) - are good initial inputs to use in a history matching framework (e.g., to history match saturation function parameters), and the lower simulation - time for the coarsened models allow for more ensemble members and more iterations (figures using plopm, see/run `docs_via_deck_drogon.sh `_). - -We can also make a nice GIF by executing: - -.. code-block:: bash - - plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v sgas -subfigs 1,3 -d 15,11 -cnum 5 -m gif -xlnum 4 -ylnum 4 -dpi 300 -t "DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER" -f 16 -interval 2000 -loop 1 -cformat .2f -cbsfax 0.15,0.93,0.7,0.02 -s ,,1 -rotate '-30' -xunits km -yunits km -xformat .0f -yformat .0f -c cet_rainbow_bgyrm_35_85_c69 -delax 1 -tunits tstep - -.. figure:: figs/sgas.gif - - Top view of the Drogon and the two coarsened models (figures using plopm, see/run `docs_via_deck_drogon.sh `_). - -Norne ------ -By downloading the `Norne model `_ (and replacing the needed include files as described in the previous -example, specially the include file `./INCLUDE/BC0407_HIST01122006.SCH `_ at the end of `NORNE_ATW2013.DATA `_ to run the example without errors), -then here we create a coarsened model by removing certain pilars in order to keep the main features of the geological model: - -.. code-block:: bash - - pycopm -i NORNE_ATW2013.DATA -s pvmean -x 0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0 -y 0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0 -z 0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0 -a min -p 1 -q 1 -m all - -this would generate the following coarsened model: - -.. figure:: figs/norne_vec.png -.. figure:: figs/norne_plopm.png - - Top view of Norne for the (top) pore volume and (bottom) gas saturation (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_deck_norne.sh `_). - -.. _spe10: - -SPE10 ------ - -By downloading the `SPE10_MODEL2 model `_, then: - -.. code-block:: bash - - pycopm -i SPE10_MODEL2.DATA -s pvmean -c 4,8,2 -m all - -generates a coarsened model from ca. 1 million cells to ca. 20 thousands cells. - -.. figure:: figs/spe10_model2_coarser.png +SPE10 also shows the submodel functionality. - Porosity values for the (left) original and (right) coarsened SPE10 model. +.. grid:: 1 1 2 2 + :gutter: 3 -To generate a submodel from the coarsened model around the injector 'INJ', this can be achieved by executing: + .. grid-item-card:: Legacy configuration workflows + :link: example-legacy-configurations + :link-type: ref + :img-top: figs/drogon_coarser.png -.. code-block:: bash + Existing Norne and Drogon TOML and ERT studies. - pycopm -i SPE10_MODEL2_PYCOPM.DATA -p 1 -v 'INJ diamondxy 5' -m all -w vicinity -l sub -m all + .. grid-item-card:: Smeaheia + :link: example-smeaheia + :link-type: ref + :img-top: figs/smeaheia.png -.. figure:: figs/vicinity.png -.. figure:: figs/spe10_plopm.png + Coarsen the Smeaheia simulation model. - Pore volume values for the coarsened and vicinity around the well INJ in the SPE10 model (top figures using ResInsight and bottom figures using plopm (running pycopm with the flag -p 0 instead of -p 1), see/run `docs_via_deck_spe10.sh `_). + .. grid-item-card:: Drogon + :link: drogon + :link-type: ref + :img-top: figs/sgas.gif + Prepare, coarsen, conserve fluids in place, compare, and animate Drogon. -Dual coarsening ---------------- + .. grid-item-card:: Norne + :link: example-norne + :link-type: ref + :img-top: figs/norne_plopm.png -The flag **-dual** allows to perform a coarsening by differentiating between net and non-net cells, resulting in -two coarsened grids. For example, using the `MODEL6.DATA `_: + Preserve the Norne geometry with directional coarsening. -.. code-block:: bash + .. grid-item-card:: SPE10 + :link: spe10 + :link-type: ref + :img-top: figs/spe10_plopm.png - pycopm -i MODEL6.DATA -z 1:4 -w STANDARD -l S -t 2 -a max - pycopm -i MODEL6.DATA -z 1:4 -w DUAL -dual 'poro <= 0.1' -l D -t 2 -a max - flow MODEL6.DATA - flow STANDARD.DATA - flow DUAL.DATA - plopm -i 'MODEL6 STANDARD DUAL' -v 'pressure - 0pressure' -subfigs 1,3 -delax 1 -cbsfax 0.1,0.95,0.8,0.02 -d 12,4 -suptitle 0 -z 0 -clabel 'Pressure increase end of simulation [bar]' -grid 'black,1e-2' + Coarsen SPE10 Model 2 and extract a well-centred submodel. -This results in the following figure, where the pressure on the most right cell compares better using the dual coarsening than the standard: + .. grid-item-card:: Dual coarsening + :link: example-dual-coarsening + :link-type: ref + :img-top: figs/dual_pressure-0pressure_i,1,k_t2.png -.. figure:: figs/dual_pressure-0pressure_i,1,k_t2.png + Compare standard and net/non-net-aware coarsening. - Figures using plopm, see/run `docs_via_deck_dual_coarsening.sh `_. + .. grid-item-card:: Graphical abstract + :link: example-graphical-abstract + :link-type: ref + :img-top: figs/pycopm.png -================== -Graphical abstract -================== + Reproduce the coarsening, submodel, refinement, and transformation illustrations. -Here we describe how to generate the geological model ilustrations in the `graphical abstract `_. -These five ilustrations are generated from the `DROGON_HIST.DATA `_ model, and the -visualization is achieve using `ResInsight `_. +The complete former Hello World example is now the :doc:`tutorial/hello-world`. -* Top figure: By running the `DROGON_HIST.DATA `_ using opm flow and visaluazing the static property pore volume. -* Coarsenings: This corresponds to the generated DROGON_HIST_PYCOPM_PYCOPM.DATA deck in :ref:`drogon`. -* Submodels: pycopm -i DROGON_HIST.DATA -v 'xypolygon [463739,5931508] [464872,5932123] [464401,5932862] [463284,5932209] [463739,5931508]' -w SUBMODELS -m all -* Refinements: pycopm -i SUBMODELS.DATA -g 2,2,2 -w REFINEMENTS -m all -* Transformations: pycopm -i DROGON_HIST.DATA -d 'rotatexy 45' -w TRANSFORMATIONS -m all +.. toctree:: + :hidden: + :maxdepth: 1 -Note that for ResInsight to show the wells, one needs to run the decks (e.g., flow REFINEMENTS.DATA). + examples/configuration-workflows + examples/smeaheia + examples/drogon + examples/norne + examples/spe10 + examples/dual-coarsening + examples/graphical-abstract diff --git a/docs/_sources/examples/configuration-workflows.rst.txt b/docs/_sources/examples/configuration-workflows.rst.txt new file mode 100644 index 0000000..64b49cb --- /dev/null +++ b/docs/_sources/examples/configuration-workflows.rst.txt @@ -0,0 +1,35 @@ +.. _example-legacy-configurations: + +Legacy configuration workflows +============================== + +.. warning:: + + This page describes the retained, model-specific Norne and Drogon TOML workflow. It is not the recommended starting point for new pycopm applications. + +The `examples `_ folder contains configuration files +to perform HM studies in drogon and norne using `ERT `_. For example, by executing inside the `example folder for drogon `_: + +.. code-block:: bash + + # From inside the main pycopm folder + cd examples/configurations/drogon + pycopm -i input.toml -o drogon_coarser + +The following are the drogon model from `opm-tests `_ and coarsened model generated using **pycopm** using ResInsight for the visualization: + +.. figure:: ../figs/drogon_coarser.png +.. figure:: ../figs/drogon_coarser_plopm.png + + Initial oil saturation from the input (left) and coarsened (right) models (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_config_drogon.sh `_). + +.. note:: + + For Drogon, a scored is printed after the run to compared the error to the results available at + https://webviz-subsurface-example.azurewebsites.net/history-match. While `input.toml `_ + only runs one HM iteration with two ensemble members that is used in testing **pycopm**, `hm.toml `_ runs a history matching + with a better score (i.e., less error compare to the observation data). This configuration file is also an example of how to use mpi to run Flow built from source + (set the flow path to your flow location; if you do not have mpi, you can remove it and still run the example). + + +.. _generic: diff --git a/docs/_sources/examples/drogon.rst.txt b/docs/_sources/examples/drogon.rst.txt new file mode 100644 index 0000000..86d872c --- /dev/null +++ b/docs/_sources/examples/drogon.rst.txt @@ -0,0 +1,91 @@ +.. _drogon: + +Drogon +------ + +.. note:: + In the current implementation of the **pycopm** tool, the handling of properties that requires definitions of i,j,k indices + (e.g., FAULTS, WELLSPECS) are assumed to be defined in the main .DATA deck. Then, in order to use **pycopm** for simulation models + where these properties are define via include files, replace those includes in the .DATA deck with the actual content of the include files. + Here are some relevant keywords per deck section that need to be in the main input deck and not via include files: + + SECTION GRID: MAPAXES, FAULTS, MULTREGT (other keywords like MULTZ, NTG, or definitions/operations for perms and poro can be in included files since + permx, permy, permz, poro, porv, multx, multy, multz are read from the .INIT file) + + SECTION PROPS: EQUALS, COPY, ADD, and MULTIPLY since this involve i,j,k indices and are applied to properties such as saturation functions parameters that + are still given in the same input format in the generated deck. In addition, SWATINIT if used in the deck, is read from the .INIT file and output for the + modified deck in a new file, then one might need to give the right include path to this special case. + + SECTION SCHEDULE: All keywords in this section must be in the input deck and no via include viles. + +Following the note above, then by downloading the `DROGON model `_, adding the `MAPAXES `_ +to the deck, replacing the lines in `DROGON_HIST.DATA `_ for the FAULTS (L127-128) and SCHEDULE (L242-243) with the actual content of those include files, then by executing: + +.. code-block:: bash + + pycopm -i DROGON_HIST.DATA -c 1,1,3 -p 1 -q 1 -l C1 + pycopm -i DROGON_HIST_PYCOPM.DATA -c 1,3,1 -p 1 -q 1 -j 2.5 -l C2 -m all + +this would generate the following coarsened model: + +.. figure:: ../figs/drogon_generic.png +.. figure:: ../figs/drogon_generic_plopm.png + + Note that the total pore volume is conserved for the coarsened model (right). The properties of the standard model (left) can be visualized using the DROGON_HIST_PREP_PYCOPM_DRYRUN generated files (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_deck_drogon.sh `_). + +Here, we first coarse in the z direction, which reduces the number of cells from 31 to 11, and after we coarse in the y direction. +After trial and error, the jump (**-j**) is set to 2.5 to avoid generated connections across the faults. For geological models with a lot of +inactive cells and faults, this divide and conquer apporach is recommended, i.e., coarsening first in the z direction and after coarsening +in the x and y directions. Also, we add labels (**-l**) C1 and C2 to differentiate between the coarse include files. In addition, we use the +flags **-p 1 -q 1** to add the remove pore volume to the closest coarser cells and to redistribute the pore volume in the locations with +gas and oil, this results in the coarsened model having the same total pore volume, field gas in place, and practically same oil and water in +place as the input model. + +.. note:: + Add to the generated deck the removed include files in the grid section related to the region operations (e.g., + ../include/grid/drogon.multregt for this case). + +Now, we also show a two times coarsened model in all directions (referring to the previous comment about divide and conquer, for the Drogon model +it seems still ok to do a two times coarsening in one go): + +.. code-block:: bash + + pycopm -i DROGON_HIST.DATA -c 2,2,2 -p 1 -q 1 -j 4 -w DROGON_2TIMES_COARSER -m all + +Here, we use the **-w** flag to give a specific name to the generated coarsened deck, as well as using a higher value of **-j** to avoid generated connections across the faults. + +.. tip:: + To use a different approach from the default ones (see the :doc:`theory <../theory>`) to coarse one of the properties (e.g., permeabilities), this can + be achieve by the **-s** flag, e.g., **-s pvmean** to coarse the permeabilities using a pv-weighted mean. In addition, one could add a different label + **-l pvweightedperms** to identify the generated .INC files with the permeabilities, and rename these files in order to be used in the coarserned model with the rest + of the properties using the default aproaches or a combination of them (e.g., **-s max -l maxpermz** and keep the maximum values of permz). + +If we run these three models using OPM Flow: + +.. code-block:: bash + + flow DROGON_HIST.DATA + flow DROGON_HIST_PYCOPM_PYCOPM + flow DROGON_2TIMES_COARSER + +then we can compare the summary vectors. To this end, we use our good old friend `plopm `_: + +.. code-block:: bash + + plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v 'FOIP,FOPR,TCPU' -tu y -fz 14 -sg 2,2 -rdl 1 -ll empty,empty,empty,center -fs 10,5 -xf '.1f' -xnt 6 -yl 'sm$^3$ sm$^3$/day seconds' -t 'Field oil in place Field oil production rate Simulation time' -llb 'DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER' -fn drogon_pycopm_comparison -yf '.2e,.0f,.0f' + +.. figure:: ../figs/drogon_pycopm_comparison.png + + Note that the coarsened models have the same initial field oil in place as the input model. It seems the coarsened properties (e.g., permeabilities) + are good initial inputs to use in a history matching framework (e.g., to history match saturation function parameters), and the lower simulation + time for the coarsened models allow for more ensemble members and more iterations (figures using plopm, see/run `docs_via_deck_drogon.sh `_). + +We can also make a nice GIF by executing: + +.. code-block:: bash + + plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v sgas -sg 1,3 -fs 15,11 -cbn 5 -m gif -xnt 4 -ynt 4 -dpi 300 -t "DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER" -fz 16 -gi 2000 -gl 1 -cbf .2f -cbp 0.15,0.93,0.7,0.02 -s ,,1 -rot '-30' -xu km -yu km -xf .0f -yf .0f -c cet_rainbow_bgyrm_35_85_c69 -rdl 1 -tu tstep + +.. figure:: ../figs/sgas.gif + + Top view of the Drogon and the two coarsened models (figures using plopm, see/run `docs_via_deck_drogon.sh `_). diff --git a/docs/_sources/examples/dual-coarsening.rst.txt b/docs/_sources/examples/dual-coarsening.rst.txt new file mode 100644 index 0000000..e61cc2c --- /dev/null +++ b/docs/_sources/examples/dual-coarsening.rst.txt @@ -0,0 +1,21 @@ +.. _example-dual-coarsening: + +Dual coarsening +=============== +The flag **-dual** allows to perform a coarsening by differentiating between net and non-net cells, resulting in +two coarsened grids. For example, using the `MODEL6.DATA `_: + +.. code-block:: bash + + pycopm -i MODEL6.DATA -z 1:4 -w STANDARD -l S -t 2 -a max + pycopm -i MODEL6.DATA -z 1:4 -w DUAL -dual 'poro <= 0.1' -l D -t 2 -a max + flow MODEL6.DATA + flow STANDARD.DATA + flow DUAL.DATA + plopm -i 'MODEL6 STANDARD DUAL' -v 'pressure - 0pressure' -sg 1,3 -rdl 1 -cbp 0.1,0.95,0.8,0.02 -fs 12,4 -st 0 -asp 0 -cbl 'Pressure increase end of simulation [bar]' -ge 'black,1e-2' + +This results in the following figure, where the pressure on the most right cell compares better using the dual coarsening than the standard: + +.. figure:: ../figs/dual_pressure-0pressure_i,1,k_t2.png + + Figures using plopm, see/run `docs_via_deck_dual_coarsening.sh `_. diff --git a/docs/_sources/examples/graphical-abstract.rst.txt b/docs/_sources/examples/graphical-abstract.rst.txt new file mode 100644 index 0000000..c26a00d --- /dev/null +++ b/docs/_sources/examples/graphical-abstract.rst.txt @@ -0,0 +1,18 @@ +.. _example-graphical-abstract: + +Graphical abstract +================== + +.. figure:: ../figs/pycopm.png + +Here we describe how to generate the geological model ilustrations in the `graphical abstract `_. +These five ilustrations are generated from the `DROGON_HIST.DATA `_ model, and the +visualization is achieve using `ResInsight `_. + +* Top figure: By running the `DROGON_HIST.DATA `_ using opm flow and visaluazing the static property pore volume. +* Coarsenings: This corresponds to the generated DROGON_HIST_PYCOPM_PYCOPM.DATA deck in :ref:`drogon`. +* Submodels: pycopm -i DROGON_HIST.DATA -v 'xypolygon [463739,5931508] [464872,5932123] [464401,5932862] [463284,5932209] [463739,5931508]' -w SUBMODELS -m all +* Refinements: pycopm -i SUBMODELS.DATA -g 2,2,2 -w REFINEMENTS -m all +* Transformations: pycopm -i DROGON_HIST.DATA -d 'rotatexy 45' -w TRANSFORMATIONS -m all + +Note that for ResInsight to show the wells, one needs to run the decks (e.g., flow REFINEMENTS.DATA). diff --git a/docs/_sources/examples/norne.rst.txt b/docs/_sources/examples/norne.rst.txt new file mode 100644 index 0000000..46d97ab --- /dev/null +++ b/docs/_sources/examples/norne.rst.txt @@ -0,0 +1,18 @@ +.. _example-norne: + +Norne +===== +By downloading the `Norne model `_ (and replacing the needed include files as described in the previous +example, specially the include file `./INCLUDE/BC0407_HIST01122006.SCH `_ at the end of `NORNE_ATW2013.DATA `_ to run the example without errors), +then here we create a coarsened model by removing certain pilars in order to keep the main features of the geological model: + +.. code-block:: bash + + pycopm -i NORNE_ATW2013.DATA -s pvmean -x 0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0 -y 0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0 -z 0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0 -a min -p 1 -q 1 -m all + +this would generate the following coarsened model: + +.. figure:: ../figs/norne_vec.png +.. figure:: ../figs/norne_plopm.png + + Top view of Norne for the (top) pore volume and (bottom) gas saturation (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_deck_norne.sh `_). diff --git a/docs/_sources/examples/smeaheia.rst.txt b/docs/_sources/examples/smeaheia.rst.txt new file mode 100644 index 0000000..4d850c0 --- /dev/null +++ b/docs/_sources/examples/smeaheia.rst.txt @@ -0,0 +1,25 @@ +.. _example-smeaheia: + +Smeaheia +======== +By downloading the `Smeaheia simulation model `_ (dataset part Simulation models), +then: + +.. code-block:: bash + + # From the download folders + cd Simulation_Models/data + pycopm -c 5,4,1 -a min -m all -i Statoil_Feasibility_sim_model_with_depletion_KROSS_INJ_SECTOR_20.DATA -o . + +will generate a coarser model five times in the x direction and four in the y direction, where the coarse cell is +made inactive if at least one cell is inactive (**-a min**). + +We use our `plopm `_ friend to generate PNG figures: + +.. code-block:: bash + + plopm -i 'STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PREP_PYCOPM_DRYRUN STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PYCOPM' -s ,,1 -v poro -sg 1,2 -fn smeaheia -t 'Smeaheia Coarsened Smeaheia' -rdl 1 -xu km -xf .0f -yu km -yf .0f -fs 5,4.5 -st 0 -c cet_rainbow_bgyrm_35_85_c69 -cbp 0.2,0.95,0.6,0.02 -cbf .2f + +.. figure:: ../figs/smeaheia.png + + Top view of porosity values for the (left) original and (right) coarsened model (figures using plopm, see/run `docs_via_deck_smeaheia.sh `_). diff --git a/docs/_sources/examples/spe10.rst.txt b/docs/_sources/examples/spe10.rst.txt new file mode 100644 index 0000000..0236ef4 --- /dev/null +++ b/docs/_sources/examples/spe10.rst.txt @@ -0,0 +1,27 @@ +.. _spe10: + +SPE10 +----- + +By downloading the `SPE10_MODEL2 model `_, then: + +.. code-block:: bash + + pycopm -i SPE10_MODEL2.DATA -s pvmean -c 4,8,2 -m all + +generates a coarsened model from ca. 1 million cells to ca. 20 thousands cells. + +.. figure:: ../figs/spe10_model2_coarser.png + + Porosity values for the (left) original and (right) coarsened SPE10 model. + +To generate a submodel from the coarsened model around the injector 'INJ', this can be achieved by executing: + +.. code-block:: bash + + pycopm -i SPE10_MODEL2_PYCOPM.DATA -p 1 -v 'INJ diamondxy 5' -m all -w vicinity -l sub -m all + +.. figure:: ../figs/vicinity.png +.. figure:: ../figs/spe10_plopm.png + + Pore volume values for the coarsened and vicinity around the well INJ in the SPE10 model (top figures using ResInsight and bottom figures using plopm (running pycopm with the flag -p 0 instead of -p 1), see/run `docs_via_deck_spe10.sh `_). diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt index 4b046b9..8dc800b 100644 --- a/docs/_sources/index.rst.txt +++ b/docs/_sources/index.rst.txt @@ -1,25 +1,142 @@ -.. pycopm documentation master file +.. _home: -Welcome to pycopm's documentation! -================================== +pycopm +====== + +.. rst-class:: lead + + Tailor OPM Flow geological models by coarsening, refining, extracting submodels, and transforming grids. + +**pycopm** creates modified, simulation-ready OPM Flow models from existing +``.DATA`` decks. Generic deck workflows are the active development path. +Legacy TOML workflows remain available for existing Norne and Drogon +coarsening and ERT studies. + +.. grid:: 1 2 2 4 + :gutter: 3 + :margin: 4 0 4 0 + + .. grid-item-card:: :octicon:`rocket;1.2em` Get started + :link: introduction + :link-type: doc + + Understand the deck-first workflow and choose a model operation. + + .. grid-item-card:: :octicon:`download;1.2em` Install + :link: installation + :link-type: doc + + Install pycopm, OPM Flow, and visualization tools. + + .. grid-item-card:: :octicon:`book;1.2em` Follow the tutorial + :link: tutorial + :link-type: doc + + Work through the complete Hello World deck workflow. + + .. grid-item-card:: :octicon:`terminal;1.2em` CLI reference + :link: command-line + :link-type: doc + + Look up options, defaults, formats, and compatibility rules. + +Quick installation +------------------ + +Install the current development version: + +.. code-block:: console + + pip install git+https://github.com/cssr-tools/pycopm.git + +See :doc:`installation` for supported Python versions, OPM Flow, visualization +tools, virtual environments, platform-specific dependencies, and installation +from source. + +Quick start +----------- + +Coarsen the bundled Hello World deck and run the complete workflow: + +.. code-block:: console + + pycopm -i examples/decks/HELLO_WORLD.DATA -c 5,5,1 -m all -o output + +Display the available command-line options: + +.. code-block:: console + + pycopm --help + +See :doc:`tutorial` for a guided workflow, :doc:`examples` for larger +applications, and :doc:`command-line` for exact syntax, accepted values, +defaults, and option compatibility. + +What can pycopm do? +------------------- + +.. grid:: 1 2 2 2 + :gutter: 3 + + .. grid-item-card:: Coarsen geological models + + Remove selected pillars and grid lines, aggregate continuous and + discrete properties, preserve pore volume, and optionally upscale + transmissibilities and non-neighbouring connections. + + .. grid-item-card:: Refine simulation grids + + Add cells globally or in selected x, y, and z intervals while mapping + properties, regions, wells, faults, and other indexed deck data to the + refined grid. + + .. grid-item-card:: Extract focused submodels + + Select cells by region values, xy polygons, or well-centred box, + diamond, and diamondxy neighbourhoods, with configurable boundary + pore-volume handling. + + .. grid-item-card:: Transform grid geometry + + Translate, scale, or rotate OPM Flow grids to reposition models, modify + dimensions, or align geological descriptions from different sources. + + .. grid-item-card:: Generate simulation-ready files + + Write modified OPM Flow decks and supporting include files, run + preparation and dry-run stages, and control names, precision, + initialization, and completion rewriting. + + .. grid-item-card:: Support advanced coarsening workflows + + Control active-cell aggregation, region and property upscaling, + fluid-in-place correction, fault-related jump handling, and dual + net/non-net coarsening. + + .. grid-item-card:: Integrate with other tools + + Use generated decks directly with OPM Flow or adapt them for ERT, PET, + visualization tools, history matching, optimization, proxy modelling, + and uncertainty studies. + + .. grid-item-card:: Maintain legacy integrated studies + + Continue existing Norne and Drogon coarsening and ERT workflows through + the retained TOML configuration interface while new development focuses + on generic deck input. .. toctree:: - :maxdepth: 4 + :hidden: + :maxdepth: 2 introduction - theory installation - configuration_file + tutorial examples + theory + command-line + related + configuration_file api output_folder contributing - related - about - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/_sources/installation.rst.txt b/docs/_sources/installation.rst.txt index 78aa6bb..c2de933 100644 --- a/docs/_sources/installation.rst.txt +++ b/docs/_sources/installation.rst.txt @@ -1,7 +1,7 @@ -============ +.. _installation: + Installation ============ - The following steps work installing the dependencies in Ubuntu via apt-get or in macOS using `brew `_ or `macports `_. While using package managers such as Anaconda, Miniforge, or Mamba might work, these are not tested. The supported Python versions are 3.12 to 3.14. @@ -132,3 +132,19 @@ You can check if the installation of OPM Flow succeded by typing in the terminal Source build in macOS +++++++++++++++++++++ See `this repository `_ dedicated to build OPM Flow from source in macOS 26 (GitHub actions), and tested with **pycopm** + + +Verify the installation +----------------------- + +.. code-block:: console + + pycopm --help + flow --help + +Next steps +---------- + +* Follow :doc:`tutorial/hello-world` for the recommended generic deck workflow. +* Use :doc:`command-line` for exact option definitions. +* Use :doc:`configuration_file` only for legacy Norne and Drogon integrated studies. diff --git a/docs/_sources/introduction.rst.txt b/docs/_sources/introduction.rst.txt index b01867f..1917826 100644 --- a/docs/_sources/introduction.rst.txt +++ b/docs/_sources/introduction.rst.txt @@ -1,64 +1,130 @@ -============ +.. image:: figs/pycopm.png + :alt: Geological-model operations supported by pycopm + :align: center + +.. _introduction: + Introduction ============ -.. image:: ./figs/pycopm.png - -This documentation describes the **pycopm** tool hosted in `https://github.com/cssr-tools/pycopm `_. +**pycopm** is an open-source tool for creating tailored OPM Flow geological +models from existing simulation decks. It can coarsen and refine grids, +extract submodels, and apply geometric transformations while generating the +files needed to run the modified model with OPM Flow. Concept ------- -User-friendly creation of OPM Flow geological models from provided input decks with options for grid refinement, grid coarsening, submodels, and transformations including scalings, rotations, and translations. -Roadmap -------- -In the initial development of the pycopm repository, the focus were two available models in `opm-tests `_: `norne `_ -and `drogon `_, where the coarsened models were used to perform history matching studies using -the Ensemble based reservoir tool `ERT `_, via a :doc:`configuration file <./configuration_file>`. +The main goal is the user-friendly creation of OPM Flow geological models from +provided input decks. The generic deck workflow supports grid refinement, grid +coarsening, submodel extraction, and transformations including scaling, +rotation, and translation. + +The generated decks can be used directly with OPM Flow or adapted for a +history-matching or optimization framework such as `ERT +`_ or `PET +`_. This supports applications +such as proxy modelling, non-conforming grids, upscaling studies, model +comparison, and uncertainty analysis. + +Recommended workflow +-------------------- + +Current pycopm development focuses on the **generic deck-based workflow**. +Provide an OPM Flow ``.DATA`` deck and select the required model operation: + +.. code-block:: console + + pycopm -i MODEL.DATA [OPTIONS] + +For example, coarsen the bundled Hello World model and run all processing +stages: + +.. code-block:: console + + pycopm -i examples/decks/HELLO_WORLD.DATA -c 5,5,1 -m all -o output -The current development of **pycopm** focuses on creating models (i.e., all needed input files to run OPM Flow such as the grid) by only providing the OPM Flow input files -(i.e., avoiding the manual work to create templates as it was done for drogon and norne). This allows for flexibility to adapt the generated decks in your -favourite history matching/optimization tool (e.g., `ERT `_, `PET `_), as well as to -test different approaches in diverse applications (e.g., proxy models, non-conforming grids, upscaling techniques). +Use :doc:`command-line` for the complete option descriptions, accepted values, +defaults, examples, and compatibility rules. Follow :doc:`tutorial` for a +guided workflow. -.. _overview: +.. warning:: -Overview + TOML configuration files are a legacy, model-specific workflow retained for + existing integrated Norne and Drogon coarsening and ERT studies. New pycopm + functionality is developed for generic OPM Flow deck input. Use + :doc:`configuration_file` only when maintaining one of those legacy studies. + +Core workflows +-------------- + +* **Grid coarsening:** Remove selected pillars and grid lines, aggregate model + properties, preserve pore volume, and optionally upscale + transmissibilities. +* **Grid refinement:** Add pillars and grid lines globally or in selected + intervals while mapping grid-dependent properties and deck keywords. +* **Submodel extraction:** Select a region by property values, an xy polygon, + or a well-centred box, diamond, or xy-diamond neighbourhood. +* **Geometric transformations:** Translate, scale, or rotate grid coordinates. +* **Simulation-ready output:** Write a modified OPM Flow deck and supporting + include files for direct simulation or use in another workflow. +* **Legacy integrated studies:** Generate coarsened Norne or Drogon projects + and optionally run OPM Flow or ERT from a TOML configuration. + +Project evolution +----------------- + +The initial development of **pycopm** focused on two public models from +`opm-tests `_: `Norne +`_ and `Drogon +`_. Coarsened versions of +these models were used in history-matching studies with ERT through a +:doc:`configuration file `. + +The current generic workflow removes the need to prepare model-specific +templates. It creates modified models directly from OPM Flow input decks, +which makes pycopm applicable to a broader range of geological models and +workflows. + +About the project +----------------- + +**pycopm** is developed within the `Center for Sustainable Subsurface +Resources `_ (CSSR, project number 331841) and has also been +funded by `Expansion of Resources for CO2 Storage on the Horda Platform +`_ +(ExpReCCS, project number 336294). + +The source code, issue tracker, and development history are available in the +`pycopm GitHub repository `_. +Contributions are welcome through the fork and pull-request workflow. Request +new features or report problems through the repository issue tracker. + +Citation -------- -The current implementation supports the following executable with the argument options: - -.. code-block:: bash - - pycopm -i name_of_input_file - -where - --i The base name of the :doc:`toml configuration file <./configuration_file>` or the name of the deck, e.g., 'DROGON.DATA', ('input.toml' by default). --o The base name of the :doc:`output folder <./output_folder>` ('.' by default, i.e., the folder where pycopm is executed). --f OPM Flow path to executable (e.g., /home/pycopm/build/opm-simulators/bin/flow), or just 'flow' if this was installed via binaries or if the folder where flow is built has been added to your path ('flow' by default). --m Execute a dry run of the input deck to generate the static properties ('prep'), generate only the modified files ('deck'), only exectute a dry run on the generated model ('dry'), 'prep_deck', 'deck_dry', or do all ('all') ('prep_deck' by default). --v The location to extract the sub model which can be assigned by region values, e.g., 'fipnum 2,4' extracts the cells with fipnums equal to 2 or 4, by a polygon given the xy locations in meters, e.g., 'xypolygon [0,0] [30,0] [30,30] [0,0]', or by the name of the well and three different options for the neighbourhood: box, diamond, and diamondxy, where for box the i, j, and k interval around the connections are given, e.g., 'welln box [-1,1] [-2,2] [0,3]' results in a vicinity with 1 pm cell in the x direction, 2 pm cells in the y direction and only 3 cells in the k positive direction, while the diamond considers only the given number of cells around the well connections (e.g., 'welln diamond 2') and diamondxy it is restricted to the xy plane ('' by default). --c Level of coarsening in the x, y, and z dir (',' by default; either use this flag or the -x, -y, and -z ones). --x Array of x-coarsening, e.g., if the grid has 6 cells in the x direction, then '0,2,0,2,0,2,0' would generate a coarsened model with 3 cells, while '0,2,2,2,2,2,0' would generate a coarser model with 1 cell, i.e., 0 keeps the pilars while 2 removes them. As an alternative, the range of the cells to coarse can be given separate them by commas, e.g., '1:3,5:6' generates a coarsened model with 3 cells where the cells with the first three and two last i indices are coarsened to one ('' by default), --y Array of y-coarsening, see the description for -x ('' by default). --z Array of z-coarsening, see the description for -x ('' by default). --g Level of grid refinement in the x, y, and z dir ('' by default). --rx Array of x-refinement, e.g., if the grid has 6 cells in the x direction, then '0,1,0,2,0,4' would generate a refined model with 13 cells, while 0,0,0,1,0,0 would generate a refined model with 7 cells ('' by default). --ry Array of y-refinement, see the description for -rx ('' by default). --rz Array of z-refinement, see the description for -rx ('' by default). --a In coarsening, use 'min', 'max', or 'mode' to scale the actnum, e.g., 'min' makes the new coarser cell inactive if at least one cell is inactive, while 'max' makes it active it at least one cell is active ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:10,11:15 -a min,mode'. --n In coarsening, use 'min', 'max', or 'mode' to scale endnum, eqlnum, fipnum, fluxnum, imbnum, miscnum, multnum, opernum, pvtnum, rocknum, and satnum ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:3,4 -a max,min'. --s In coarsening, use 'min', 'max', 'mean', or 'pvmean' to scale permx, permy, permz, poro, swatinit, disperc, thconr, and all mult(-)xyz ('' by default, i.e., using the arithmetic average for permx/permy, harmonic average for permz, volume weighted mean for mult(-)xyz, and the pore volume weighted mean ('pvmean') for the rest). . For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:40,41:43,44:50 -a pvmean,max,pvmean'. --p In coarsening, set to '1' to add the removed pore volume to the closest coarser cells, while in submodels '1' adds the porv from outside on the boundary of the submodel, '2' adds the corner regions (e.g., below the mini and minj from the input model) to the corners in the submodel, '3' distributes the porv uniformly along the boundary, and '4' distributes it on the whole submodel ('0' by default, i.e., no porv correction). --q Adjust the pv to the initial FGIP and FOIP from the input deck; use this option only for systems with initial oil, gas, and water, e.g., norne or drogon, but no in Smeaheia ('0' by default, '1' to enable). --t In coarsening, write and use upscaled transmissibilities by ('1') armonic averaging and summing the transmissibilities in the corresponding coarsening direction and ('2') scaling the face transmissibily on the coarse faces ('0' by default, i.e., transmissibilities are not used). --r Remove CONFACT and KH from COMPDAT ('1') and also remove PEQVR ('2') (ITEM 13, the last entry) to compute the well transmisibility connections internally in OPM Flow using the grid properties ('2' by default; set to '0' to not remove). --j In coarsening, tuning parameter to avoid creation of neighbouring connections in the coarsened model where there are discontinuities between cells along the z direction, e.g., around faults ('' by default, i.e., nothing corrected; if need it, try with values of the order of 1). --w Name of the generated deck ('' by default, i.e., the name of the input deck plus _PYCOPM.DATA). --l Added text before each generated .INC ('PYCOPM\_' by default, i.e., the modified porv is saved in PYCOPM_PORV.INC; set to '' to generate PORV.INC, PERMX.INC, etc). --e Use 'utf8' or 'ISO-8859-1' encoding to read the deck ('ISO-8859-1' by default). --ijk Given i,j,k indices in the input model, return the modified i,j,k corresponding positions ('' by default; if not empty, e.g., '1,2,3', then there will not be generation of modified files, only the i,j,k mapped indices in the terminal). --d Options to transform the x,y,z coordinates: 'translate [10,-5,4]' adds the values in meters to the coordinates, 'scale [1,2,3]' multiplies the coordinates by the given values respectively, and 'rotatexy 45' applies a rotation in degrees in the xy plane (rotatexz and rotateyz applies a rotation around the y and x axis respectively) ('' by default). --explicit Set to 1 to explicitly write the cell values in the SOLUTION section in the deck ('0' by default). --dual Set the criterium to differentiate net and non-net in coarsening using a static variable, e.g., 'poro <= 0.1' ('' by default). --precision Set the number of significant digits used when writing floating-point values, or 0 to use machine precision + +If **pycopm** contributes to published work, cite the project paper: + + Landa-Marbán, D. (2026). *pycopm: An open-source tool to tailor OPM Flow + geological models*. arXiv:2602.11777. + https://doi.org/10.48550/arXiv.2602.11777. + +Where to continue +----------------- + +* Complete :doc:`installation` and verify both pycopm and OPM Flow. +* Follow the :doc:`Hello World tutorial ` to coarsen, + extract, refine, transform, run, and visualize a small model. +* Use :doc:`command-line` for exact flag syntax, accepted values, defaults, and + compatibility rules. +* Read :doc:`theory` for the complete coarsening, refinement, submodel, and + transformation methods. +* Browse :doc:`examples` for Smeaheia, Drogon, Norne, SPE10, dual coarsening, + and reproducible visualization commands. +* Review :doc:`output_folder` to understand generated decks, include files, + simulation results, and legacy ERT projects. +* Consult :doc:`configuration_file` for retained Norne and Drogon TOML studies. +* Browse :doc:`api` for the Python modules, classes, and functions. +* See :doc:`contributing` to contribute code, run checks, report issues, or seek + support. +* Explore :doc:`related` for complementary open-source subsurface tools. diff --git a/docs/_sources/options/aggregation.rst.txt b/docs/_sources/options/aggregation.rst.txt new file mode 100644 index 0000000..516d53c --- /dev/null +++ b/docs/_sources/options/aggregation.rst.txt @@ -0,0 +1,72 @@ +.. _options-aggregation: + +Property aggregation and conservation +===================================== + +Control active cells, discrete regions, continuous properties, pore +volume, fluids in place, transmissibilities, and fault-related jump handling. + +.. program:: pycopm + +-a/--active_cell_methods +---------------------------------- + +.. option:: -a , --active_cell_methods + :no-contents-entry: + :no-typesetting: + +In coarsening, use 'min', 'max', or 'mode' to scale the actnum, e.g., 'min' makes the new coarser cell inactive if at least one cell is inactive, while 'max' makes it active it at least one cell is active ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:10,11:15 -a min,mode'. + +-n/--discrete_aggregation_method +------------------------------------------ + +.. option:: -n , --discrete_aggregation_method + :no-contents-entry: + :no-typesetting: + +In coarsening, use 'min', 'max', or 'mode' to scale endnum, eqlnum, fipnum, fluxnum, imbnum, miscnum, multnum, opernum, pvtnum, rocknum, and satnum ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:3,4 -a max,min'. + +-s/--continuous_aggregation_method +-------------------------------------------- + +.. option:: -s , --continuous_aggregation_method + :no-contents-entry: + :no-typesetting: + +In coarsening, use 'min', 'max', 'mean', or 'pvmean' to scale permx, permy, permz, poro, swatinit, disperc, thconr, and all mult(-)xyz ('' by default, i.e., using the arithmetic average for permx/permy, harmonic average for permz, volume weighted mean for mult(-)xyz, and the pore volume weighted mean ('pvmean') for the rest). . For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:40,41:43,44:50 -a pvmean,max,pvmean'. + +-p/--pore_volume_correction <0|1|2|3|4> +--------------------------------------- + +.. option:: -p <0|1|2|3|4>, --pore_volume_correction <0|1|2|3|4> + :no-contents-entry: + :no-typesetting: + +In coarsening, set to '1' to add the removed pore volume to the closest coarser cells, while in submodels '1' adds the porv from outside on the boundary of the submodel, '2' adds the corner regions (e.g., below the mini and minj from the input model) to the corners in the submodel, '3' distributes the porv uniformly along the boundary, and '4' distributes it on the whole submodel ('0' by default, i.e., no porv correction). + +-q/--correct_fluid_in_place <0|1> +--------------------------------- + +.. option:: -q <0|1>, --correct_fluid_in_place <0|1> + :no-contents-entry: + :no-typesetting: + +Adjust the pv to the initial FGIP and FOIP from the input deck; use this option only for systems with initial oil, gas, and water, e.g., norne or drogon, but no in Smeaheia ('0' by default, '1' to enable). + +-t/--transmissibility_coarsening_method <0|1|2> +----------------------------------------------- + +.. option:: -t <0|1|2>, --transmissibility_coarsening_method <0|1|2> + :no-contents-entry: + :no-typesetting: + +In coarsening, write and use upscaled transmissibilities by ('1') armonic averaging and summing the transmissibilities in the corresponding coarsening direction and ('2') scaling the face transmissibily on the coarse faces ('0' by default, i.e., transmissibilities are not used). + +-j/--jump_thresholds +----------------------------- + +.. option:: -j , --jump_thresholds + :no-contents-entry: + :no-typesetting: + +In coarsening, tuning parameter to avoid creation of neighbouring connections in the coarsened model where there are discontinuities between cells along the z direction, e.g., around faults ('' by default, i.e., nothing corrected; if need it, try with values of the order of 1). diff --git a/docs/_sources/options/compatibility.rst.txt b/docs/_sources/options/compatibility.rst.txt new file mode 100644 index 0000000..5004c70 --- /dev/null +++ b/docs/_sources/options/compatibility.rst.txt @@ -0,0 +1,22 @@ +.. _options-compatibility: + +Option compatibility and validation +=================================== + +pycopm validates input formats and rejects incompatible workflows before +processing the model. + +.. program:: pycopm + +The implementation validates the following combinations before processing a deck: + +* Use either uniform ``-c`` or directional ``-x``, ``-y``, and ``-z`` coarsening. +* Use either uniform ``-g`` or directional ``-rx``, ``-ry``, and ``-rz`` refinement. +* Coarsening and refinement are mutually exclusive. +* ``-v`` cannot be combined with ``-d``, refinement, or ``-q 1``. +* ``-d`` cannot be combined with refinement. +* Non-default ``-a`` and ``-n``, and any ``-s``, ``-t``, ``-j``, or ``-dual``, require coarsening. +* ``-p 1`` requires coarsening or submodel extraction. ``-p 2``, ``-p 3``, and ``-p 4`` require submodel extraction. +* Multiple aggregation methods require range-based ``-z`` coarsening and one method per z group. +* An ``xypolygon`` must have at least four points and repeat its first point at the end. +* Scale factors cannot be zero, jump thresholds must be positive, and requested ``i,j,k`` indices must be positive. diff --git a/docs/_sources/options/grid.rst.txt b/docs/_sources/options/grid.rst.txt new file mode 100644 index 0000000..0042170 --- /dev/null +++ b/docs/_sources/options/grid.rst.txt @@ -0,0 +1,81 @@ +.. _options-grid: + +Grid coarsening and refinement +============================== + +Define uniform or directional coarsening and refinement. The detailed +array and range examples from the original introduction are retained below. + +.. program:: pycopm + +-c/--coarsening +----------------------- + +.. option:: -c , --coarsening + :no-contents-entry: + :no-typesetting: + +Level of coarsening in the x, y, and z dir (',' by default; either use this flag or the -x, -y, and -z ones). + +-x/--x_coarsening +-------------------------- + +.. option:: -x , --x_coarsening + :no-contents-entry: + :no-typesetting: + +Array of x-coarsening, e.g., if the grid has 6 cells in the x direction, then '0,2,0,2,0,2,0' would generate a coarsened model with 3 cells, while '0,2,2,2,2,2,0' would generate a coarser model with 1 cell, i.e., 0 keeps the pillars while 2 removes them. As an alternative, the range of the cells to coarse can be given separate them by commas, e.g., '1:3,5:6' generates a coarsened model with 3 cells where the cells with the first three and two last i indices are coarsened to one ('' by default), + +-y/--y_coarsening +-------------------------- + +.. option:: -y , --y_coarsening + :no-contents-entry: + :no-typesetting: + +Array of y-coarsening, see the description for -x ('' by default). + +-z/--z_coarsening +-------------------------- + +.. option:: -z , --z_coarsening + :no-contents-entry: + :no-typesetting: + +Array of z-coarsening, see the description for -x ('' by default). + +-g/--refinement +----------------------- + +.. option:: -g , --refinement + :no-contents-entry: + :no-typesetting: + +Level of grid refinement in the x, y, and z dir ('' by default). + +-rx/--x_refinement +--------------------------- + +.. option:: -rx , --x_refinement + :no-contents-entry: + :no-typesetting: + +Array of x-refinement, e.g., if the grid has 6 cells in the x direction, then '0,1,0,2,0,4' would generate a refined model with 13 cells, while 0,0,0,1,0,0 would generate a refined model with 7 cells ('' by default). + +-ry/--y_refinement +--------------------------- + +.. option:: -ry , --y_refinement + :no-contents-entry: + :no-typesetting: + +Array of y-refinement, see the description for -rx ('' by default). + +-rz/--z_refinement +--------------------------- + +.. option:: -rz , --z_refinement + :no-contents-entry: + :no-typesetting: + +Array of z-refinement, see the description for -rx ('' by default). diff --git a/docs/_sources/options/output.rst.txt b/docs/_sources/options/output.rst.txt new file mode 100644 index 0000000..990f05e --- /dev/null +++ b/docs/_sources/options/output.rst.txt @@ -0,0 +1,72 @@ +.. _options-output: + +Deck rewriting and output +========================= + +Control completion rewriting, generated names, include prefixes, input +encoding, index mapping, explicit initialization, and numeric precision. + +.. program:: pycopm + +-r/--completion_removal_level <0|1|2> +------------------------------------- + +.. option:: -r <0|1|2>, --completion_removal_level <0|1|2> + :no-contents-entry: + :no-typesetting: + +Remove CONFACT and KH from COMPDAT ('1') and also remove PEQVR ('2') (ITEM 13, the last entry) to compute the well transmisibility connections internally in OPM Flow using the grid properties ('2' by default; set to '0' to not remove). + +-w/--output_deck_name +---------------------------- + +.. option:: -w , --output_deck_name + :no-contents-entry: + :no-typesetting: + +Name of the generated deck ('' by default, i.e., the name of the input deck plus _PYCOPM.DATA). + +-l/--include_prefix +---------------------------- + +.. option:: -l , --include_prefix + :no-contents-entry: + :no-typesetting: + +Added text before each generated .INC ('PYCOPM\_' by default, i.e., the modified porv is saved in PYCOPM_PORV.INC; set to '' to generate PORV.INC, PERMX.INC, etc). + +-e/--deck_encoding +----------------------------- + +.. option:: -e , --deck_encoding + :no-contents-entry: + :no-typesetting: + +Use 'utf8' or 'ISO-8859-1' encoding to read the deck ('ISO-8859-1' by default). + +-ijk/--requested_ijk +---------------------------- + +.. option:: -ijk , --requested_ijk + :no-contents-entry: + :no-typesetting: + +Given i,j,k indices in the input model, return the modified i,j,k corresponding positions ('' by default; if not empty, e.g., '1,2,3', then there will not be generation of modified files, only the i,j,k mapped indices in the terminal). + +-explicit/--write_explicit_solution <0|1> +----------------------------------------- + +.. option:: -explicit <0|1>, --write_explicit_solution <0|1> + :no-contents-entry: + :no-typesetting: + +Set to 1 to explicitly write the cell values in the SOLUTION section in the deck ('0' by default). + +-precision/--significant_digits <0..15> +--------------------------------------- + +.. option:: -precision <0..15>, --significant_digits <0..15> + :no-contents-entry: + :no-typesetting: + +Set the number of significant digits used when writing floating-point values, or 0 to use machine precision diff --git a/docs/_sources/options/submodels-transformations.rst.txt b/docs/_sources/options/submodels-transformations.rst.txt new file mode 100644 index 0000000..5588948 --- /dev/null +++ b/docs/_sources/options/submodels-transformations.rst.txt @@ -0,0 +1,36 @@ +.. _options-submodels-transformations: + +Submodels, transformations, and dual coarsening +=============================================== + +Extract submodels by region, polygon, or well neighbourhood; transform +grid coordinates; or separate net and non-net cells during coarsening. + +.. program:: pycopm + +-v/--vicinity_specification +------------------------------------------- + +.. option:: -v , --vicinity_specification + :no-contents-entry: + :no-typesetting: + +The location to extract the sub model which can be assigned by region values, e.g., 'fipnum 2,4' extracts the cells with fipnums equal to 2 or 4, by a polygon given the xy locations in meters, e.g., 'xypolygon [0,0] [30,0] [30,30] [0,0]', or by the name of the well and three different options for the neighbourhood: box, diamond, and diamondxy, where for box the i, j, and k interval around the connections are given, e.g., 'welln box [-1,1] [-2,2] [0,3]' results in a vicinity with 1 pm cell in the x direction, 2 pm cells in the y direction and only 3 cells in the k positive direction, while the diamond considers only the given number of cells around the well connections (e.g., 'welln diamond 2') and diamondxy it is restricted to the xy plane ('' by default). + +-d/--grid_transformation +---------------------------------------- + +.. option:: -d , --grid_transformation + :no-contents-entry: + :no-typesetting: + +Options to transform the x,y,z coordinates: 'translate [10,-5,4]' adds the values in meters to the coordinates, 'scale [1,2,3]' multiplies the coordinates by the given values respectively, and 'rotatexy 45' applies a rotation in degrees in the xy plane (rotatexz and rotateyz applies a rotation around the y and x axis respectively) ('' by default). + +-dual/--dual_porosity_criterion +------------------------------------------- + +.. option:: -dual , --dual_porosity_criterion + :no-contents-entry: + :no-typesetting: + +Set the criterium to differentiate net and non-net in coarsening using a static variable, e.g., 'poro <= 0.1' ('' by default). diff --git a/docs/_sources/options/workflow.rst.txt b/docs/_sources/options/workflow.rst.txt new file mode 100644 index 0000000..ae7583f --- /dev/null +++ b/docs/_sources/options/workflow.rst.txt @@ -0,0 +1,44 @@ +.. _options-workflow: + +Input and workflow +================== + +Select the input, output location, OPM Flow command, and processing stages. + +.. program:: pycopm + +-i/--input_deck_path +---------------------------- + +.. option:: -i , --input_deck_path + :no-contents-entry: + :no-typesetting: + +The base name of the :doc:`toml configuration file <../configuration_file>` or the name of the deck, e.g., 'DROGON.DATA', ('input.toml' by default). + +-o/--output_directory +------------------------------ + +.. option:: -o , --output_directory + :no-contents-entry: + :no-typesetting: + +The base name of the :doc:`output folder <../output_folder>` ('.' by default, i.e., the folder where pycopm is executed). + +-f/--flow_command +--------------------------- + +.. option:: -f , --flow_command + :no-contents-entry: + :no-typesetting: + +OPM Flow path to executable (e.g., /home/pycopm/build/opm-simulators/bin/flow), or just 'flow' if this was installed via binaries or if the folder where flow is built has been added to your path ('flow' by default). + +-m/--execution_mode +-------------------------- + +.. option:: -m , --execution_mode + :no-contents-entry: + :no-typesetting: + +Execute a dry run of the input deck to generate the static properties ('prep'), generate only the modified files ('deck'), only exectute a dry run on the generated model ('dry'), 'prep_deck', 'deck_dry', or do all ('all') ('prep_deck' by default). diff --git a/docs/_sources/output_folder.rst.txt b/docs/_sources/output_folder.rst.txt index 1c06710..e2371f0 100644 --- a/docs/_sources/output_folder.rst.txt +++ b/docs/_sources/output_folder.rst.txt @@ -1,8 +1,8 @@ -============= +.. _output-folder: + Output folder ============= - -Via configuration files +Legacy TOML workflow ----------------------- As described in the :doc:`introduction <./introduction>`, in the early development of **pycopm** the focus was on history matching studies using `ERT `_ for the @@ -19,12 +19,12 @@ on the drogon model. The generate ert.ert file can be run directly calling ERT for further studies, and some useful plots and files are generated in the postprocessing folder. The OPM simulation results can be visualized using `ResInsight `_. -Via an OPM Flow input deck --------------------------- +Deck-based workflow (recommended) +--------------------------------- The current development of **pycopm** focuses on creating modified models, i.e., all needed input files to run OPM Flow, by using the input deck and defining the approach (grid refinement, grid coarsening, submodels, and transformations including scalings, rotations, and translations). -The following screenshot shows the input deck and generated files in the selected output folder (coarser for this example) after executing **pycopm** on the :ref:`generic` +The following screenshot shows the input deck and generated files in the selected output folder (coarser for this example) after executing **pycopm** on the :doc:`examples` .. figure:: figs/output_generic.png diff --git a/docs/_sources/related.rst.txt b/docs/_sources/related.rst.txt index d80715c..33e6a6c 100644 --- a/docs/_sources/related.rst.txt +++ b/docs/_sources/related.rst.txt @@ -1,66 +1,59 @@ -======= -Related -======= +.. _related: -Below are some tools that might be of interest; check 'em out 🙂. - -********** -pyopmspe11 -********** - -.. image:: ./figs/pyopmspe11.gif - :scale: 50% - -`A Python framework using OPM Flow for the CSP SPE11 benchmark project `_. +Related projects +================ -***** -pofff -***** - -.. image:: ./figs/pofff.png - -`An image-based history-matching framework for the FluidFlower Benchmark using OPM Flow `_. +Below are some tools that might be of interest; check 'em out 🙂. -************* -pyopmnearwell -************* +.. grid:: 1 1 2 2 + :gutter: 3 + :class-container: related-projects -.. image:: ./figs/pyopmnearwell.png - :scale: 60% + .. grid-item-card:: pyopmspe11 + :link: https://github.com/OPM/pyopmspe11 + :link-type: url + :img-top: figs/pyopmspe11.gif -`A Python framework to simulate near well dynamics using OPM Flow `_. + A Python framework using OPM Flow for the CSP SPE11 benchmark project. -***** -plopm -***** + .. grid-item-card:: pofff + :link: https://github.com/cssr-tools/pofff + :link-type: url + :img-top: figs/pofff.png -.. image:: ./figs/plopm.png + An image-based history-matching framework for the FluidFlower Benchmark using OPM Flow. -`Quick generation of PNGs, GIFs, and VTKs from a OPM Flow type model `_. + .. grid-item-card:: pyopmnearwell + :link: https://github.com/cssr-tools/pyopmnearwell + :link-type: url + :img-top: figs/pyopmnearwell.png -******** -expreccs -******** + A Python framework to simulate near-well dynamics using OPM Flow. -.. image:: ./figs/expreccs.gif - :scale: 50% + .. grid-item-card:: plopm + :link: https://github.com/cssr-tools/plopm + :link-type: url + :img-top: figs/plopm.png -`A Python framework using OPM Flow to simulate regional and site reservoirs for CO2 storage `_. + Quick generation of PNGs, GIFs, and VTKs from an OPM Flow model. -******* -ad-micp -******* + .. grid-item-card:: expreccs + :link: https://github.com/cssr-tools/expreccs + :link-type: url + :img-top: figs/expreccs.gif -.. image:: ./figs/ad-micp.gif - :scale: 40% + A Python framework using OPM Flow to simulate regional and site reservoirs for CO2 storage. -`A module to study CO2 leakage remediation by microbially induced calcite precipitation (MICP) `_. + .. grid-item-card:: ad-micp + :link: https://github.com/daavid00/ad-micp + :link-type: url + :img-top: figs/ad-micp.gif -**** -pymm -**** + A module to study CO2 leakage remediation by microbially induced calcite precipitation. -.. image:: ./figs/pymm.gif - :scale: 15% + .. grid-item-card:: pymm + :link: https://github.com/cssr-tools/pymm + :link-type: url + :img-top: figs/pymm.gif -`An open-source image-based framework for CFD in microsystems `_. + An open-source image-based framework for CFD in microsystems. diff --git a/docs/_sources/theory.rst.txt b/docs/_sources/theory.rst.txt index e942d72..89d2640 100644 --- a/docs/_sources/theory.rst.txt +++ b/docs/_sources/theory.rst.txt @@ -1,167 +1,46 @@ -****** -Theory -****** +.. _theory: +Theory and methods +================== Here insight about the **pycopm** methods regarding grid coarsening, grid refinement, submodels, and transformations is given. -=============== -Grid coarsening -=============== -The coarsening approach implemented in **pycopm** is based on industry standard grid formats which is preserved during coarsening. -This allows for direct application in standard reservoir simulators. In coarsening accuracy is traded for speed. -This is often appropriate, but care must be taken to assure that the coarse solution preserve the key properties of the model. -The `Lie 2019 textbook `_ -gives an excellent introduction to grid coarsening and upscaling geological properties, and the methods implemented in **pycopm** follow similar techniques. +.. grid:: 1 2 2 2 + :gutter: 3 -Corner-point grids are commonly use in subsurface simulations, due to its flexibility to handle complex geometries such as faults. -The grid is defined by vertical pillars and horizontal lines connecting the pillars, resulting in cells that can have up to six faces (hexahedrons), -but also these grids allow to define connections between non-neighboring cells (NNC). A special case of corner-point grids are Cartesian regular grids. -These grids also feature inactive cells, which are cells with zero pore volume and no connected to the active cells. + .. grid-item-card:: Grid coarsening + :link: theory-coarsening + :link-type: ref + :img-top: figs/coarsening_plopm.png -We define the grid coarsening operator :math:`\Xi_{xyz}^C`, which creates from an input grid :math:`\Omega` a coarser grid :math:`\Omega^*` (we adopt the notation * for -coarser quantities, while :math:`{}^C` in the coarsening operator emphasize the different approaches for the cell clustering) by removing -vertical pillars and horizontal lines: + Cell clustering, activity, property upscaling, transmissibilities, NNCs, and conservation. -.. math:: - \Xi_{xyz}^C(\Omega)\rightarrow\Omega^* + .. grid-item-card:: Grid refinement + :link: theory-refinement + :link-type: ref + :img-top: figs/refinement.png -To give flexibility in the coarsening, the cell clustering can be given as x, y, and z arrays to define the pillars/lines to be removed. -One natural question is how to handle the inactive cells in a cluster, and for this, three options to define the coarser cells are implemented: -min, max, and mode (mode is the default, i.e., the coarser cells is active if the number of active cells is the most common value in the cluster). -Figure 1 shows a simple 2D corner-point grid with different coarsening using min, max, and mode. + Added grid lines, inherited properties, and mapped indexed data. -.. figure:: figs/coarsening_plopm.png + .. grid-item-card:: Submodels + :link: theory-submodels + :link-type: ref + :img-top: figs/submodel.png - Figure 1: Example of coarser models from a grid with 18 cells, where the cell #11 is inactive. - One application where the max option could be useful is for models where there are a lot of - inactive cells in the z direction, while the min option could be useful for models applying - coarsening in the xy plane, since using min results in coarser models that do not generate - new connections across inactive cells. + Region, polygon, and well-based extraction with boundary pore-volume methods. -.. tip:: + .. grid-item-card:: Transformations + :link: theory-transformations + :link-type: ref + :img-top: figs/transformation_plopm.png - The figure can be generated by executing in the terminal (`docs_theory_coarsening.sh `_): + Translation, scaling, and rotation of geological grids. - .. code-block:: bash +.. toctree:: + :hidden: + :maxdepth: 1 - . ./tests/scripts/docs_theory_coarsening.sh - - -For upscaling geophysical properties, naturally, the pore volume in a coarser cell :math:`\Phi_{i^*,j^*,k^*}^*` (:math:`i^*`, :math:`j^*`, and :math:`k^*` referring to the cell -coarse indices in the x, y, and z direction respectively) are equal to the sum of pore volume from the corresponding cells -:math:`\Phi_{i,j,k}` in the input model, which are part of the cluster :math:`\mathbb{C}_{i^*,j^*,k^*}`: - -.. math:: - \Phi_{i^*,j^*,k^*}^* = \sum_{(i,j,k)\in\mathbb{C}_{i^*,j^*,k^*}}\Phi_{i,j,k} - -For example, in Figure 1 when coarsening into two cells using mode/max resulted in two coarse cells, where: - -.. math:: - - \mathbb{C}_{1^*,1^*,1^*}=\{ (1,1,1),(2,1,1),(3,1,1),(1,1,2),(2,1,2),(3,1,2),(1,1,3),(2,1,3),(3,1,3) \}. - -From this definition, the porosity in the coarse model :math:`\phi_{i^*,j^*,k^*}^*` can be simply computed by: - -.. math:: - \phi_{i^*,j^*,k^*}^* = \frac{\Phi_{i^*,j^*,k^*}^*}{\mathbb{V}_{i^*,j^*,k^*}^*} - -where :math:`\mathbb{V}_{i^*,j^*,k^*}^*` is the geometric volume of the coarser cell. - -For the rock permeability, there are different upscaling methods (e.g., arithmetic or harmonic average) that are case dependent and perform different, -see the `Lie 2019 textbook `_ -for comparison of these methods. In **pycopm**, by default the permeability in the x and y directions are computed using the arithmetic average, while -the permeability in the z direction by the harmonic average. As additional options, the permeabilities in the coarser cells :math:`\mathbb{K}_{i^*,j^*,k^*}^*` can -be set to equal the min, max, mean, or pv-weighted mean (pvmean) values from the permeabilities in the corresponding cluster :math:`\mathbb{K}_{i,j,k}`. For example, using the max for -permeabilities could be useful for history matching studies, where the parameters to history match are saturation functions -(relative permeabilities and capillary pressure). - -The above line mentions the initial application to develop **pycopm** (coarsening to history match saturation functions), as such there are no upscaling -methods implemented in **pycopm** for saturation functions. In a geological model, it is common to define different regions (referred as satnum) to assign -different saturation function tables. Then, if a cluster :math:`\mathbb{C}_{i^*,j^*,k^*}` involves different values for satnum, the mode (the most frequent value) is used -to assign the value in the coarser cell (this is also used to assign additional discrete coarser values such as fluid-in-place regions (fipnum)). - -For grids with large number of non-neighboring connections (faults) and inactive cells, then a better approach is to upscale transmisibilities. -A drawback of upscaling transmissibilities is that permeabilities cannot be used in history matching, but instead, transmissibilities multipliers, -which increases the number of parameters to history match and might break history match workflows where different permeability fields are generated -from spatial correlations. To this end, two approaches to upscale transmissibilities are implemented in **pycopm**. The former computes the coarser -transmissibility - -.. math:: - \scriptsize\mathbb{T}_{i^*,j^*,k^*}^*=\left[ T_{i^*,j^*,k^*\rightarrow i^*+1,j^*,k^*}^*,T_{i^*-1,j^*,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*+1,k^*}^*,T_{i^*,j^*-1,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*,T_{i^*,j^*,k^*-1\rightarrow i^*,j^*,k^*}^* \right] - -using the armonic averaging along the transsmissibility direction and summing over these values over the cell coarser face. For example, -for the transmissibility in the z+ direction: - -.. math:: - T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*=\sum_{(i,j)\in\mathbb{C}_{i^*,j^*,k^*}}\left( \sum_{(i,j,k)\rightarrow(i,j,k+1)\in\mathbb{C}_{i^*,j^*,k^*}} \frac{1}{T_{i,j,k\rightarrow i,j,k+1}} \right)^{-1} - -For cases where the coarsening is only along one direction (e.g., the z direction), the second method sets the transmissibility on the coarse cell -faces in that direction equal to the overlapping cell face values in the corresponding cluster (instead of computing the armonic average). For input -models with a large number of inactive cells, this approach has resulted in better results with respect to the input model simulations than using the -armonic average. For both approaches, the transmissibilities are scaled with the ratio of the cell -face effective areas (input model) to the coarse cell area. For non-neighbouring connections, this approach is also implemented, i.e., -the non-neighbouring connections in the coarser model sum the values from the non-neighbouring connections in the input model, which is important in -order to honor the pressure connections along open faults communicating different formations. - -=============== -Grid refinement -=============== - -The grid refinement is achieved by adding vertical pillars and horizontal lines in the grid from the input model. The refinement can be defined globally -in any direction (i, j, or k), as well as localized in defined grid indices. Properties such as porosity, permeabilities, and region numbers are set to the -same value in the corresponding finner cells inside the unrefined cell. Model properties defined by i,j,k locations such as wells, faults, and -boundary conditions are mapped to the new range of refined indices (i.e., adding additional entries to the generated deck). - -.. figure:: figs/refinement.png -.. figure:: figs/refinement_plopm.png - - Figure 2: Faults and wells in `MODEL3.DATA `_ (left) and after grid refinement "-g 2,2,2" (right) (upper and lower visualizations using `ResInsight `_ and `plopm `_ respectively (see/run `docs_theory_refinements.sh `_)). - -========= -Submodels -========= - -The generation of a submodel, i.e., a selected region in the input model, makes possible to lower the number of active cells and focus on an area of interest in -the input model. This results in smaller size of the input files, and faster simulations using OPM Flow. The submodel can be defined by properties matching a -value, e.g., all cells with fipnum equal to 1, or by a polygon given the xy locations in meters. Model properties defined by i,j,k locations such as wells and faults -are shifted to their corresponding values. If the wells/faults are not inside the extracted submodel, then these are not written to the generated deck. - -Regarding the boundary conditions in the extracted model with respect to the pore volume outisde the submodel, four options are provided by **pycopm**: - -#. no correction for the pore volume -#. adding the pore volume in each cell on the submodel boundary by summing all cell pore volumes in their corresponding i and j directions. If there is pore volume in the outside corners, this is equally distributed among the boundary cells in the two corresponding sides. -#. distributing the pore volume equally among the boundary cells in the submodel. -#. distributing the pore volume equally among all cells in the submodel - -.. figure:: figs/submodel.png -.. figure:: figs/submodel_plopm.png - - Figure 3: The shape to extract the sudmodel corresponds to "-v 'xypolygon [50,90] [60,60] [90,60] [65,40] [75,10] [50,30] [25,10] [35,40] [10,60] [40,60] [50,90]'". - The j indices for the cells have been accordingly shifted in the extracted model, and the right figure shows the projected pore volume on the boundary. Upper figures - are generated from ResInsight (screenshots) while the lower figures using plopm (see/run `docs_theory_submodels.sh `_). - -In addition, it is possible to extract submodels around wells, with three different options for the neighbourhood: box, diamond, and diamondxy. The box option allows to define -the intervals to extract the cells, while the diamond and diamondxy results in fewer cells since the cells in the corners are trimmed. - -.. figure:: figs/submodelwell.png -.. figure:: figs/submodelwell_plopm.png - - Figure 4: The submodel in `norne `_ by executing "-v 'E-3H diamondxy 0' -p 1", "-v 'E-3H diamond 1' -p 1", and "-v 'E-3H box [-1,2] [-2,3] [-1,1]' -p 1" respectively. - The top figures are visualized using ResInsight, while the lower figures are the 2D front view of the models using plopm (see/run `docs_theory_submodels.sh `_). - -=============== -Transformations -=============== - -Affine transformations are widely used in diverse applications since they preserve points, straight lines, and planes. In the field of reservoir management, there are large -uncertainties in the characterization of geological formations (reservoirs are typically located several kilometers below the surface). Once a reservoir model is created, over time -additional information from field measurements (e.g., seismic data, addiitonal wells, well's pressures, production rates) can indicate a different model characterization. This is when having -tools like **pycopm** can be handy, i.e., to apply translations of the grid (e.g., a different depth which impacts the pressure), scaling (e.g., to ease comparison between models made by different -groups which missmatch in the thickness of layers), and rotations (e.g., to align grids betweens two different models). - -.. figure:: figs/transformation.png -.. figure:: figs/transformation_plopm.png - - Figure 5: Extracted shape in Figure 3 after a rotation "-d 'rotatexy 45'" (left) and scaling "-d 'scale [1,0.25,1]'" (right). - The top figures are visualized using ResInsight, while the lower figures are generated using plopm (see/run `docs_theory_transformations.sh `_). + theory/coarsening + theory/refinement + theory/submodels + theory/transformations diff --git a/docs/_sources/theory/coarsening.rst.txt b/docs/_sources/theory/coarsening.rst.txt new file mode 100644 index 0000000..e2a4ce1 --- /dev/null +++ b/docs/_sources/theory/coarsening.rst.txt @@ -0,0 +1,99 @@ +.. _theory-coarsening: + +Grid coarsening +=============== +The coarsening approach implemented in **pycopm** is based on industry standard grid formats which is preserved during coarsening. +This allows for direct application in standard reservoir simulators. In coarsening accuracy is traded for speed. +This is often appropriate, but care must be taken to assure that the coarse solution preserve the key properties of the model. +The `Lie 2019 textbook `_ +gives an excellent introduction to grid coarsening and upscaling geological properties, and the methods implemented in **pycopm** follow similar techniques. + +Corner-point grids are commonly use in subsurface simulations, due to its flexibility to handle complex geometries such as faults. +The grid is defined by vertical pillars and horizontal lines connecting the pillars, resulting in cells that can have up to six faces (hexahedrons), +but also these grids allow to define connections between non-neighboring cells (NNC). A special case of corner-point grids are Cartesian regular grids. +These grids also feature inactive cells, which are cells with zero pore volume and no connected to the active cells. + +We define the grid coarsening operator :math:`\Xi_{xyz}^C`, which creates from an input grid :math:`\Omega` a coarser grid :math:`\Omega^*` (we adopt the notation * for +coarser quantities, while :math:`{}^C` in the coarsening operator emphasize the different approaches for the cell clustering) by removing +vertical pillars and horizontal lines: + +.. math:: + \Xi_{xyz}^C(\Omega)\rightarrow\Omega^* + +To give flexibility in the coarsening, the cell clustering can be given as x, y, and z arrays to define the pillars/lines to be removed. +One natural question is how to handle the inactive cells in a cluster, and for this, three options to define the coarser cells are implemented: +min, max, and mode (mode is the default, i.e., the coarser cells is active if the number of active cells is the most common value in the cluster). +Figure 1 shows a simple 2D corner-point grid with different coarsening using min, max, and mode. + +.. figure:: ../figs/coarsening_plopm.png + + Figure 1: Example of coarser models from a grid with 18 cells, where the cell #11 is inactive. + One application where the max option could be useful is for models where there are a lot of + inactive cells in the z direction, while the min option could be useful for models applying + coarsening in the xy plane, since using min results in coarser models that do not generate + new connections across inactive cells. + +.. tip:: + + The figure can be generated by executing in the terminal (`docs_theory_coarsening.sh `_): + + .. code-block:: bash + + . ./tests/scripts/docs_theory_coarsening.sh + + +For upscaling geophysical properties, naturally, the pore volume in a coarser cell :math:`\Phi_{i^*,j^*,k^*}^*` (:math:`i^*`, :math:`j^*`, and :math:`k^*` referring to the cell +coarse indices in the x, y, and z direction respectively) are equal to the sum of pore volume from the corresponding cells +:math:`\Phi_{i,j,k}` in the input model, which are part of the cluster :math:`\mathbb{C}_{i^*,j^*,k^*}`: + +.. math:: + \Phi_{i^*,j^*,k^*}^* = \sum_{(i,j,k)\in\mathbb{C}_{i^*,j^*,k^*}}\Phi_{i,j,k} + +For example, in Figure 1 when coarsening into two cells using mode/max resulted in two coarse cells, where: + +.. math:: + + \mathbb{C}_{1^*,1^*,1^*}=\{ (1,1,1),(2,1,1),(3,1,1),(1,1,2),(2,1,2),(3,1,2),(1,1,3),(2,1,3),(3,1,3) \}. + +From this definition, the porosity in the coarse model :math:`\phi_{i^*,j^*,k^*}^*` can be simply computed by: + +.. math:: + \phi_{i^*,j^*,k^*}^* = \frac{\Phi_{i^*,j^*,k^*}^*}{\mathbb{V}_{i^*,j^*,k^*}^*} + +where :math:`\mathbb{V}_{i^*,j^*,k^*}^*` is the geometric volume of the coarser cell. + +For the rock permeability, there are different upscaling methods (e.g., arithmetic or harmonic average) that are case dependent and perform different, +see the `Lie 2019 textbook `_ +for comparison of these methods. In **pycopm**, by default the permeability in the x and y directions are computed using the arithmetic average, while +the permeability in the z direction by the harmonic average. As additional options, the permeabilities in the coarser cells :math:`\mathbb{K}_{i^*,j^*,k^*}^*` can +be set to equal the min, max, mean, or pv-weighted mean (pvmean) values from the permeabilities in the corresponding cluster :math:`\mathbb{K}_{i,j,k}`. For example, using the max for +permeabilities could be useful for history matching studies, where the parameters to history match are saturation functions +(relative permeabilities and capillary pressure). + +The above line mentions the initial application to develop **pycopm** (coarsening to history match saturation functions), as such there are no upscaling +methods implemented in **pycopm** for saturation functions. In a geological model, it is common to define different regions (referred as satnum) to assign +different saturation function tables. Then, if a cluster :math:`\mathbb{C}_{i^*,j^*,k^*}` involves different values for satnum, the mode (the most frequent value) is used +to assign the value in the coarser cell (this is also used to assign additional discrete coarser values such as fluid-in-place regions (fipnum)). + +For grids with large number of non-neighboring connections (faults) and inactive cells, then a better approach is to upscale transmisibilities. +A drawback of upscaling transmissibilities is that permeabilities cannot be used in history matching, but instead, transmissibilities multipliers, +which increases the number of parameters to history match and might break history match workflows where different permeability fields are generated +from spatial correlations. To this end, two approaches to upscale transmissibilities are implemented in **pycopm**. The former computes the coarser +transmissibility + +.. math:: + \scriptsize\mathbb{T}_{i^*,j^*,k^*}^*=\left[ T_{i^*,j^*,k^*\rightarrow i^*+1,j^*,k^*}^*,T_{i^*-1,j^*,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*+1,k^*}^*,T_{i^*,j^*-1,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*,T_{i^*,j^*,k^*-1\rightarrow i^*,j^*,k^*}^* \right] + +using the armonic averaging along the transsmissibility direction and summing over these values over the cell coarser face. For example, +for the transmissibility in the z+ direction: + +.. math:: + T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*=\sum_{(i,j)\in\mathbb{C}_{i^*,j^*,k^*}}\left( \sum_{(i,j,k)\rightarrow(i,j,k+1)\in\mathbb{C}_{i^*,j^*,k^*}} \frac{1}{T_{i,j,k\rightarrow i,j,k+1}} \right)^{-1} + +For cases where the coarsening is only along one direction (e.g., the z direction), the second method sets the transmissibility on the coarse cell +faces in that direction equal to the overlapping cell face values in the corresponding cluster (instead of computing the armonic average). For input +models with a large number of inactive cells, this approach has resulted in better results with respect to the input model simulations than using the +armonic average. For both approaches, the transmissibilities are scaled with the ratio of the cell +face effective areas (input model) to the coarse cell area. For non-neighbouring connections, this approach is also implemented, i.e., +the non-neighbouring connections in the coarser model sum the values from the non-neighbouring connections in the input model, which is important in +order to honor the pressure connections along open faults communicating different formations. diff --git a/docs/_sources/theory/refinement.rst.txt b/docs/_sources/theory/refinement.rst.txt new file mode 100644 index 0000000..4c837af --- /dev/null +++ b/docs/_sources/theory/refinement.rst.txt @@ -0,0 +1,13 @@ +.. _theory-refinement: + +Grid refinement +=============== +The grid refinement is achieved by adding vertical pillars and horizontal lines in the grid from the input model. The refinement can be defined globally +in any direction (i, j, or k), as well as localized in defined grid indices. Properties such as porosity, permeabilities, and region numbers are set to the +same value in the corresponding finner cells inside the unrefined cell. Model properties defined by i,j,k locations such as wells, faults, and +boundary conditions are mapped to the new range of refined indices (i.e., adding additional entries to the generated deck). + +.. figure:: ../figs/refinement.png +.. figure:: ../figs/refinement_plopm.png + + Figure 2: Faults and wells in `MODEL3.DATA `_ (left) and after grid refinement "-g 2,2,2" (right) (upper and lower visualizations using `ResInsight `_ and `plopm `_ respectively (see/run `docs_theory_refinements.sh `_)). diff --git a/docs/_sources/theory/submodels.rst.txt b/docs/_sources/theory/submodels.rst.txt new file mode 100644 index 0000000..8eb8711 --- /dev/null +++ b/docs/_sources/theory/submodels.rst.txt @@ -0,0 +1,31 @@ +.. _theory-submodels: + +Submodels +========= +The generation of a submodel, i.e., a selected region in the input model, makes possible to lower the number of active cells and focus on an area of interest in +the input model. This results in smaller size of the input files, and faster simulations using OPM Flow. The submodel can be defined by properties matching a +value, e.g., all cells with fipnum equal to 1, or by a polygon given the xy locations in meters. Model properties defined by i,j,k locations such as wells and faults +are shifted to their corresponding values. If the wells/faults are not inside the extracted submodel, then these are not written to the generated deck. + +Regarding the boundary conditions in the extracted model with respect to the pore volume outisde the submodel, four options are provided by **pycopm**: + +#. no correction for the pore volume +#. adding the pore volume in each cell on the submodel boundary by summing all cell pore volumes in their corresponding i and j directions. If there is pore volume in the outside corners, this is equally distributed among the boundary cells in the two corresponding sides. +#. distributing the pore volume equally among the boundary cells in the submodel. +#. distributing the pore volume equally among all cells in the submodel + +.. figure:: ../figs/submodel.png +.. figure:: ../figs/submodel_plopm.png + + Figure 3: The shape to extract the sudmodel corresponds to "-v 'xypolygon [50,90] [60,60] [90,60] [65,40] [75,10] [50,30] [25,10] [35,40] [10,60] [40,60] [50,90]'". + The j indices for the cells have been accordingly shifted in the extracted model, and the right figure shows the projected pore volume on the boundary. Upper figures + are generated from ResInsight (screenshots) while the lower figures using plopm (see/run `docs_theory_submodels.sh `_). + +In addition, it is possible to extract submodels around wells, with three different options for the neighbourhood: box, diamond, and diamondxy. The box option allows to define +the intervals to extract the cells, while the diamond and diamondxy results in fewer cells since the cells in the corners are trimmed. + +.. figure:: ../figs/submodelwell.png +.. figure:: ../figs/submodelwell_plopm.png + + Figure 4: The submodel in `norne `_ by executing "-v 'E-3H diamondxy 0' -p 1", "-v 'E-3H diamond 1' -p 1", and "-v 'E-3H box [-1,2] [-2,3] [-1,1]' -p 1" respectively. + The top figures are visualized using ResInsight, while the lower figures are the 2D front view of the models using plopm (see/run `docs_theory_submodels.sh `_). diff --git a/docs/_sources/theory/transformations.rst.txt b/docs/_sources/theory/transformations.rst.txt new file mode 100644 index 0000000..49cf1d4 --- /dev/null +++ b/docs/_sources/theory/transformations.rst.txt @@ -0,0 +1,15 @@ +.. _theory-transformations: + +Transformations +=============== +Affine transformations are widely used in diverse applications since they preserve points, straight lines, and planes. In the field of reservoir management, there are large +uncertainties in the characterization of geological formations (reservoirs are typically located several kilometers below the surface). Once a reservoir model is created, over time +additional information from field measurements (e.g., seismic data, addiitonal wells, well's pressures, production rates) can indicate a different model characterization. This is when having +tools like **pycopm** can be handy, i.e., to apply translations of the grid (e.g., a different depth which impacts the pressure), scaling (e.g., to ease comparison between models made by different +groups which missmatch in the thickness of layers), and rotations (e.g., to align grids betweens two different models). + +.. figure:: ../figs/transformation.png +.. figure:: ../figs/transformation_plopm.png + + Figure 5: Extracted shape in Figure 3 after a rotation "-d 'rotatexy 45'" (left) and scaling "-d 'scale [1,0.25,1]'" (right). + The top figures are visualized using ResInsight, while the lower figures are generated using plopm (see/run `docs_theory_transformations.sh `_). diff --git a/docs/_sources/tutorial.rst.txt b/docs/_sources/tutorial.rst.txt new file mode 100644 index 0000000..1eef150 --- /dev/null +++ b/docs/_sources/tutorial.rst.txt @@ -0,0 +1,22 @@ +.. _tutorial: + +Tutorial +======== + +Learn the actively developed generic deck workflow with the bundled ``HELLO_WORLD.DATA`` model. The tutorial covers coarsening, active-cell and pore-volume handling, submodel extraction, refinement, transformations, visualization, and explicit initialization. + +.. grid:: 1 1 1 1 + :gutter: 3 + + .. grid-item-card:: Hello World: generic deck workflow + :link: tutorial-hello-world + :link-type: ref + :img-top: figs/hello_world_3.png + + Work through all four generic model operations without losing the original example detail. + +.. toctree:: + :hidden: + :maxdepth: 1 + + tutorial/hello-world diff --git a/docs/_sources/tutorial/hello-world.rst.txt b/docs/_sources/tutorial/hello-world.rst.txt new file mode 100644 index 0000000..30a6f13 --- /dev/null +++ b/docs/_sources/tutorial/hello-world.rst.txt @@ -0,0 +1,90 @@ +.. _tutorial-hello-world: + +Hello World: generic deck workflow +================================== + +This is the recommended starting point for new pycopm users. + +Learning objectives +------------------- + +* Coarsen and run a small OPM Flow deck. +* Control pore-volume and active-cell aggregation. +* Extract a polygonal submodel. +* Refine selected intervals. +* Rotate a generated grid. +* Visualize outputs and optionally write explicit solution values. + +Complete workflow +----------------- + +For the `HELLO_WORLD.DATA `_ deck, by executing: + +.. code-block:: bash + + # From inside the main pycopm folder + cd examples/decks + pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output + +.. note:: + + If the folder to flow is not added to your path, then pass the full path to the flow executable using the flag **-f /path/to/flow**. + +Using `plopm `_, then we can visualize the generated files in the output folder: + +.. code-block:: bash + + flow HELLO_WORLD + flow HELLO_WORLD_PYCOPM + plopm -i HELLO_WORLD -v porv -s ,,1 -ge 'black,1e-2' -fn hello_world_1_left + plopm -i HELLO_WORLD_PYCOPM -v porv -s ,,1 -ge 'black,1e-2' -fn hello_world_1_right + +.. figure:: ../figs/hello_world_1.png + + Dry run from the input cloned deck (left) and (right) coarsened model. Adding the flag **-p 1** would add the remove pore volume to the neighbouring cells (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). + +As mentioned above, if you do not have ResInsight, then to visualize the results in ParaView run + +.. code-block:: bash + + flow HELLO_WORLD.DATA --enable-vtk-output=true + flow HELLO_WORLD_PYCOPM.DATA --enable-vtk-output=true + +To make active the coarsened cell where there is only one active cell, this can be achieved by: + +.. code-block:: bash + + pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -a max + +.. figure:: ../figs/hello_world_2.png + + Dry run from the input cloned deck (left) and (right) coarsened model. The region numbers by default are given by the mode, e.g., use the flag **-n max** to keep the maximum integer (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). + +As described in the :doc:`theory <../theory>`, **pycopm** can be not only used for grid coarsening, but also to apply grid refinements, submodels, and transformations. +Then, with the following commands first we substract a submodel around the isolated grid cell proyecting the outside pore volume on the boundaries, after +we apply a grid refinement on the cells in the middle x and y location, and finally we rotate the model 45 degrees. + +.. code-block:: bash + + pycopm -i HELLO_WORLD.DATA -v 'xypolygon [4,8.5] [4,16.5] [11.5,16.5] [11.5,8.5] [4,8.5]' -p 1 -m all + pycopm -i HELLO_WORLD_PYCOPM.DATA -rx 0,0,0,2,0,0,0 -ry 0,0,0,2,0,0,0 -m all + pycopm -i HELLO_WORLD_PYCOPM_PYCOPM.DATA -d 'rotatexy 45' -m all + +.. figure:: ../figs/hello_world_3.png + + Extracted region with the projected pore volumes (bottom left), refinement around the center cells (top right), and rotation (bottom right). + The text in the legends highlight that the pore volume is conserved (35.58) and the number of active cells is reduced from 351 to 25 in the + submodel and after increased to 41 due to the grid refinement (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). + +.. note:: + + To write the cell values for the SOLUTION section instead of using the EQUIL keyword, this can be achieved by the flag **-explicit 1**; the + only requirement is that the EQUIL keyword needs to be in the main input DATA file and no via INCLUDE files. + + +Next steps +---------- + +* Read :doc:`../theory` for equations and method details. +* Use :doc:`../command-line` for defaults and compatibility. +* Continue with :doc:`../examples` for larger models. diff --git a/docs/_sphinx_design_static/design-tabs.js b/docs/_sphinx_design_static/design-tabs.js new file mode 100644 index 0000000..b25bd6a --- /dev/null +++ b/docs/_sphinx_design_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ +function ready() { + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } + } + } + }); +} + +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onSDLabelClick() { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { + if (label === this) continue; + // @ts-ignore + label.previousElementSibling.checked = true; + } + window.sessionStorage.setItem(storageKeyPrefix + group, id); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/docs/_sphinx_design_static/sphinx-design.min.css b/docs/_sphinx_design_static/sphinx-design.min.css new file mode 100644 index 0000000..860c36d --- /dev/null +++ b/docs/_sphinx_design_static/sphinx-design.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative;font-size:var(--sd-fontsize-dropdown)}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary.sd-summary-title{padding:.5em .6em .5em 1em;font-size:var(--sd-fontsize-dropdown-title);font-weight:var(--sd-fontweight-dropdown-title);user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;list-style:none;display:inline-flex;justify-content:space-between}details.sd-dropdown summary.sd-summary-title::-webkit-details-marker{display:none}details.sd-dropdown summary.sd-summary-title:focus{outline:none}details.sd-dropdown summary.sd-summary-title .sd-summary-icon{margin-right:.6em;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary.sd-summary-title .sd-summary-text{flex-grow:1;line-height:1.5;padding-right:.5rem}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker{pointer-events:none;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker svg{opacity:.6}details.sd-dropdown summary.sd-summary-title:hover .sd-summary-state-marker svg{opacity:1;transform:scale(1.1)}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown .sd-summary-chevron-right{transition:.25s}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-right{transform:rotate(90deg)}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-down{transform:rotate(180deg)}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-bg: rgba(0, 113, 188, 0.2);--sd-color-secondary-bg: rgba(108, 117, 125, 0.2);--sd-color-success-bg: rgba(40, 167, 69, 0.2);--sd-color-info-bg: rgba(23, 162, 184, 0.2);--sd-color-warning-bg: rgba(240, 179, 126, 0.2);--sd-color-danger-bg: rgba(220, 53, 69, 0.2);--sd-color-light-bg: rgba(248, 249, 250, 0.2);--sd-color-muted-bg: rgba(108, 117, 125, 0.2);--sd-color-dark-bg: rgba(33, 37, 41, 0.2);--sd-color-black-bg: rgba(0, 0, 0, 0.2);--sd-color-white-bg: rgba(255, 255, 255, 0.2);--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem;--sd-fontsize-dropdown: inherit;--sd-fontsize-dropdown-title: 1rem;--sd-fontweight-dropdown-title: 700} diff --git a/docs/_static/_sphinx_javascript_frameworks_compat.js b/docs/_static/_sphinx_javascript_frameworks_compat.js deleted file mode 100644 index 8141580..0000000 --- a/docs/_static/_sphinx_javascript_frameworks_compat.js +++ /dev/null @@ -1,123 +0,0 @@ -/* Compatability shim for jQuery and underscores.js. - * - * Copyright Sphinx contributors - * Released under the two clause BSD licence - */ - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x - } - return decodeURIComponent(x.replace(/\+/g, ' ')); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} diff --git a/docs/_static/check-solid.svg b/docs/_static/check-solid.svg new file mode 100644 index 0000000..92fad4b --- /dev/null +++ b/docs/_static/check-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/_static/clipboard.min.js b/docs/_static/clipboard.min.js new file mode 100644 index 0000000..54b3c46 --- /dev/null +++ b/docs/_static/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 + + + + diff --git a/docs/_static/copybutton.css b/docs/_static/copybutton.css new file mode 100644 index 0000000..f1916ec --- /dev/null +++ b/docs/_static/copybutton.css @@ -0,0 +1,94 @@ +/* Copy buttons */ +button.copybtn { + position: absolute; + display: flex; + top: .3em; + right: .3em; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + /* The colors that GitHub uses */ + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +button.copybtn.success { + border-color: #22863a; + color: #22863a; +} + +button.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +div.highlight { + position: relative; +} + +/* Show the copybutton */ +.highlight:hover button.copybtn, button.copybtn.success { + opacity: 1; +} + +.highlight button.copybtn:hover { + background-color: rgb(235, 235, 235); +} + +.highlight button.copybtn:active { + background-color: rgb(187, 187, 187); +} + +/** + * A minimal CSS-only tooltip copied from: + * https://codepen.io/mildrenben/pen/rVBrpK + * + * To use, write HTML like the following: + * + *

Short

+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/docs/_static/copybutton.js b/docs/_static/copybutton.js new file mode 100644 index 0000000..bfda98a --- /dev/null +++ b/docs/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '>>> |\\.\\.\\. |\\$ |# ', true, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/docs/_static/copybutton_funcs.js b/docs/_static/copybutton_funcs.js new file mode 100644 index 0000000..dbe1aaa --- /dev/null +++ b/docs/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/docs/_static/css/badge_only.css b/docs/_static/css/badge_only.css deleted file mode 100644 index 88ba55b..0000000 --- a/docs/_static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} \ No newline at end of file diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/_static/css/fonts/Roboto-Slab-Bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Bold.woff and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/_static/css/fonts/Roboto-Slab-Regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Regular.woff and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.eot b/docs/_static/css/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca..0000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.svg b/docs/_static/css/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845..0000000 --- a/docs/_static/css/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_static/css/fonts/fontawesome-webfont.ttf b/docs/_static/css/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2..0000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff b/docs/_static/css/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a..0000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff2 b/docs/_static/css/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc6..0000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff b/docs/_static/css/fonts/lato-bold-italic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/_static/css/fonts/lato-bold-italic.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff2 b/docs/_static/css/fonts/lato-bold-italic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/_static/css/fonts/lato-bold-italic.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold.woff b/docs/_static/css/fonts/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/_static/css/fonts/lato-bold.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold.woff2 b/docs/_static/css/fonts/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/_static/css/fonts/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff b/docs/_static/css/fonts/lato-normal-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/_static/css/fonts/lato-normal-italic.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff2 b/docs/_static/css/fonts/lato-normal-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/_static/css/fonts/lato-normal-italic.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal.woff b/docs/_static/css/fonts/lato-normal.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/_static/css/fonts/lato-normal.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal.woff2 b/docs/_static/css/fonts/lato-normal.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/_static/css/fonts/lato-normal.woff2 and /dev/null differ diff --git a/docs/_static/css/theme.css b/docs/_static/css/theme.css deleted file mode 100644 index a88467c..0000000 --- a/docs/_static/css/theme.css +++ /dev/null @@ -1,4 +0,0 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%;float:none;margin-left:0}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..b704b1d --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,101 @@ +:root { + --pst-color-primary: #1769aa; + --pst-color-secondary: #3f8fc5; + --pst-color-link: #1769aa; + --pst-color-link-hover: #0f4f82; +} + +html[data-theme="dark"] { + --pst-color-primary: #71b7e6; + --pst-color-secondary: #8cc8ec; + --pst-color-link: #71b7e6; + --pst-color-link-hover: #a7d8f4; +} + +.bd-main .bd-content .bd-article-container { + max-width: 72rem; +} + +.bd-page-width { + max-width: 96rem; +} + +.bd-header .navbar-header-items__start { + min-width: max-content; +} + +.navbar-brand .title { + font-weight: 700; + letter-spacing: 0.01em; +} + +.bd-header .navbar-brand img { + max-height: 102px; +} + +.sd-card { + height: 100%; + overflow: hidden; + border-radius: 0.75rem; + transition: + transform 160ms ease, + box-shadow 160ms ease, + border-color 160ms ease; +} + +.sd-card-title { + font-weight: 650; +} + +.sd-card:hover { + transform: translateY(-3px); + border-color: var(--pst-color-primary); + box-shadow: 0 0.5rem 1.25rem rgb(0 0 0 / 14%); +} + +.sd-card .sd-card-title { + color: var(--pst-color-primary); +} + +.sd-card .sd-card-body { + padding: 1rem; +} + +.rst-class.lead, +.lead { + font-size: 1.2rem; + color: var(--pst-color-text-muted); +} + +div.highlight pre { + font-size: 0.9rem; +} + +table.docutils { + width: 100%; +} + +.sd-card-img-top { + display: block; + width: 100%; + height: 250px; + object-fit: contain; + object-position: center; + padding: 0.75rem; + background: var(--pst-color-surface); + border-bottom: 1px solid var(--pst-color-border); +} + +html[data-theme="dark"] .sd-card-img-top { + opacity: 0.92; +} + +.related-projects .sd-card-img-top { + height: 260px; + padding: 1rem; +} + +.related-projects .sd-card-footer { + color: var(--pst-color-primary); + font-weight: 600; +} diff --git a/docs/_static/design-tabs.js b/docs/_static/design-tabs.js new file mode 100644 index 0000000..b25bd6a --- /dev/null +++ b/docs/_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ +function ready() { + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } + } + } + }); +} + +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onSDLabelClick() { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { + if (label === this) continue; + // @ts-ignore + label.previousElementSibling.checked = true; + } + window.sessionStorage.setItem(storageKeyPrefix + group, id); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 7e4c114..5a01a39 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '', + VERSION: '2026.4', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/_static/fonts/Lato/lato-bold.eot b/docs/_static/fonts/Lato/lato-bold.eot deleted file mode 100644 index 3361183..0000000 Binary files a/docs/_static/fonts/Lato/lato-bold.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.ttf b/docs/_static/fonts/Lato/lato-bold.ttf deleted file mode 100644 index 29f691d..0000000 Binary files a/docs/_static/fonts/Lato/lato-bold.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.woff b/docs/_static/fonts/Lato/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/_static/fonts/Lato/lato-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.woff2 b/docs/_static/fonts/Lato/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/_static/fonts/Lato/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.eot b/docs/_static/fonts/Lato/lato-bolditalic.eot deleted file mode 100644 index 3d41549..0000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.ttf b/docs/_static/fonts/Lato/lato-bolditalic.ttf deleted file mode 100644 index f402040..0000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.woff b/docs/_static/fonts/Lato/lato-bolditalic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/_static/fonts/Lato/lato-bolditalic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.eot b/docs/_static/fonts/Lato/lato-italic.eot deleted file mode 100644 index 3f82642..0000000 Binary files a/docs/_static/fonts/Lato/lato-italic.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.ttf b/docs/_static/fonts/Lato/lato-italic.ttf deleted file mode 100644 index b4bfc9b..0000000 Binary files a/docs/_static/fonts/Lato/lato-italic.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.woff b/docs/_static/fonts/Lato/lato-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/_static/fonts/Lato/lato-italic.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.woff2 b/docs/_static/fonts/Lato/lato-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/_static/fonts/Lato/lato-italic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.eot b/docs/_static/fonts/Lato/lato-regular.eot deleted file mode 100644 index 11e3f2a..0000000 Binary files a/docs/_static/fonts/Lato/lato-regular.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.ttf b/docs/_static/fonts/Lato/lato-regular.ttf deleted file mode 100644 index 74decd9..0000000 Binary files a/docs/_static/fonts/Lato/lato-regular.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.woff b/docs/_static/fonts/Lato/lato-regular.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/_static/fonts/Lato/lato-regular.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.woff2 b/docs/_static/fonts/Lato/lato-regular.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/_static/fonts/Lato/lato-regular.woff2 and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot deleted file mode 100644 index 79dc8ef..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf deleted file mode 100644 index df5d1df..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot deleted file mode 100644 index 2f7ca78..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf deleted file mode 100644 index eb52a79..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 and /dev/null differ diff --git a/docs/_static/jquery.js b/docs/_static/jquery.js deleted file mode 100644 index c4c6022..0000000 --- a/docs/_static/jquery.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t a.language.name.localeCompare(b.language.name)); - - const languagesHTML = ` -
-
Languages
- ${languages - .map( - (translation) => ` -
- ${translation.language.code} -
- `, - ) - .join("\n")} -
- `; - return languagesHTML; - } - - function renderVersions(config) { - if (!config.versions.active.length) { - return ""; - } - const versionsHTML = ` -
-
Versions
- ${config.versions.active - .map( - (version) => ` -
- ${version.slug} -
- `, - ) - .join("\n")} -
- `; - return versionsHTML; - } - - function renderDownloads(config) { - if (!Object.keys(config.versions.current.downloads).length) { - return ""; - } - const downloadsNameDisplay = { - pdf: "PDF", - epub: "Epub", - htmlzip: "HTML", - }; - - const downloadsHTML = ` -
-
Downloads
- ${Object.entries(config.versions.current.downloads) - .map( - ([name, url]) => ` -
- ${downloadsNameDisplay[name]} -
- `, - ) - .join("\n")} -
- `; - return downloadsHTML; - } - - document.addEventListener("readthedocs-addons-data-ready", function (event) { - const config = event.detail.data(); - - const flyout = ` -
- - Read the Docs - v: ${config.versions.current.slug} - - -
-
- ${renderLanguages(config)} - ${renderVersions(config)} - ${renderDownloads(config)} -
-
On Read the Docs
-
- Project Home -
-
- Builds -
-
- Downloads -
-
-
-
Search
-
-
- -
-
-
-
- - Hosted by Read the Docs - -
-
- `; - - // Inject the generated flyout into the body HTML element. - document.body.insertAdjacentHTML("beforeend", flyout); - - // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout. - document - .querySelector("#flyout-search-form") - .addEventListener("focusin", () => { - const event = new CustomEvent("readthedocs-search-show"); - document.dispatchEvent(event); - }); - }) -} - -if (themeLanguageSelector || themeVersionSelector) { - function onSelectorSwitch(event) { - const option = event.target.selectedIndex; - const item = event.target.options[option]; - window.location.href = item.dataset.url; - } - - document.addEventListener("readthedocs-addons-data-ready", function (event) { - const config = event.detail.data(); - - const versionSwitch = document.querySelector( - "div.switch-menus > div.version-switch", - ); - if (themeVersionSelector) { - let versions = config.versions.active; - if (config.versions.current.hidden || config.versions.current.type === "external") { - versions.unshift(config.versions.current); - } - const versionSelect = ` - - `; - - versionSwitch.innerHTML = versionSelect; - versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); - } - - const languageSwitch = document.querySelector( - "div.switch-menus > div.language-switch", - ); - - if (themeLanguageSelector) { - if (config.projects.translations.length) { - // Add the current language to the options on the selector - let languages = config.projects.translations.concat( - config.projects.current, - ); - languages = languages.sort((a, b) => - a.language.name.localeCompare(b.language.name), - ); - - const languageSelect = ` - - `; - - languageSwitch.innerHTML = languageSelect; - languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); - } - else { - languageSwitch.remove(); - } - } - }); -} - -document.addEventListener("readthedocs-addons-data-ready", function (event) { - // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav. - document - .querySelector("[role='search'] input") - .addEventListener("focusin", () => { - const event = new CustomEvent("readthedocs-search-show"); - document.dispatchEvent(event); - }); -}); \ No newline at end of file diff --git a/docs/_static/pygments.css b/docs/_static/pygments.css index 6f8b210..d7dd577 100644 --- a/docs/_static/pygments.css +++ b/docs/_static/pygments.css @@ -1,75 +1,152 @@ -pre { line-height: 125%; } -td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #F00 } /* Error */ -.highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.highlight .o { color: #666 } /* Operator */ -.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #9C6500 } /* Comment.Preproc */ -.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ -.highlight .gr { color: #E40000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #008400 } /* Generic.Inserted */ -.highlight .go { color: #717171 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #04D } /* Generic.Traceback */ -.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #B00040 } /* Keyword.Type */ -.highlight .m { color: #666 } /* Literal.Number */ -.highlight .s { color: #BA2121 } /* Literal.String */ -.highlight .na { color: #687822 } /* Name.Attribute */ -.highlight .nb { color: #008000 } /* Name.Builtin */ -.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */ -.highlight .no { color: #800 } /* Name.Constant */ -.highlight .nd { color: #A2F } /* Name.Decorator */ -.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #00F } /* Name.Function */ -.highlight .nl { color: #767600 } /* Name.Label */ -.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #19177C } /* Name.Variable */ -.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #BBB } /* Text.Whitespace */ -.highlight .mb { color: #666 } /* Literal.Number.Bin */ -.highlight .mf { color: #666 } /* Literal.Number.Float */ -.highlight .mh { color: #666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666 } /* Literal.Number.Oct */ -.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ -.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ -.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ -.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #00F } /* Name.Function.Magic */ -.highlight .vc { color: #19177C } /* Name.Variable.Class */ -.highlight .vg { color: #19177C } /* Name.Variable.Global */ -.highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.highlight .vm { color: #19177C } /* Name.Variable.Magic */ -.highlight .il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #fae4c2 } +html[data-theme="light"] .highlight { background: #fefefe; color: #080808 } +html[data-theme="light"] .highlight .c { color: #515151 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #A12236 } /* Error */ +html[data-theme="light"] .highlight .k { color: #6730C5 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #7F4707 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #080808 } /* Name */ +html[data-theme="light"] .highlight .o { color: #00622F } /* Operator */ +html[data-theme="light"] .highlight .p { color: #080808 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #515151 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #515151 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #515151 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #515151 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #515151 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #515151 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #005B82 } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #005B82 } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #005B82 } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #6730C5 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #6730C5 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #6730C5 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #6730C5 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #6730C5 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #7F4707 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #7F4707 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #7F4707 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #00622F } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #912583 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #7F4707 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #005B82 } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #005B82 } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #7F4707 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #00622F } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #6730C5 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #005B82 } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #7F4707 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #080808 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #080808 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #005B82 } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #005B82 } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #A12236 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #6730C5 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #080808 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #080808 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #7F4707 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #7F4707 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #7F4707 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #7F4707 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #7F4707 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #00622F } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #00622F } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #00622F } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #00622F } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #00622F } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #00622F } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #00622F } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #00622F } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #00622F } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #00622F } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #A12236 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #00622F } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #005B82 } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #7F4707 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #005B82 } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #A12236 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #A12236 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #A12236 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #7F4707 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #7F4707 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #F8F8F2 } +html[data-theme="dark"] .highlight .c { color: #FFD900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #FFA07A } /* Error */ +html[data-theme="dark"] .highlight .k { color: #DCC6E0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #FFD900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #F8F8F2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #ABE338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #F8F8F2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #FFD900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #FFD900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #FFD900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #FFD900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #FFD900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #FFD900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00E0E0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00E0E0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00E0E0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #DCC6E0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #DCC6E0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #DCC6E0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #DCC6E0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #DCC6E0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #FFD900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #FFD900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #FFD900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #ABE338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #FFD900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #FFD900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00E0E0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00E0E0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #FFD900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #ABE338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #DCC6E0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00E0E0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #FFD900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #F8F8F2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #F8F8F2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00E0E0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00E0E0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #FFA07A } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #DCC6E0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #F8F8F2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #F8F8F2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #FFD900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #FFD900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #FFD900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #FFD900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #FFD900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #ABE338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #ABE338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #ABE338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #ABE338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #ABE338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #ABE338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #ABE338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #ABE338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #ABE338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #ABE338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #FFA07A } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #ABE338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00E0E0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #FFD900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00E0E0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #FFA07A } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #FFA07A } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #FFA07A } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #FFD900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #FFD900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_static/scripts/bootstrap.js b/docs/_static/scripts/bootstrap.js new file mode 100644 index 0000000..92d9df9 --- /dev/null +++ b/docs/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>E,afterRead:()=>v,afterWrite:()=>T,applyStyles:()=>$,arrow:()=>J,auto:()=>a,basePlacements:()=>l,beforeMain:()=>y,beforeRead:()=>_,beforeWrite:()=>A,bottom:()=>s,clippingParents:()=>d,computeStyles:()=>it,createPopper:()=>Dt,createPopperBase:()=>St,createPopperLite:()=>$t,detectOverflow:()=>_t,end:()=>h,eventListeners:()=>st,flip:()=>bt,hide:()=>wt,left:()=>r,main:()=>w,modifierPhases:()=>O,offset:()=>Et,placements:()=>g,popper:()=>f,popperGenerator:()=>Lt,popperOffsets:()=>At,preventOverflow:()=>Ct,read:()=>b,reference:()=>p,right:()=>o,start:()=>c,top:()=>n,variationPlacements:()=>m,viewport:()=>u,write:()=>C});var i={};t.r(i),t.d(i,{Alert:()=>xe,Button:()=>Le,Carousel:()=>ci,Collapse:()=>Ai,Dropdown:()=>Qi,Modal:()=>Sn,Offcanvas:()=>Qn,Popover:()=>vs,ScrollSpy:()=>Ss,Tab:()=>Zs,Toast:()=>mo,Tooltip:()=>ps});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",d="clippingParents",u="viewport",f="popper",p="reference",m=l.reduce(function(t,e){return t.concat([e+"-"+c,e+"-"+h])},[]),g=[].concat(l,[a]).reduce(function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])},[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",E="afterMain",A="beforeWrite",C="write",T="afterWrite",O=[_,b,v,y,w,E,A,C,T];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function L(t){return t instanceof k(t).Element||t instanceof Element}function S(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach(function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];S(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach(function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)}))})},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach(function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce(function(t,e){return t[e]="",t},{});S(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach(function(t){n.removeAttribute(t)}))})}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var N=Math.max,P=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map(function(t){return t.brand+"/"+t.version}).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&S(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(L(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function z(t){return k(t).getComputedStyle(t)}function R(t){return["table","td","th"].indexOf(x(t))>=0}function q(t){return((L(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function Y(t){return S(t)&&"fixed"!==z(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=Y(t);i&&R(i)&&"static"===z(i).position;)i=Y(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===z(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&S(t)&&"fixed"===z(t).position)return null;var i=V(t);for(D(i)&&(i=i.host);S(i)&&["html","body"].indexOf(x(i))<0;){var n=z(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return N(t,P(e,i))}function U(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce(function(e,i){return e[i]=t,e},{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,d=i.modifiersData.popperOffsets,u=I(i.placement),f=Q(u),p=[r,o].indexOf(u)>=0?"height":"width";if(h&&d){var m=function(t,e){return U("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=B(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-d[f]-i.rects.popper[p],y=d[f]-i.rects.reference[f],w=K(h),E=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,A=v/2-y/2,C=m[_],T=E-g[p]-m[b],O=E/2-g[p]/2+A,x=X(C,O,T),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y}):{x:b,y};b=w.x,y=w.y;var E=d.hasOwnProperty("x"),A=d.hasOwnProperty("y"),C=r,T=n,O=window;if(p){var x=K(i),L="clientHeight",S="clientWidth";x===k(i)&&"static"!==z(x=q(i)).position&&"absolute"===u&&(L="scrollHeight",S="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(T=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[L])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(C=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[S])-a.width,b*=f?1:-1)}var D,$=Object.assign({position:u},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y},k(i)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},$,((D={})[T]=A?"0":"",D[C]=E?"0":"",D.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",D)):Object.assign({},$,((e={})[T]=A?y+"px":"",e[C]=E?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach(function(t){t.addEventListener("scroll",i.update,nt)}),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach(function(t){t.removeEventListener("scroll",i.update,nt)}),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,function(t){return ot[t]})}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,function(t){return at[t]})}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(q(t)).left+ct(t).scrollLeft}function dt(t){var e=z(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ut(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:S(t)&&dt(t)?t:ut(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=ut(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],dt(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===u?pt(function(t,e){var i=k(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):L(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=q(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=N(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=N(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===z(s||i).direction&&(a+=N(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,d=l?I(l):null,u=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(d){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=d?Q(d):null;if(null!=m){var g="y"===m?"height":"width";switch(u){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?d:m,_=i.rootBoundary,b=void 0===_?u:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,E=void 0!==w&&w,A=i.padding,C=void 0===A?0:A,T=U("number"!=typeof C?C:G(C,l)),O=y===f?p:f,k=t.rects.popper,D=t.elements[E?O:y],$=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(z(t).position)>=0&&S(t)?K(t):t;return L(i)?e.filter(function(t){return L(t)&&W(t,i)&&"body"!==x(t)}):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce(function(e,i){var s=mt(t,i,n);return e.top=N(s.top,e.top),e.right=P(s.right,e.right),e.bottom=P(s.bottom,e.bottom),e.left=N(s.left,e.left),e},mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(L(D)?D:D.contextElement||q(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,strategy:"absolute",placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,B={top:$.top-F.top+T.top,bottom:F.bottom-$.bottom+T.bottom,left:$.left-F.left+T.left,right:F.right-$.right+T.right},R=t.modifiersData.offset;if(y===f&&R){var Y=R[a];Object.keys(B).forEach(function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";B[t]+=Y[i]*e})}return B}const bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=i.mainAxis,u=void 0===d||d,f=i.altAxis,p=void 0===f||f,_=i.fallbackPlacements,b=i.padding,v=i.boundary,y=i.rootBoundary,w=i.altBoundary,E=i.flipVariations,A=void 0===E||E,C=i.allowedAutoPlacements,T=e.options.placement,O=I(T),x=_||(O!==T&&A?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(T):[rt(T)]),k=[T].concat(x).reduce(function(t,i){return t.concat(I(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=Z(n),u=d?a?m:m.filter(function(t){return Z(t)===d}):l,f=u.filter(function(t){return h.indexOf(t)>=0});0===f.length&&(f=u);var p=f.reduce(function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e},{});return Object.keys(p).sort(function(t,e){return p[t]-p[e]})}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:A,allowedAutoPlacements:C}):i)},[]),L=e.rects.reference,S=e.rects.popper,D=new Map,$=!0,N=k[0],P=0;P=0,B=H?"width":"height",W=_t(e,{placement:M,boundary:v,rootBoundary:y,altBoundary:w,padding:b}),z=H?F?o:r:F?s:n;L[B]>S[B]&&(z=rt(z));var R=rt(z),q=[];if(u&&q.push(W[j]<=0),p&&q.push(W[z]<=0,W[R]<=0),q.every(function(t){return t})){N=M,$=!1;break}D.set(M,q)}if($)for(var V=function(t){var e=k.find(function(e){var i=D.get(e);if(i)return i.slice(0,t).every(function(t){return t})});if(e)return N=e,"break"},Y=A?3:1;Y>0&&"break"!==V(Y);Y--);e.placement!==N&&(e.modifiersData[h]._skip=!0,e.placement=N,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function yt(t){return[n,o,s,r].some(function(e){return t[e]>=0})}const wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=_t(e,{elementContext:"reference"}),a=_t(e,{altBoundary:!0}),l=vt(r,n),c=vt(a,s,o),h=yt(l),d=yt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,a=i.offset,l=void 0===a?[0,0]:a,c=g.reduce(function(t,i){return t[i]=function(t,e,i){var s=I(t),a=[r,n].indexOf(s)>=0?-1:1,l="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[r,o].indexOf(s)>=0?{x:h,y:c}:{x:c,y:h}}(i,e.rects,l),t},{}),h=c[e.placement],d=h.x,u=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=u),e.modifiersData[s]=c}},At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Ct={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,l=i.mainAxis,h=void 0===l||l,d=i.altAxis,u=void 0!==d&&d,f=i.boundary,p=i.rootBoundary,m=i.altBoundary,g=i.padding,_=i.tether,b=void 0===_||_,v=i.tetherOffset,y=void 0===v?0:v,w=_t(e,{boundary:f,rootBoundary:p,padding:g,altBoundary:m}),E=I(e.placement),A=Z(e.placement),C=!A,T=Q(E),O="x"===T?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,L=e.rects.popper,S="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,D="number"==typeof S?{mainAxis:S,altAxis:S}:Object.assign({mainAxis:0,altAxis:0},S),$=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,M={x:0,y:0};if(x){if(h){var j,F="y"===T?n:r,H="y"===T?s:o,W="y"===T?"height":"width",z=x[T],R=z+w[F],q=z-w[H],V=b?-L[W]/2:0,Y=A===c?k[W]:L[W],U=A===c?-L[W]:-k[W],G=e.elements.arrow,J=b&&G?B(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[F],it=tt[H],nt=X(0,k[W],J[W]),st=C?k[W]/2-V-nt-et-D.mainAxis:Y-nt-et-D.mainAxis,ot=C?-k[W]/2+V+nt+it+D.mainAxis:U+nt+it+D.mainAxis,rt=e.elements.arrow&&K(e.elements.arrow),at=rt?"y"===T?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(j=null==$?void 0:$[T])?j:0,ct=z+ot-lt,ht=X(b?P(R,z+st-lt-at):R,z,b?N(q,ct):q);x[T]=ht,M[T]=ht-z}if(u){var dt,ut="x"===T?n:r,ft="x"===T?s:o,pt=x[O],mt="y"===O?"height":"width",gt=pt+w[ut],bt=pt-w[ft],vt=-1!==[n,r].indexOf(E),yt=null!=(dt=null==$?void 0:$[O])?dt:0,wt=vt?gt:pt-k[mt]-L[mt]-yt+D.altAxis,Et=vt?pt+k[mt]+L[mt]-yt-D.altAxis:bt,At=b&&vt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,pt,Et):X(b?wt:gt,pt,b?Et:bt);x[O]=At,M[O]=At-pt}e.modifiersData[a]=M}},requiresIfExists:["offset"]};function Tt(t,e,i){void 0===i&&(i=!1);var n,s,o=S(e),r=S(e)&&function(t){var e=t.getBoundingClientRect(),i=M(e.width)/t.offsetWidth||1,n=M(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=q(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==x(e)||dt(a))&&(c=(n=e)!==k(n)&&S(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ct(n)),S(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ht(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function Ot(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach(function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}}),n.push(t)}return t.forEach(function(t){e.set(t.name,t)}),t.forEach(function(t){i.has(t.name)||s(t)}),n}var xt={placement:"bottom",modifiers:[],strategy:"absolute"};function kt(){for(var t=arguments.length,e=new Array(t),i=0;iIt.has(t)&&It.get(t).get(e)||null,remove(t,e){if(!It.has(t))return;const i=It.get(t);i.delete(e),0===i.size&&It.delete(t)}},Pt="transitionend",Mt=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,(t,e)=>`#${CSS.escape(e)}`)),t),jt=t=>null==t?`${t}`:Object.prototype.toString.call(t).match(/\s([a-z]+)/i)[1].toLowerCase(),Ft=t=>{t.dispatchEvent(new Event(Pt))},Ht=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Bt=t=>Ht(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(Mt(t)):null,Wt=t=>{if(!Ht(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},zt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),Rt=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?Rt(t.parentNode):null},qt=()=>{},Vt=t=>{t.offsetHeight},Yt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Kt=[],Qt=()=>"rtl"===document.documentElement.dir,Xt=t=>{var e;e=()=>{const e=Yt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Kt.length||document.addEventListener("DOMContentLoaded",()=>{for(const t of Kt)t()}),Kt.push(e)):e()},Ut=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,Gt=(t,e,i=!0)=>{if(!i)return void Ut(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(Pt,o),Ut(t))};e.addEventListener(Pt,o),setTimeout(()=>{s||Ft(e)},n)},Jt=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},Zt=/[^.]*(?=\..*)\.|.*/,te=/\..*/,ee=/::\d+$/,ie={};let ne=1;const se={mouseenter:"mouseover",mouseleave:"mouseout"},oe=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function re(t,e){return e&&`${e}::${ne++}`||t.uidEvent||ne++}function ae(t){const e=re(t);return t.uidEvent=e,ie[e]=ie[e]||{},ie[e]}function le(t,e,i=null){return Object.values(t).find(t=>t.callable===e&&t.delegationSelector===i)}function ce(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=fe(t);return oe.has(o)||(o=t),[n,s,o]}function he(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=ce(e,i,n);if(e in se){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=ae(t),c=l[a]||(l[a]={}),h=le(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=re(r,e.replace(Zt,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return me(s,{delegateTarget:r}),n.oneOff&&pe.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return me(n,{delegateTarget:t}),i.oneOff&&pe.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function de(t,e,i,n,s){const o=le(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function ue(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&de(t,e,i,r.callable,r.delegationSelector)}function fe(t){return t=t.replace(te,""),se[t]||t}const pe={on(t,e,i,n){he(t,e,i,n,!1)},one(t,e,i,n){he(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=ce(e,i,n),a=r!==e,l=ae(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))ue(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(ee,"");a&&!e.includes(s)||de(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;de(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Yt();let s=null,o=!0,r=!0,a=!1;e!==fe(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=me(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function me(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function ge(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function _e(t){return t.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`)}const be={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${_e(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${_e(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter(t=>t.startsWith("bs")&&!t.startsWith("bsConfig"));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=ge(t.dataset[n])}return e},getDataAttribute:(t,e)=>ge(t.getAttribute(`data-bs-${_e(e)}`))};class ve{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Ht(e)?be.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Ht(e)?be.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[i,n]of Object.entries(e)){const e=t[i],s=Ht(e)?"element":jt(e);if(!new RegExp(n).test(s))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${s}" but expected type "${n}".`)}}}class ye extends ve{constructor(t,e){super(),(t=Bt(t))&&(this._element=t,this._config=this._getConfig(e),Nt.set(this._element,this.constructor.DATA_KEY,this))}dispose(){Nt.remove(this._element,this.constructor.DATA_KEY),pe.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Gt(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return Nt.get(Bt(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const we=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map(t=>Mt(t)).join(","):null},Ee={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>`${t}:not([tabindex^="-"])`).join(",");return this.find(e,t).filter(t=>!zt(t)&&Wt(t))},getSelectorFromElement(t){const e=we(t);return e&&Ee.findOne(e)?e:null},getElementFromSelector(t){const e=we(t);return e?Ee.findOne(e):null},getMultipleElementsFromSelector(t){const e=we(t);return e?Ee.find(e):[]}},Ae=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;pe.on(document,i,`[data-bs-dismiss="${n}"]`,function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),zt(this))return;const s=Ee.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()})},Ce=".bs.alert",Te=`close${Ce}`,Oe=`closed${Ce}`;class xe extends ye{static get NAME(){return"alert"}close(){if(pe.trigger(this._element,Te).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),pe.trigger(this._element,Oe),this.dispose()}static jQueryInterface(t){return this.each(function(){const e=xe.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}Ae(xe,"close"),Xt(xe);const ke='[data-bs-toggle="button"]';class Le extends ye{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each(function(){const e=Le.getOrCreateInstance(this);"toggle"===t&&e[t]()})}}pe.on(document,"click.bs.button.data-api",ke,t=>{t.preventDefault();const e=t.target.closest(ke);Le.getOrCreateInstance(e).toggle()}),Xt(Le);const Se=".bs.swipe",De=`touchstart${Se}`,$e=`touchmove${Se}`,Ie=`touchend${Se}`,Ne=`pointerdown${Se}`,Pe=`pointerup${Se}`,Me={endCallback:null,leftCallback:null,rightCallback:null},je={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class Fe extends ve{constructor(t,e){super(),this._element=t,t&&Fe.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Me}static get DefaultType(){return je}static get NAME(){return"swipe"}dispose(){pe.off(this._element,Se)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Ut(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Ut(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(pe.on(this._element,Ne,t=>this._start(t)),pe.on(this._element,Pe,t=>this._end(t)),this._element.classList.add("pointer-event")):(pe.on(this._element,De,t=>this._start(t)),pe.on(this._element,$e,t=>this._move(t)),pe.on(this._element,Ie,t=>this._end(t)))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const He=".bs.carousel",Be=".data-api",We="ArrowLeft",ze="ArrowRight",Re="next",qe="prev",Ve="left",Ye="right",Ke=`slide${He}`,Qe=`slid${He}`,Xe=`keydown${He}`,Ue=`mouseenter${He}`,Ge=`mouseleave${He}`,Je=`dragstart${He}`,Ze=`load${He}${Be}`,ti=`click${He}${Be}`,ei="carousel",ii="active",ni=".active",si=".carousel-item",oi=ni+si,ri={[We]:Ye,[ze]:Ve},ai={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},li={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ci extends ye{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=Ee.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===ei&&this.cycle()}static get Default(){return ai}static get DefaultType(){return li}static get NAME(){return"carousel"}next(){this._slide(Re)}nextWhenVisible(){!document.hidden&&Wt(this._element)&&this.next()}prev(){this._slide(qe)}pause(){this._isSliding&&Ft(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?pe.one(this._element,Qe,()=>this.cycle()):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void pe.one(this._element,Qe,()=>this.to(t));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?Re:qe;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&pe.on(this._element,Xe,t=>this._keydown(t)),"hover"===this._config.pause&&(pe.on(this._element,Ue,()=>this.pause()),pe.on(this._element,Ge,()=>this._maybeEnableCycle())),this._config.touch&&Fe.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of Ee.find(".carousel-item img",this._element))pe.on(t,Je,t=>t.preventDefault());const t={leftCallback:()=>this._slide(this._directionToOrder(Ve)),rightCallback:()=>this._slide(this._directionToOrder(Ye)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),500+this._config.interval))}};this._swipeHelper=new Fe(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=ri[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=Ee.findOne(ni,this._indicatorsElement);e.classList.remove(ii),e.removeAttribute("aria-current");const i=Ee.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(ii),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===Re,s=e||Jt(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>pe.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(Ke).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),Vt(s),i.classList.add(l),s.classList.add(l),this._queueCallback(()=>{s.classList.remove(l,c),s.classList.add(ii),i.classList.remove(ii,c,l),this._isSliding=!1,r(Qe)},i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return Ee.findOne(oi,this._element)}_getItems(){return Ee.find(si,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Qt()?t===Ve?qe:Re:t===Ve?Re:qe}_orderToDirection(t){return Qt()?t===qe?Ve:Ye:t===qe?Ye:Ve}static jQueryInterface(t){return this.each(function(){const e=ci.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)})}}pe.on(document,ti,"[data-bs-slide], [data-bs-slide-to]",function(t){const e=Ee.getElementFromSelector(this);if(!e||!e.classList.contains(ei))return;t.preventDefault();const i=ci.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===be.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())}),pe.on(window,Ze,()=>{const t=Ee.find('[data-bs-ride="carousel"]');for(const e of t)ci.getOrCreateInstance(e)}),Xt(ci);const hi=".bs.collapse",di=`show${hi}`,ui=`shown${hi}`,fi=`hide${hi}`,pi=`hidden${hi}`,mi=`click${hi}.data-api`,gi="show",_i="collapse",bi="collapsing",vi=`:scope .${_i} .${_i}`,yi='[data-bs-toggle="collapse"]',wi={parent:null,toggle:!0},Ei={parent:"(null|element)",toggle:"boolean"};class Ai extends ye{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=Ee.find(yi);for(const t of i){const e=Ee.getSelectorFromElement(t),i=Ee.find(e).filter(t=>t===this._element);null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return wi}static get DefaultType(){return Ei}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter(t=>t!==this._element).map(t=>Ai.getOrCreateInstance(t,{toggle:!1}))),t.length&&t[0]._isTransitioning)return;if(pe.trigger(this._element,di).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(_i),this._element.classList.add(bi),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(bi),this._element.classList.add(_i,gi),this._element.style[e]="",pe.trigger(this._element,ui)},this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(pe.trigger(this._element,fi).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,Vt(this._element),this._element.classList.add(bi),this._element.classList.remove(_i,gi);for(const t of this._triggerArray){const e=Ee.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(bi),this._element.classList.add(_i),pe.trigger(this._element,pi)},this._element,!0)}_isShown(t=this._element){return t.classList.contains(gi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Bt(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(yi);for(const e of t){const t=Ee.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=Ee.find(vi,this._config.parent);return Ee.find(t,this._config.parent).filter(t=>!e.includes(t))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each(function(){const i=Ai.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}})}}pe.on(document,mi,yi,function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of Ee.getMultipleElementsFromSelector(this))Ai.getOrCreateInstance(t,{toggle:!1}).toggle()}),Xt(Ai);const Ci="dropdown",Ti=".bs.dropdown",Oi=".data-api",xi="ArrowUp",ki="ArrowDown",Li=`hide${Ti}`,Si=`hidden${Ti}`,Di=`show${Ti}`,$i=`shown${Ti}`,Ii=`click${Ti}${Oi}`,Ni=`keydown${Ti}${Oi}`,Pi=`keyup${Ti}${Oi}`,Mi="show",ji='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',Fi=`${ji}.${Mi}`,Hi=".dropdown-menu",Bi=Qt()?"top-end":"top-start",Wi=Qt()?"top-start":"top-end",zi=Qt()?"bottom-end":"bottom-start",Ri=Qt()?"bottom-start":"bottom-end",qi=Qt()?"left-start":"right-start",Vi=Qt()?"right-start":"left-start",Yi={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Ki={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Qi extends ye{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=Ee.next(this._element,Hi)[0]||Ee.prev(this._element,Hi)[0]||Ee.findOne(Hi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Yi}static get DefaultType(){return Ki}static get NAME(){return Ci}toggle(){return this._isShown()?this.hide():this.show()}show(){if(zt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!pe.trigger(this._element,Di,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))pe.on(t,"mouseover",qt);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Mi),this._element.classList.add(Mi),pe.trigger(this._element,$i,t)}}hide(){if(zt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!pe.trigger(this._element,Li,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))pe.off(t,"mouseover",qt);this._popper&&this._popper.destroy(),this._menu.classList.remove(Mi),this._element.classList.remove(Mi),this._element.setAttribute("aria-expanded","false"),be.removeDataAttribute(this._menu,"popper"),pe.trigger(this._element,Si,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Ht(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ci.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){let t=this._element;"parent"===this._config.reference?t=this._parent:Ht(this._config.reference)?t=Bt(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=Dt(t,this._menu,e)}_isShown(){return this._menu.classList.contains(Mi)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return qi;if(t.classList.contains("dropstart"))return Vi;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Wi:Bi:e?Ri:zi}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(be.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...Ut(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=Ee.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(t=>Wt(t));i.length&&Jt(i,e,t===ki,!i.includes(e)).focus()}static jQueryInterface(t){return this.each(function(){const e=Qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=Ee.find(Fi);for(const i of e){const e=Qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[xi,ki].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(ji)?this:Ee.prev(this,ji)[0]||Ee.next(this,ji)[0]||Ee.findOne(ji,t.delegateTarget.parentNode),o=Qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}pe.on(document,Ni,ji,Qi.dataApiKeydownHandler),pe.on(document,Ni,Hi,Qi.dataApiKeydownHandler),pe.on(document,Ii,Qi.clearMenus),pe.on(document,Pi,Qi.clearMenus),pe.on(document,Ii,ji,function(t){t.preventDefault(),Qi.getOrCreateInstance(this).toggle()}),Xt(Qi);const Xi="backdrop",Ui="show",Gi=`mousedown.bs.${Xi}`,Ji={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Zi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class tn extends ve{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Ji}static get DefaultType(){return Zi}static get NAME(){return Xi}show(t){if(!this._config.isVisible)return void Ut(t);this._append();const e=this._getElement();this._config.isAnimated&&Vt(e),e.classList.add(Ui),this._emulateAnimation(()=>{Ut(t)})}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ui),this._emulateAnimation(()=>{this.dispose(),Ut(t)})):Ut(t)}dispose(){this._isAppended&&(pe.off(this._element,Gi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Bt(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),pe.on(t,Gi,()=>{Ut(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(t){Gt(t,this._getElement(),this._config.isAnimated)}}const en=".bs.focustrap",nn=`focusin${en}`,sn=`keydown.tab${en}`,on="backward",rn={autofocus:!0,trapElement:null},an={autofocus:"boolean",trapElement:"element"};class ln extends ve{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return rn}static get DefaultType(){return an}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),pe.off(document,en),pe.on(document,nn,t=>this._handleFocusin(t)),pe.on(document,sn,t=>this._handleKeydown(t)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,pe.off(document,en))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=Ee.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===on?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?on:"forward")}}const cn=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",hn=".sticky-top",dn="padding-right",un="margin-right";class fn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,dn,e=>e+t),this._setElementAttributes(cn,dn,e=>e+t),this._setElementAttributes(hn,un,e=>e-t)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,dn),this._resetElementAttributes(cn,dn),this._resetElementAttributes(hn,un)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)})}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&be.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=be.getDataAttribute(t,e);null!==i?(be.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)})}_applyManipulationCallback(t,e){if(Ht(t))e(t);else for(const i of Ee.find(t,this._element))e(i)}}const pn=".bs.modal",mn=`hide${pn}`,gn=`hidePrevented${pn}`,_n=`hidden${pn}`,bn=`show${pn}`,vn=`shown${pn}`,yn=`resize${pn}`,wn=`click.dismiss${pn}`,En=`mousedown.dismiss${pn}`,An=`keydown.dismiss${pn}`,Cn=`click${pn}.data-api`,Tn="modal-open",On="show",xn="modal-static",kn={backdrop:!0,focus:!0,keyboard:!0},Ln={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Sn extends ye{constructor(t,e){super(t,e),this._dialog=Ee.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new fn,this._addEventListeners()}static get Default(){return kn}static get DefaultType(){return Ln}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||pe.trigger(this._element,bn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Tn),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t)))}hide(){this._isShown&&!this._isTransitioning&&(pe.trigger(this._element,mn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(On),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())))}dispose(){pe.off(window,pn),pe.off(this._dialog,pn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new tn({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new ln({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=Ee.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),Vt(this._element),this._element.classList.add(On),this._queueCallback(()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,pe.trigger(this._element,vn,{relatedTarget:t})},this._dialog,this._isAnimated())}_addEventListeners(){pe.on(this._element,An,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())}),pe.on(window,yn,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),pe.on(this._element,En,t=>{pe.one(this._element,wn,e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Tn),this._resetAdjustments(),this._scrollBar.reset(),pe.trigger(this._element,_n)})}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(pe.trigger(this._element,gn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(xn)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(xn),this._queueCallback(()=>{this._element.classList.remove(xn),this._queueCallback(()=>{this._element.style.overflowY=e},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Qt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Qt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each(function(){const i=Sn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}})}}pe.on(document,Cn,'[data-bs-toggle="modal"]',function(t){const e=Ee.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),pe.one(e,bn,t=>{t.defaultPrevented||pe.one(e,_n,()=>{Wt(this)&&this.focus()})});const i=Ee.findOne(".modal.show");i&&Sn.getInstance(i).hide(),Sn.getOrCreateInstance(e).toggle(this)}),Ae(Sn),Xt(Sn);const Dn=".bs.offcanvas",$n=".data-api",In=`load${Dn}${$n}`,Nn="show",Pn="showing",Mn="hiding",jn=".offcanvas.show",Fn=`show${Dn}`,Hn=`shown${Dn}`,Bn=`hide${Dn}`,Wn=`hidePrevented${Dn}`,zn=`hidden${Dn}`,Rn=`resize${Dn}`,qn=`click${Dn}${$n}`,Vn=`keydown.dismiss${Dn}`,Yn={backdrop:!0,keyboard:!0,scroll:!1},Kn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Qn extends ye{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Yn}static get DefaultType(){return Kn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||pe.trigger(this._element,Fn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new fn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Pn),this._queueCallback(()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Nn),this._element.classList.remove(Pn),pe.trigger(this._element,Hn,{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(pe.trigger(this._element,Bn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Mn),this._backdrop.hide(),this._queueCallback(()=>{this._element.classList.remove(Nn,Mn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new fn).reset(),pe.trigger(this._element,zn)},this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new tn({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():pe.trigger(this._element,Wn)}:null})}_initializeFocusTrap(){return new ln({trapElement:this._element})}_addEventListeners(){pe.on(this._element,Vn,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():pe.trigger(this._element,Wn))})}static jQueryInterface(t){return this.each(function(){const e=Qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}pe.on(document,qn,'[data-bs-toggle="offcanvas"]',function(t){const e=Ee.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),zt(this))return;pe.one(e,zn,()=>{Wt(this)&&this.focus()});const i=Ee.findOne(jn);i&&i!==e&&Qn.getInstance(i).hide(),Qn.getOrCreateInstance(e).toggle(this)}),pe.on(window,In,()=>{for(const t of Ee.find(jn))Qn.getOrCreateInstance(t).show()}),pe.on(window,Rn,()=>{for(const t of Ee.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Qn.getOrCreateInstance(t).hide()}),Ae(Qn),Xt(Qn);const Xn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Un=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Gn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Jn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Un.has(i)||Boolean(Gn.test(t.nodeValue)):e.filter(t=>t instanceof RegExp).some(t=>t.test(i))},Zn={allowList:Xn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},ts={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},es={entry:"(string|element|function|null)",selector:"(string|element)"};class is extends ve{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Zn}static get DefaultType(){return ts}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map(t=>this._resolvePossibleFunction(t)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},es)}_setContent(t,e,i){const n=Ee.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Ht(e)?this._putElementInTemplate(Bt(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Jn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return Ut(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const ns=new Set(["sanitize","allowList","sanitizeFn"]),ss="fade",os="show",rs=".tooltip-inner",as=".modal",ls="hide.bs.modal",cs="hover",hs="focus",ds={AUTO:"auto",TOP:"top",RIGHT:Qt()?"left":"right",BOTTOM:"bottom",LEFT:Qt()?"right":"left"},us={allowList:Xn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},fs={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class ps extends ye{constructor(t,e){super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return us}static get DefaultType(){return fs}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),pe.off(this._element.closest(as),ls,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=pe.trigger(this._element,this.constructor.eventName("show")),e=(Rt(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),pe.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(os),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))pe.on(t,"mouseover",qt);this._queueCallback(()=>{pe.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!pe.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(os),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))pe.off(t,"mouseover",qt);this._activeTrigger.click=!1,this._activeTrigger[hs]=!1,this._activeTrigger[cs]=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),pe.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ss,os),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ss),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new is({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[rs]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ss)}_isShown(){return this.tip&&this.tip.classList.contains(os)}_createPopper(t){const e=Ut(this._config.placement,[this,t,this._element]),i=ds[e.toUpperCase()];return Dt(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return Ut(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...Ut(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)pe.on(this._element,this.constructor.eventName("click"),this._config.selector,t=>{this._initializeOnDelegatedTarget(t).toggle()});else if("manual"!==e){const t=e===cs?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===cs?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");pe.on(this._element,t,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?hs:cs]=!0,e._enter()}),pe.on(this._element,i,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?hs:cs]=e._element.contains(t.relatedTarget),e._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},pe.on(this._element.closest(as),ls,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=be.getDataAttributes(this._element);for(const t of Object.keys(e))ns.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Bt(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each(function(){const e=ps.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}Xt(ps);const ms=".popover-header",gs=".popover-body",_s={...ps.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},bs={...ps.DefaultType,content:"(null|string|element|function)"};class vs extends ps{static get Default(){return _s}static get DefaultType(){return bs}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ms]:this._getTitle(),[gs]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each(function(){const e=vs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}Xt(vs);const ys=".bs.scrollspy",ws=`activate${ys}`,Es=`click${ys}`,As=`load${ys}.data-api`,Cs="active",Ts="[href]",Os=".nav-link",xs=`${Os}, .nav-item > ${Os}, .list-group-item`,ks={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Ls={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ss extends ye{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ks}static get DefaultType(){return Ls}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Bt(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map(t=>Number.parseFloat(t))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(pe.off(this._config.target,Es),pe.on(this._config.target,Es,Ts,t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}}))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(t=>this._observerCallback(t),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=Ee.find(Ts,this._config.target);for(const e of t){if(!e.hash||zt(e))continue;const t=Ee.findOne(decodeURI(e.hash),this._element);Wt(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Cs),this._activateParents(t),pe.trigger(this._element,ws,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))Ee.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Cs);else for(const e of Ee.parents(t,".nav, .list-group"))for(const t of Ee.prev(e,xs))t.classList.add(Cs)}_clearActiveClass(t){t.classList.remove(Cs);const e=Ee.find(`${Ts}.${Cs}`,t);for(const t of e)t.classList.remove(Cs)}static jQueryInterface(t){return this.each(function(){const e=Ss.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}pe.on(window,As,()=>{for(const t of Ee.find('[data-bs-spy="scroll"]'))Ss.getOrCreateInstance(t)}),Xt(Ss);const Ds=".bs.tab",$s=`hide${Ds}`,Is=`hidden${Ds}`,Ns=`show${Ds}`,Ps=`shown${Ds}`,Ms=`click${Ds}`,js=`keydown${Ds}`,Fs=`load${Ds}`,Hs="ArrowLeft",Bs="ArrowRight",Ws="ArrowUp",zs="ArrowDown",Rs="Home",qs="End",Vs="active",Ys="fade",Ks="show",Qs=".dropdown-toggle",Xs=`:not(${Qs})`,Us='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Gs=`.nav-link${Xs}, .list-group-item${Xs}, [role="tab"]${Xs}, ${Us}`,Js=`.${Vs}[data-bs-toggle="tab"], .${Vs}[data-bs-toggle="pill"], .${Vs}[data-bs-toggle="list"]`;class Zs extends ye{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),pe.on(this._element,js,t=>this._keydown(t)))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?pe.trigger(e,$s,{relatedTarget:t}):null;pe.trigger(t,Ns,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Vs),this._activate(Ee.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),pe.trigger(t,Ps,{relatedTarget:e})):t.classList.add(Ks)},t,t.classList.contains(Ys)))}_deactivate(t,e){t&&(t.classList.remove(Vs),t.blur(),this._deactivate(Ee.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),pe.trigger(t,Is,{relatedTarget:e})):t.classList.remove(Ks)},t,t.classList.contains(Ys)))}_keydown(t){if(![Hs,Bs,Ws,zs,Rs,qs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter(t=>!zt(t));let i;if([Rs,qs].includes(t.key))i=e[t.key===Rs?0:e.length-1];else{const n=[Bs,zs].includes(t.key);i=Jt(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Zs.getOrCreateInstance(i).show())}_getChildren(){return Ee.find(Gs,this._parent)}_getActiveElem(){return this._getChildren().find(t=>this._elemIsActive(t))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=Ee.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=Ee.findOne(t,i);s&&s.classList.toggle(n,e)};n(Qs,Vs),n(".dropdown-menu",Ks),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Vs)}_getInnerElement(t){return t.matches(Gs)?t:Ee.findOne(Gs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each(function(){const e=Zs.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}pe.on(document,Ms,Us,function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),zt(this)||Zs.getOrCreateInstance(this).show()}),pe.on(window,Fs,()=>{for(const t of Ee.find(Js))Zs.getOrCreateInstance(t)}),Xt(Zs);const to=".bs.toast",eo=`mouseover${to}`,io=`mouseout${to}`,no=`focusin${to}`,so=`focusout${to}`,oo=`hide${to}`,ro=`hidden${to}`,ao=`show${to}`,lo=`shown${to}`,co="hide",ho="show",uo="showing",fo={animation:"boolean",autohide:"boolean",delay:"number"},po={animation:!0,autohide:!0,delay:5e3};class mo extends ye{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return po}static get DefaultType(){return fo}static get NAME(){return"toast"}show(){pe.trigger(this._element,ao).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(co),Vt(this._element),this._element.classList.add(ho,uo),this._queueCallback(()=>{this._element.classList.remove(uo),pe.trigger(this._element,lo),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(pe.trigger(this._element,oo).defaultPrevented||(this._element.classList.add(uo),this._queueCallback(()=>{this._element.classList.add(co),this._element.classList.remove(uo,ho),pe.trigger(this._element,ro)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(ho),super.dispose()}isShown(){return this._element.classList.contains(ho)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){pe.on(this._element,eo,t=>this._onInteraction(t,!0)),pe.on(this._element,io,t=>this._onInteraction(t,!1)),pe.on(this._element,no,t=>this._onInteraction(t,!0)),pe.on(this._element,so,t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each(function(){const e=mo.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}})}}function go(t){"loading"!=document.readyState?t():document.addEventListener("DOMContentLoaded",t)}Ae(mo),Xt(mo),go(function(){[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map(function(t){return new ps(t,{delay:{show:500,hide:100}})})}),go(function(){const t=document.querySelector(".theme-switch-container");if(!t)return;const e=ps.getInstance(t);e&&(t.addEventListener("show.bs.dropdown",()=>{e.hide(),e.disable()}),t.addEventListener("hide.bs.dropdown",()=>{e.enable()}))}),go(function(){var t=document.getElementById("pst-back-to-top");t&&t.addEventListener("click",function(){document.body.scrollTop=0,document.documentElement.scrollTop=0})}),go(function(){var t=document.getElementById("pst-back-to-top");if(t){var e=document.getElementsByClassName("bd-header")[0].getBoundingClientRect();window.addEventListener("scroll",function(){this.oldScroll>this.scrollY&&this.scrollY>e.bottom?t.style.display="block":t.style.display="none",this.oldScroll=this.scrollY})}}),window.bootstrap=i})(); +//# sourceMappingURL=bootstrap.js.map \ No newline at end of file diff --git a/docs/_static/scripts/bootstrap.js.LICENSE.txt b/docs/_static/scripts/bootstrap.js.LICENSE.txt new file mode 100644 index 0000000..28755c2 --- /dev/null +++ b/docs/_static/scripts/bootstrap.js.LICENSE.txt @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/docs/_static/scripts/bootstrap.js.map b/docs/_static/scripts/bootstrap.js.map new file mode 100644 index 0000000..a8ea336 --- /dev/null +++ b/docs/_static/scripts/bootstrap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/bootstrap.js","mappings":"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","sources":["webpack://pydata_sphinx_theme/webpack/bootstrap","webpack://pydata_sphinx_theme/webpack/runtime/define property getters","webpack://pydata_sphinx_theme/webpack/runtime/hasOwnProperty shorthand","webpack://pydata_sphinx_theme/webpack/runtime/make namespace object","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/enums.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/applyStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getBasePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/math.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/userAgent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/contains.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/within.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/expandToHashMap.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/arrow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getVariation.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/computeStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/eventListeners.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/rectToClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/detectOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/flip.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/hide.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/offset.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getAltAxis.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/orderModifiers.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/createPopper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/debounce.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergeByName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper-lite.js","webpack://pydata_sphinx_theme/./node_modules/bootstrap/dist/js/bootstrap.esm.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/mixin.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/bootstrap.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.3.3 (https://getbootstrap.com/)\n * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n const instanceMap = elementMap.get(element);\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n instanceMap.set(key, instance);\n },\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n return null;\n },\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key);\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend';\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n }\n return selector;\n};\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n return prefix;\n};\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n }\n\n // Get transition-duration of the element\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n return typeof object.nodeType !== 'undefined';\n};\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object));\n }\n return null;\n};\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n if (!closedDetails) {\n return elementIsVisible;\n }\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n if (summary === null) {\n return false;\n }\n }\n return elementIsVisible;\n};\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n if (element.classList.contains('disabled')) {\n return true;\n }\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n if (element instanceof ShadowRoot) {\n return element;\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null;\n }\n return findShadowRoot(element.parentNode);\n};\nconst noop = () => {};\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n return null;\n};\nconst DOMContentLoadedCallbacks = [];\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\nconst isRTL = () => document.documentElement.dir === 'rtl';\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n};\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement);\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n index += shouldGetNext ? 1 : -1;\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n return fn.apply(element, [event]);\n };\n}\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n hydrateObj(event, {\n delegateTarget: target\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n return fn.apply(target, [event]);\n }\n }\n };\n}\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string';\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n return [isDelegated, callable, typeEvent];\n}\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n callable = wrapFunction(callable);\n }\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n if (!fn) {\n return;\n }\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n const evt = hydrateObj(new Event(event, {\n bubbles,\n cancelable: true\n }), args);\n if (defaultPrevented) {\n evt.preventDefault();\n }\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n return evt;\n }\n};\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value;\n }\n });\n }\n }\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n if (value === 'false') {\n return false;\n }\n if (value === Number(value).toString()) {\n return Number(value);\n }\n if (value === '' || value === 'null') {\n return null;\n }\n if (typeof value !== 'string') {\n return value;\n }\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n return attributes;\n },\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n static get DefaultType() {\n return {};\n }\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n return config;\n }\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.3';\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n if (!element) {\n return;\n }\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n static get VERSION() {\n return VERSION;\n }\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href');\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n }\n return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null;\n};\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n return parents;\n },\n prev(element, selector) {\n let previous = element.previousElementSibling;\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n previous = previous.previousElementSibling;\n }\n return [];\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n next = next.nextElementSibling;\n }\n return [];\n },\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n },\n getSelectorFromElement(element) {\n const selector = getSelector(element);\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null;\n }\n return null;\n },\n getElementFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.findOne(selector) : null;\n },\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.find(selector) : [];\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target);\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n if (closeEvent.defaultPrevented) {\n return;\n }\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n }\n\n // Private\n _destroyElement() {\n this._element.remove();\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close');\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n if (!element || !Swipe.isSupported()) {\n return;\n }\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n this._initEvents();\n }\n\n // Getters\n static get Default() {\n return Default$c;\n }\n static get DefaultType() {\n return DefaultType$c;\n }\n static get NAME() {\n return NAME$d;\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n this._handleSwipe();\n execute(this._config.endCallback);\n }\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n if (!direction) {\n return;\n }\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n this._addEventListeners();\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$b;\n }\n static get DefaultType() {\n return DefaultType$b;\n }\n static get NAME() {\n return NAME$c;\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT);\n }\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n prev() {\n this._slide(ORDER_PREV);\n }\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n this._clearInterval();\n }\n cycle() {\n this._clearInterval();\n this._updateInterval();\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n this.cycle();\n }\n to(index) {\n const items = this._getItems();\n if (index > items.length - 1 || index < 0) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n const activeIndex = this._getItemIndex(this._getActive());\n if (activeIndex === index) {\n return;\n }\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n this._slide(order, items[index]);\n }\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause();\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n const direction = KEY_TO_DIRECTION[event.key];\n if (direction) {\n event.preventDefault();\n this._slide(this._directionToOrder(direction));\n }\n }\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n if (!element) {\n return;\n }\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n const activeElement = this._getActive();\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n if (nextElement === activeElement) {\n return;\n }\n const nextElementIndex = this._getItemIndex(nextElement);\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n const slideEvent = triggerEvent(EVENT_SLIDE);\n if (slideEvent.defaultPrevented) {\n return;\n }\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return;\n }\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n this._setActiveIndicatorElement(nextElementIndex);\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n if (isCycling) {\n this.cycle();\n }\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n if (slideIndex) {\n carousel.to(slideIndex);\n carousel._maybeEnableCycle();\n return;\n }\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n carousel._maybeEnableCycle();\n return;\n }\n carousel.prev();\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n this._initializeChildren();\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n if (this._config.toggle) {\n this.toggle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$a;\n }\n static get DefaultType() {\n return DefaultType$a;\n }\n static get NAME() {\n return NAME$b;\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n let activeChildren = [];\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n const dimension = this._getDimension();\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.style[dimension] = 0;\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n this._queueCallback(complete, this._element, true);\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n const dimension = this._getDimension();\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger);\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n this._element.style[dimension] = '';\n this._queueCallback(complete, this._element, true);\n }\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n config.parent = getElement(config.parent);\n return config;\n }\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element);\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {};\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n }\n\n // Getters\n static get Default() {\n return Default$9;\n }\n static get DefaultType() {\n return DefaultType$9;\n }\n static get NAME() {\n return NAME$a;\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n if (showEvent.defaultPrevented) {\n return;\n }\n this._createPopper();\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n this._element.focus();\n this._element.setAttribute('aria-expanded', true);\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n this._element.classList.add(CLASS_NAME_SHOW$6);\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n this._completeHide(relatedTarget);\n }\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n super.dispose();\n }\n update() {\n this._inNavbar = this._detectNavbar();\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n if (this._popper) {\n this._popper.destroy();\n }\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n this._element.setAttribute('aria-expanded', 'false');\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n _getConfig(config) {\n config = super._getConfig(config);\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n return config;\n }\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n let referenceElement = this._element;\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n const popperConfig = this._getPopperConfig();\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n _getPlacement() {\n const parentDropdown = this._parent;\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n };\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n if (!items.length) {\n return;\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n if (!context || context._config.autoClose === false) {\n continue;\n }\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n const relatedTarget = {\n relatedTarget: context._element\n };\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n context._completeHide(relatedTarget);\n }\n }\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n if (isInput && !isEscapeEvent) {\n return;\n }\n event.preventDefault();\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n instance._selectMenuItem(event);\n return;\n }\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n};\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n }\n\n // Getters\n static get Default() {\n return Default$8;\n }\n static get DefaultType() {\n return DefaultType$8;\n }\n static get NAME() {\n return NAME$9;\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._append();\n const element = this._getElement();\n if (this._config.isAnimated) {\n reflow(element);\n }\n element.classList.add(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n dispose() {\n if (!this._isAppended) {\n return;\n }\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n this._element.remove();\n this._isAppended = false;\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n this._element = backdrop;\n }\n return this._element;\n }\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n _append() {\n if (this._isAppended) {\n return;\n }\n const element = this._getElement();\n this._config.rootElement.append(element);\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n};\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n }\n\n // Getters\n static get Default() {\n return Default$7;\n }\n static get DefaultType() {\n return DefaultType$7;\n }\n static get NAME() {\n return NAME$8;\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return;\n }\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n deactivate() {\n if (!this._isActive) {\n return;\n }\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n }\n\n // Private\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n const elements = SelectorEngine.focusableChildren(trapElement);\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n hide() {\n const width = this.getWidth();\n this._disableOverFlow();\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n isOverflowing() {\n return this.getWidth() > 0;\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n this._element.style.overflow = 'hidden';\n }\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n this._saveInitialAttribute(element, styleProperty);\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty);\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$6;\n }\n static get DefaultType() {\n return DefaultType$6;\n }\n static get NAME() {\n return NAME$7;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._isTransitioning = true;\n this._scrollBar.hide();\n document.body.classList.add(CLASS_NAME_OPEN);\n this._adjustDialog();\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._isShown = false;\n this._isTransitioning = true;\n this._focustrap.deactivate();\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n dispose() {\n EventHandler.off(window, EVENT_KEY$4);\n EventHandler.off(this._dialog, EVENT_KEY$4);\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n handleUpdate() {\n this._adjustDialog();\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n this._element.style.display = 'block';\n this._element.removeAttribute('aria-hidden');\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_SHOW$4);\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n return;\n }\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n _hideModal() {\n this._element.style.display = 'none';\n this._element.setAttribute('aria-hidden', true);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n this._isTransitioning = false;\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n this._resetAdjustments();\n this._scrollBar.reset();\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n if (hideEvent.defaultPrevented) {\n return;\n }\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY;\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n this._element.classList.add(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n this._element.focus();\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const scrollbarWidth = this._scrollBar.getWidth();\n const isBodyOverflowing = scrollbarWidth > 0;\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](relatedTarget);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n });\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$5;\n }\n static get DefaultType() {\n return DefaultType$5;\n }\n static get NAME() {\n return NAME$6;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._backdrop.show();\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n this._element.classList.add(CLASS_NAME_SHOW$3);\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n this._queueCallback(completeCallBack, this._element, true);\n }\n hide() {\n if (!this._isShown) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._focustrap.deactivate();\n this._element.blur();\n this._isShown = false;\n this._element.classList.add(CLASS_NAME_HIDING);\n this._backdrop.hide();\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n this._queueCallback(completeCallback, this._element, true);\n }\n dispose() {\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n this.hide();\n };\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n });\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n });\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n dd: [],\n div: [],\n dl: [],\n dt: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\n// js-docs-end allow-list\n\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n }\n return true;\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n }\n\n // Getters\n static get Default() {\n return Default$4;\n }\n static get DefaultType() {\n return DefaultType$4;\n }\n static get NAME() {\n return NAME$5;\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n hasContent() {\n return this.getContent().length > 0;\n }\n changeContent(content) {\n this._checkContent(content);\n this._config.content = {\n ...this._config.content,\n ...content\n };\n return this;\n }\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n const template = templateWrapper.children[0];\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n return template;\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n this._checkContent(config.content);\n }\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n if (!templateElement) {\n return;\n }\n content = this._resolvePossibleFunction(content);\n if (!content) {\n templateElement.remove();\n return;\n }\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n return;\n }\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n templateElement.textContent = content;\n }\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this]);\n }\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n templateElement.textContent = element.textContent;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n super(element, config);\n\n // Private\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null;\n\n // Protected\n this.tip = null;\n this._setListeners();\n if (!this._config.selector) {\n this._fixTitle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$3;\n }\n static get DefaultType() {\n return DefaultType$3;\n }\n static get NAME() {\n return NAME$4;\n }\n\n // Public\n enable() {\n this._isEnabled = true;\n }\n disable() {\n this._isEnabled = false;\n }\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n this._activeTrigger.click = !this._activeTrigger.click;\n if (this._isShown()) {\n this._leave();\n return;\n }\n this._enter();\n }\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n this._disposePopper();\n super.dispose();\n }\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper();\n const tip = this._getTipElement();\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n const {\n container\n } = this._config;\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n if (this._isHovered === false) {\n this._leave();\n }\n this._isHovered = false;\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n hide() {\n if (!this._isShown()) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n if (hideEvent.defaultPrevented) {\n return;\n }\n const tip = this._getTipElement();\n tip.classList.remove(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n if (!this._isHovered) {\n this._disposePopper();\n }\n this._element.removeAttribute('aria-describedby');\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n update() {\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n return this.tip;\n }\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml();\n\n // TODO: remove this check in v6\n if (!tip) {\n return null;\n }\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n return tip;\n }\n setContent(content) {\n this._newContent = content;\n if (this._isShown()) {\n this._disposePopper();\n this.show();\n }\n }\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n return this._templateFactory;\n }\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element]);\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element]);\n }\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n context._leave();\n });\n }\n }\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n _fixTitle() {\n const title = this._element.getAttribute('title');\n if (!title) {\n return;\n }\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title');\n }\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n this._isHovered = true;\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n this._isHovered = false;\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n return config;\n }\n _getDelegateConfig() {\n const config = {};\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value;\n }\n }\n config.selector = false;\n config.trigger = 'manual';\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config;\n }\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n this._popper = null;\n }\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n static get DefaultType() {\n return DefaultType$2;\n }\n static get NAME() {\n return NAME$3;\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent();\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n }\n\n // Getters\n static get Default() {\n return Default$1;\n }\n static get DefaultType() {\n return DefaultType$1;\n }\n static get NAME() {\n return NAME$2;\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables();\n this._maybeEnableSmoothScroll();\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n dispose() {\n this._observer.disconnect();\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body;\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n return config;\n }\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height;\n }\n });\n }\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n this._process(targetElement(entry));\n };\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n this._clearActiveClass(targetElement(entry));\n continue;\n }\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry);\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return;\n }\n continue;\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor);\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n this._clearActiveClass(this._config.target);\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n this._activateParents(target);\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both
+ + + + + + + + + + + + + + + - - - -
- -
-
-
-
+ - + + + + + + +
+ +
+ + + + + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/api/modules.html b/docs/api/modules.html new file mode 100644 index 0000000..6e24339 --- /dev/null +++ b/docs/api/modules.html @@ -0,0 +1,682 @@ + + + + + + + + + + + pycopm — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + + + + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.core.html b/docs/api/pycopm.core.html new file mode 100644 index 0000000..1a475e4 --- /dev/null +++ b/docs/api/pycopm.core.html @@ -0,0 +1,704 @@ + + + + + + + + + + + pycopm.core package — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + + + + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.core.pycopm.html b/docs/api/pycopm.core.pycopm.html new file mode 100644 index 0000000..13b7f3b --- /dev/null +++ b/docs/api/pycopm.core.pycopm.html @@ -0,0 +1,902 @@ + + + + + + + + + + + pycopm.core.pycopm module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.core.pycopm module#

+

Command-line entry point and top-level workflow coordination for pycopm.

+

pycopm supports two input workflows:

+
    +
  • OPM .DATA decks can be coarsened, refined, transformed, or reduced to a +submodel.

  • +
  • TOML configurations generate coarsened Norne or Drogon cases and can +optionally run OPM Flow or ERT studies.

  • +
+

This module parses and validates command-line arguments, selects the appropriate +workflow, and coordinates its major processing steps. The numerical and +file-generation details are implemented in the utility modules.

+
+
+main(argv=None)[source]#
+

Run the deck-based or TOML-based pycopm workflow.

+

OPM .DATA decks can be coarsened, refined, transformed, or reduced to +a submodel. TOML configurations generate coarsened Norne or Drogon cases +and can optionally run OPM Flow or ERT studies.

+
+
Parameters:
+
+
argv

Command-line arguments. If omitted, arguments are read from +sys.argv.

+
+
+
+
Other Parameters:
+
+
-i, --input_deck_path

Input .DATA deck or TOML configuration file.

+
+
-o, --output_directory

Directory for generated decks, include files, and simulation results.

+
+
-f, --flow_command

Command or path used to run OPM Flow.

+
+
-m, --execution_mode

Deck-processing stages to run: prep, deck, dry, +prep_deck, deck_dry, or all.

+
+
-v, --vicinity_specification

Submodel selection based on region values, an xy polygon, or a +well-centred box, diamond, or diamondxy neighbourhood.

+
+
-c, --coarsening

Uniform coarsening factors in the x, y, and z directions.

+
+
-x, --x_coarsening

Cell-specific coarsening specification along the x axis.

+
+
-y, --y_coarsening

Cell-specific coarsening specification along the y axis.

+
+
-z, --z_coarsening

Cell-specific coarsening specification along the z axis.

+
+
-g, --refinement

Uniform numbers of additional cells along the x, y, and z axes.

+
+
-rx, --x_refinement

Number of additional cells for each original x interval.

+
+
-ry, --y_refinement

Number of additional cells for each original y interval.

+
+
-rz, --z_refinement

Number of additional cells for each original z interval.

+
+
-a, --active_cell_methods

Aggregation method for active-cell values: min, max, or +mode.

+
+
-n, --discrete_aggregation_method

Aggregation method for discrete properties: min, max, or +mode.

+
+
-s, --continuous_aggregation_method

Aggregation method for continuous properties: min, max, +mean, or pore-volume-weighted mean (pvmean). If omitted, +property-specific physical aggregation is used.

+
+
-p, --pore_volume_correction

Pore-volume correction method. The available values are 0 through +4; supported methods depend on the selected workflow.

+
+
-q, --correct_fluid_in_place

Set to 1 to adjust pore volume to match the initial oil and gas in +place of the input model.

+
+
-t, --transmissibility_coarsening_method

Transmissibility coarsening method: 0, 1, or 2.

+
+
-r, --completion_removal_level

Level of COMPDAT data removed after coarsening: 0, 1, or +2.

+
+
-j, --jump_thresholds

Positive depth-jump thresholds used to prevent unwanted connections +between cells grouped during coarsening.

+
+
-w, --output_deck_name

Name of the generated OPM deck.

+
+
-l, --include_prefix

Prefix added to generated include filenames.

+
+
-e, --deck_encoding

Character encoding used to read the input deck: ISO-8859-1 or +utf8.

+
+
-ijk, --requested_ijk

One-based input-grid i,j,k indices to map to the modified grid.

+
+
-d, --grid_transformation

Coordinate transformation: translate [x,y,z], scale [x,y,z], +or rotatexy, rotatexz, or rotateyz followed by an angle +in degrees.

+
+
-explicit, --write_explicit_solution

Set to 1 to write initial solution properties explicitly instead +of retaining EQUIL initialization.

+
+
-dual, --dual_porosity_criterion

Static-property criterion used to separate matrix and fracture or +non-net cells during coarsening.

+
+
-precision, --significant_digits

Number of significant digits used when writing floating-point values. +Set to 0 to preserve machine precision.

+
+
+
+
Parameters:
+

argv (list[str] | None)

+
+
Return type:
+

None

+
+
+
+ +
+
+_parse_arguments(argv=None)[source]#
+

Parse supported command-line arguments.

+

Unknown arguments are left unprocessed for compatibility with external +launchers.

+
+
Parameters:
+
+
argv

Command-line arguments. If omitted, arguments are read from sys.argv.

+
+
+
+
Returns:
+
+
dict[str, str]

Arguments keyed by their destination names.

+
+
+
+
Parameters:
+

argv (list[str] | None)

+
+
Return type:
+

Namespace

+
+
+
+ +
+
+_check_cmdargs(cmdargs)[source]#
+

Validate command-line arguments and incompatible operations.

+

The checks cover input type, Flow availability, coarsening and refinement +syntax, aggregation methods, vicinity selections, transformations, and +options restricted to particular workflows.

+
+
Parameters:
+
+
cmdargs

Parsed arguments returned by _parse_arguments().

+
+
+
+
Raises:
+
+
SystemExit

If an argument is invalid or an incompatible combination is requested.

+
+
+
+
Parameters:
+

cmdargs (Namespace)

+
+
Return type:
+

None

+
+
+
+ +
+
+_check_flow(flow_cmdargs, flow_toml, input_file)[source]#
+

Select an available OPM Flow command for a TOML workflow.

+
+
Parameters:
+
+
flow_cmdargs

Flow command supplied through the command line.

+
+
flow_toml

Flow command read from the TOML configuration.

+
+
input_file

TOML filename used in validation messages.

+
+
+
+
Returns:
+
+
str

The selected Flow command.

+
+
+
+
Raises:
+
+
SystemExit

If neither command identifies a working Flow executable.

+
+
+
+
Parameters:
+
    +
  • flow_cmdargs (str)

  • +
  • flow_toml (str)

  • +
  • input_file (str)

  • +
+
+
Return type:
+

str

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/pycopm.html b/docs/api/pycopm.html similarity index 53% rename from docs/pycopm.html rename to docs/api/pycopm.html index 496177f..3b34192 100644 --- a/docs/pycopm.html +++ b/docs/api/pycopm.html @@ -1,100 +1,566 @@ - - - - - - - pycopm package — pycopm documentation - - - - - - - - - - - - - + + + + + + + + + pycopm package — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + -
- - -
- -
-
-
-
+ + +
+ + + + + + + +
+ +
+ + + + + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/api/pycopm.utils.coarsening.html b/docs/api/pycopm.utils.coarsening.html new file mode 100644 index 0000000..ce9be0f --- /dev/null +++ b/docs/api/pycopm.utils.coarsening.html @@ -0,0 +1,1481 @@ + + + + + + + + + + + pycopm.utils.coarsening module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.coarsening module#

+

Coarsen corner-point grids and aggregate reservoir properties.

+

The module supports deck-based coarsening and the TOML workflows used to +generate reduced Norne and Drogon models.

+
+
+class CoarseningMaps(x, y, z, cell_groups, coarsened_axes, matrix_mask, vertical_transfer_enabled, reference_to_coarse=<factory>, nnc_text='NNC\n', coarse_tranx=<factory>, coarse_trany=<factory>, dual_tranx=<factory>, dual_trany=<factory>, dual_defaults=<factory>)[source]#
+

Bases: object

+

Store mappings and intermediate values used during coarsening.

+
+
Parameters:
+
    +
  • x (NDArray)

  • +
  • y (NDArray)

  • +
  • z (NDArray)

  • +
  • cell_groups (NDArray)

  • +
  • coarsened_axes (str)

  • +
  • matrix_mask (NDArray)

  • +
  • vertical_transfer_enabled (bool)

  • +
  • reference_to_coarse (list[int])

  • +
  • nnc_text (str)

  • +
  • coarse_tranx (NDArray)

  • +
  • coarse_trany (NDArray)

  • +
  • dual_tranx (NDArray)

  • +
  • dual_trany (NDArray)

  • +
  • dual_defaults (dict[str, float])

  • +
+
+
+
+
+x: NDArray#
+

Axis array marking boundaries removed by coarsening in the x direction. +Values greater than one identify intervals merged with the preceding +interval.

+
+ +
+
+y: NDArray#
+

Axis array marking boundaries removed by coarsening in the y direction. +Values greater than one identify intervals merged with the preceding +interval.

+
+ +
+
+z: NDArray#
+

Axis array marking boundaries removed by coarsening in the z direction. +Values greater than one identify intervals merged with the preceding +interval.

+
+ +
+
+cell_groups: NDArray#
+

One-based coarse-cell identifier for each original cell, flattened in +(z, y, x) order.

+
+ +
+
+coarsened_axes: str#
+

Concatenated names of the coarsened axes, for example "xz".

+
+ +
+
+matrix_mask: NDArray#
+

Per-cell mask separating matrix cells (one) from fracture or non-net +cells (zero) in dual-porosity models.

+
+ +
+
+vertical_transfer_enabled: bool#
+

Whether vertical matrix-fracture transfer connections are retained.

+
+ +
+
+reference_to_coarse: list[int]#
+

Coarse-cell identifier for each reference-grid cell, populated while +properties are coarsened.

+
+ +
+
+nnc_text: str#
+

NNC include-file content accumulated while mapping non-neighbouring and +matrix-fracture connections.

+
+ +
+
+coarse_tranx: NDArray#
+

Horizontal x-direction transmissibilities for the matrix or +single-porosity coarse grid.

+
+ +
+
+coarse_trany: NDArray#
+

Horizontal y-direction transmissibilities for the matrix or +single-porosity coarse grid.

+
+ +
+
+dual_tranx: NDArray#
+

Horizontal x-direction transmissibilities for the fracture continuum of +a dual-porosity grid.

+
+ +
+
+dual_trany: NDArray#
+

Horizontal y-direction transmissibilities for the fracture continuum of +a dual-porosity grid.

+
+ +
+
+dual_defaults: dict[str, float]#
+

Default property values inserted into separator rows of the extended +dual-porosity grid.

+
+ +
+ +
+
+create_coarsening_maps(dck, cmdargs)[source]#
+

Create axis mappings and assign original cells to coarse cells.

+
+
Parameters:
+
+
dck

Deck configuration whose output dimensions are updated.

+
+
cmdargs

Command arguments containing coarsening, x_coarsening, +y_coarsening, and z_coarsening.

+
+
+
+
Returns:
+
+
CoarseningMaps

Axis mappings, cell groups, and dual-porosity masks.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • cmdargs (Namespace)

  • +
+
+
Return type:
+

CoarseningMaps

+
+
+
+ +
+
+_grouped_sum(values, groups, size=None)[source]#
+

Return the sum of values for each one-based group.

+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • size (int | None)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_grouped_count(values, groups, size=None)[source]#
+

Return the number of non-NaN values for each one-based group.

+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • size (int | None)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_grouped_min(values, groups, size=None)[source]#
+

Return the minimum value for each one-based group, ignoring NaNs.

+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • size (int | None)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_grouped_max(values, groups, size=None)[source]#
+

Return the maximum value for each one-based group, ignoring NaNs.

+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • size (int | None)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_grouped_mean(values, groups, size=None)[source]#
+

Return the mean value for each one-based group, ignoring NaNs.

+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • size (int | None)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_grouped_first(values, groups, size=None)[source]#
+

Return the first non-NaN value for each one-based group.

+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • size (int | None)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_grouped_last(values, groups, size=None)[source]#
+

Return the last non-NaN value for each one-based group.

+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • size (int | None)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_grouped_mode(values, group_codes, number_groups)[source]#
+

Return the smallest mode for each zero-based group, ignoring NaNs.

+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • group_codes (NDArray)

  • +
  • number_groups (int)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+coarsen_properties(dck, coarsening, modified_deck, wellcind)[source]#
+

Aggregate reservoir properties onto the coarsened grid.

+

Continuous properties use their configured or property-specific aggregation; +discrete properties use min, max, or mode. The function writes +property include files and updates output pore volume and active cells.

+
+
Parameters:
+
+
dck

Deck configuration and source INIT or restart properties.

+
+
coarsening

Cell groups and masks created by create_coarsening_maps().

+
+
modified_deck

Deck lines updated with generated property includes.

+
+
wellcind

Coarse-cell indices containing well completions.

+
+
+
+
Returns:
+
+
cluster_minimum, cluster_maximum, removal_mask, generated_files

Activity summaries, the mask used to remove depth-jump cells, +and the generated include file names.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • coarsening (CoarseningMaps)

  • +
  • modified_deck (list[str])

  • +
  • wellcind (list[int])

  • +
+
+
Return type:
+

tuple[NDArray, NDArray, NDArray, list[str]]

+
+
+
+ +
+
+_interleave_dual_property(property_values, dual_values, nx, nz, default_value=0)[source]#
+

Interleave property and dual-property layers with separator rows.

+
+
Parameters:
+
    +
  • property_values (NDArray)

  • +
  • dual_values (NDArray)

  • +
  • nx (int)

  • +
  • nz (int)

  • +
  • default_value (float)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_find_include_statement(modified_deck, include_line)[source]#
+

Return the list interval containing an INCLUDE statement.

+
+
Parameters:
+
    +
  • modified_deck (list[str])

  • +
  • include_line (str)

  • +
+
+
Return type:
+

tuple[int, int]

+
+
+
+ +
+
+_compact_permeability_properties(dck, permx, permy, permz, modified_deck)[source]#
+

Use COPY and MULTIPLY if PERMY and PERMZ can be generated from PERMX.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • permx (NDArray)

  • +
  • permy (NDArray)

  • +
  • permz (NDArray)

  • +
  • modified_deck (list[str])

  • +
+
+
Return type:
+

list[str]

+
+
+
+ +
+
+redistribute_removed_pore_volume(dck, con, cluster_minimum, cluster_maximum, removal_mask)[source]#
+

Redistribute pore volume from removed coarse cells.

+

Pore volume is divided among the nearest active neighbours without changing +the total pore volume.

+
+
Parameters:
+
+
dck

Deck configuration whose output_porv is updated.

+
+
con

One-based coarse-cell identifier for each original cell.

+
+
cluster_minimum, cluster_maximum

Aggregated activity values used to identify changed clusters.

+
+
removal_mask

Mask identifying retained coarse cells.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • con (NDArray)

  • +
  • cluster_minimum (NDArray)

  • +
  • cluster_maximum (NDArray)

  • +
  • removal_mask (NDArray)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_find_active_neighbors(dck, neighbor_indices, cluster_id, distance, offset, ijk)[source]#
+

Find active neighbouring cells for pore-volume redistribution.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • neighbor_indices (list[int])

  • +
  • cluster_id (int)

  • +
  • distance (int)

  • +
  • offset (int)

  • +
  • ijk (list)

  • +
+
+
Return type:
+

list[int]

+
+
+
+ +
+
+_global_index_to_ijk(dck, global_index)[source]#
+

Return the i, j, and k indices from a zero-based global cell index.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • global_index (int)

  • +
+
+
Return type:
+

tuple[int, int, int]

+
+
+
+ +
+
+coarsen_corner_point_grid(dck, coarsening)[source]#
+

Remove selected pillars and ZCORN surfaces from the grid.

+
+
Parameters:
+
+
dck

Deck configuration containing the original corner-point grid.

+
+
coarsening

Axis mappings defining the removed rows, columns, and layers.

+
+
+
+
Returns:
+
+
coord, zcorn

Coarsened arrays when dual porosity is enabled; otherwise empty arrays.

+
+
+
+
Parameters:
+
+
+
Return type:
+

tuple[NDArray, NDArray]

+
+
+
+ +
+
+build_dual_porosity_grid(dck, coarsening, cr, zc)[source]#
+

Extend a coarsened grid with a second porosity continuum.

+

The matrix and fracture grids are separated in the j direction, and their +connections are added to coarsening.nnc_text.

+
+
Parameters:
+
+
dck

Deck configuration for the coarsened model.

+
+
coarsening

Coarsening data containing continuum masks and transmissibilities.

+
+
cr, zc

Coarsened COORD and ZCORN arrays.

+
+
+
+
Returns:
+
+
coord, zcorn

Extended dual-porosity grid arrays.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • coarsening (CoarseningMaps)

  • +
  • cr (NDArray)

  • +
  • zc (NDArray)

  • +
+
+
Return type:
+

tuple[NDArray, NDArray]

+
+
+
+ +
+
+_collect_removed_zcorn_indices(dck, coa_z, removal_indices)[source]#
+

Add the ZCORN indices removed by vertical coarsening.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • coa_z (NDArray)

  • +
  • removal_indices (list[int])

  • +
+
+
Return type:
+

list[int]

+
+
+
+ +
+
+map_nnc_transmissibilities(dck, coarsening)[source]#
+

Map original non-neighbouring transmissibilities to the coarse grid.

+

Connections that become Cartesian neighbours are accumulated in TRANX or +TRANY; remaining connections are written as NNC records.

+
+
Parameters:
+
+
dck

Deck configuration and source NNC data.

+
+
coarsening

Coarse mapping updated with transmissibilities and NNC text.

+
+
+
+
Returns:
+
+
generated_files

Names of the written include files.

+
+
+
+
Parameters:
+
+
+
Return type:
+

list[str]

+
+
+
+ +
+
+create_coarsening_map(cfg)[source]#
+

Map each fine-grid cell to a one-based coarse-cell identifier.

+

The output dimensions and original-to-output axis mappings in cfg are also +updated.

+
+
Parameters:
+
+
cfg

TOML configuration containing the axis coarsening arrays.

+
+
+
+
Returns:
+
+
NDArray

One-based coarse-cell identifier for every fine-grid cell.

+
+
+
+
Parameters:
+

cfg (ConfigViaTOML)

+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_group_minimum_zero_based(values, groups, number_groups)[source]#
+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • number_groups (int)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_group_maximum_zero_based(values, groups, number_groups)[source]#
+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • number_groups (int)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_group_sum_zero_based(values, groups, number_groups)[source]#
+
+
Parameters:
+
    +
  • values (NDArray)

  • +
  • groups (NDArray)

  • +
  • number_groups (int)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_read_satnum(cfg, actnum, nxyz, satnum_opm)[source]#
+

Read or generate fine-grid SATNUM values.

+
+
Parameters:
+
+
cfg

TOML configuration controlling the SATNUM source.

+
+
actnum

Fine-grid active-cell mask.

+
+
nxyz

Number of fine-grid cells.

+
+
satnum_opm

SATNUM values read from the reference INIT file.

+
+
+
+
Returns:
+
+
NDArray

SATNUM value for every fine-grid cell.

+
+
+
+
Parameters:
+
    +
  • cfg (ConfigViaTOML)

  • +
  • actnum (NDArray)

  • +
  • nxyz (int)

  • +
  • satnum_opm (NDArray)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+coarsen_and_write_properties(cfg, coa_map)[source]#
+

Aggregate and write properties for a TOML-generated model.

+
+
Parameters:
+
+
cfg

TOML configuration and reference-case settings.

+
+
coa_map

One-based fine-to-coarse cell mapping.

+
+
+
+
Returns:
+
+
int

Highest generated SATNUM value, used as the number of saturation tables.

+
+
+
+
Parameters:
+
    +
  • cfg (ConfigViaTOML)

  • +
  • coa_map (NDArray)

  • +
+
+
Return type:
+

int

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.utils.files_writer.html b/docs/api/pycopm.utils.files_writer.html new file mode 100644 index 0000000..9e30a1c --- /dev/null +++ b/docs/api/pycopm.utils.files_writer.html @@ -0,0 +1,1118 @@ + + + + + + + + + + + pycopm.utils.files_writer module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.files_writer module#

+

Write OPM Flow decks, corner-point grids, properties, and ERT files.

+
+
+format_opm_compact_values(v)[source]#
+

Convert values to OPM repeated-value notation.

+
+
Parameters:
+
+
v

One-dimensional values to compact.

+
+
+
+
Returns:
+
+
list[str]

Values formatted as n*value where consecutive values repeat.

+
+
+
+
Parameters:
+

v (NDArray)

+
+
Return type:
+

list[str]

+
+
+
+ +
+
+round_like_e(values, significant_digits)[source]#
+

Round values to a number of significant digits.

+
+
Parameters:
+
+
values

Values to round.

+
+
significant_digits

Number of significant digits, or zero to preserve machine precision.

+
+
+
+
Returns:
+
+
NDArray

Rounded values with the original shape.

+
+
+
+
Parameters:
+
    +
  • values (NDArray | list)

  • +
  • significant_digits (int)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+_identify_removed_pillars(cfg)[source]#
+

Find COORD and ZCORN indices removed by TOML coarsening.

+
+
Parameters:
+
+
cfg

TOML configuration containing axis coarsening arrays.

+
+
+
+
Returns:
+
+
coord_indices, zcorn_indices

Indices to remove from the reference grid arrays.

+
+
+
+
Parameters:
+

cfg (ConfigViaTOML)

+
+
Return type:
+

tuple[NDArray, NDArray]

+
+
+
+ +
+
+_render_template(template_path, output_path, **variables)[source]#
+

Render a Mako template to a UTF-8 file.

+
+
Parameters:
+
+
template_path

Source template path.

+
+
output_path

Destination path.

+
+
**variables

Values passed to the template.

+
+
+
+
Parameters:
+
    +
  • template_path (Path)

  • +
  • output_path (Path)

  • +
  • variables (Any)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+write_coarsened_model_files(cfg, number_tables)[source]#
+

Write grid, deck, schedule, job, observation, and ERT files.

+
+
Parameters:
+
+
cfg

TOML configuration for the generated project.

+
+
number_tables

Number of saturation-function tables to generate.

+
+
+
+
Parameters:
+
    +
  • cfg (ConfigViaTOML)

  • +
  • number_tables (int)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_write_ert_files(cfg, number_tables)[source]#
+

Write the ERT configuration, parameter, job, and observation files.

+
+
Parameters:
+
    +
  • cfg (ConfigViaTOML)

  • +
  • number_tables (int)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_write_let_tables(cfg, number_tables)[source]#
+

Write the LET saturation-function tables.

+
+
Parameters:
+
    +
  • cfg (ConfigViaTOML)

  • +
  • number_tables (int)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_write_grid_files(cfg)[source]#
+

Write the OPM grid-related files.

+
+
Parameters:
+

cfg (ConfigViaTOML)

+
+
Return type:
+

None

+
+
+
+ +
+
+write_include(output_path, text)[source]#
+

Write text to an OPM include file with the pycopm header.

+
+
Parameters:
+
+
output_path

Destination include path.

+
+
text

OPM deck text written after the header.

+
+
+
+
Parameters:
+
    +
  • output_path (Path)

  • +
  • text (str)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+write_property(output_path, keyword, values, num_dig)[source]#
+

Write one compact OPM property.

+
+
Parameters:
+
+
output_path

Destination include path.

+
+
keyword

OPM property keyword.

+
+
values

Property values in global-cell order.

+
+
num_dig

Number of significant digits.

+
+
+
+
Parameters:
+
    +
  • output_path (Path)

  • +
  • keyword (str)

  • +
  • values (NDArray)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+write_compact_property_file(preprocessing_path, property_name, values, num_dig)[source]#
+

Write one compacted OPM property to a file.

+
+
Parameters:
+
    +
  • preprocessing_path (Path)

  • +
  • property_name (str)

  • +
  • values (NDArray)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+write_porv(dck, modified_deck)[source]#
+

Write the OPM grid-related files.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • modified_deck (list[str])

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+write_property_inc(dck, property_name, property_values, number_values, modified_deck, allow_inline=False, file_suffix='')[source]#
+

Write a property include or inline a constant property.

+
+
Parameters:
+
+
dck

Deck configuration controlling paths, prefixes, and precision.

+
+
property_name

OPM property name.

+
+
property_values

Values in output-grid order.

+
+
number_values

Expected number of output values.

+
+
modified_deck

Deck lines in which a constant property may be inlined.

+
+
allow_inline

Inline a property when all values are equal.

+
+
file_suffix

Optional suffix added before .INC.

+
+
+
+
Returns:
+
+
bool

True when the property was inlined, otherwise False.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • property_name (str)

  • +
  • property_values (NDArray)

  • +
  • number_values (int)

  • +
  • modified_deck (list[str])

  • +
  • allow_inline (bool)

  • +
  • file_suffix (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+write_grid(cfg, cr, zc, dual)[source]#
+

Write a corner-point grid in GRDECL syntax.

+
+
Parameters:
+
+
cfg

Deck or TOML configuration containing output dimensions and precision.

+
+
cr, zc

Flattened COORD and ZCORN arrays.

+
+
dual

Extend the j dimension for a dual-porosity grid.

+
+
+
+
Parameters:
+
    +
  • cfg (ConfigViaDeck | ConfigViaTOML)

  • +
  • cr (NDArray)

  • +
  • zc (NDArray)

  • +
  • dual (bool)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+write_reference_to_coarse_map(dck, reftocoa)[source]#
+

Write the reference-to-coarse mapping as OPERNUM.

+
+
Parameters:
+
+
dck

Deck configuration controlling the output path and precision.

+
+
reftocoa

Coarse-cell identifier for every reference-grid cell.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • reftocoa (NDArray)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+write_dual_properties(dck, coarsening, number_values, modified_deck)[source]#
+

Finalize property files for a dual-porosity grid.

+
+
Parameters:
+
+
dck

Deck configuration and generated property names.

+
+
coarsening

Coarsening data containing matrix and fracture transmissibilities.

+
+
number_values

Number of cells in the extended dual grid.

+
+
modified_deck

Deck lines that may receive inlined properties.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • number_values (int)

  • +
  • modified_deck (list[str])

  • +
+
+
Return type:
+

None

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.utils.generate_decks.html b/docs/api/pycopm.utils.generate_decks.html new file mode 100644 index 0000000..fb7fc2a --- /dev/null +++ b/docs/api/pycopm.utils.generate_decks.html @@ -0,0 +1,812 @@ + + + + + + + + + + + pycopm.utils.generate_decks module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.generate_decks module#

+

Coordinate coarsening, refinement, submodel extraction, and grid transformations.

+
+
+create_deck(dck, cmdargs)[source]#
+

Generate a modified OPM deck and its include files.

+

The selected workflow can preprocess the input deck, coarsen or refine the +grid, extract a vicinity submodel, transform coordinates, and optionally run +a validation dry run.

+
+
Parameters:
+
+
dck

Deck configuration populated from command-line arguments.

+
+
cmdargs

Parsed command arguments used to build coarsening or refinement maps.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • cmdargs (Namespace)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_correct_fluid_in_place(dck, modified_deck)[source]#
+

Adjust output pore volume to match input oil and gas in place.

+

Short Flow runs provide the fluid-in-place values used for two successive pore +volume corrections.

+
+
Parameters:
+
+
dck

Deck configuration whose output_porv is updated.

+
+
modified_deck

Generated deck lines used to create the correction case.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • modified_deck (list[str])

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_create_index_mappings(dck, vicinity, refinement, coarsening)[source]#
+

Create original-to-output mappings for each grid axis.

+

Depending on the selected workflow, the mappings represent coarse cells, +submodel indices, or the first and last cells created by refinement.

+
+
Parameters:
+
+
dck

Deck configuration updated with the index mappings.

+
+
vicinity

Vicinity bounds for submodel extraction.

+
+
refinement

Per-axis refinement values.

+
+
coarsening

Per-axis coarsening values.

+
+
+
+
Parameters:
+
+
+
Return type:
+

None

+
+
+
+ +
+
+_initialize_deck_data(dck)[source]#
+

Load dry-run grid and property data into the deck configuration.

+

The function opens EGRID, INIT, and optional restart files, determines grid +dimensions, and collects available property keywords.

+
+
Parameters:
+
+
dck

Deck configuration updated with OPM files, dimensions, and keyword lists.

+
+
+
+
Parameters:
+

dck (ConfigViaDeck)

+
+
Return type:
+

None

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/pycopm.utils.html b/docs/api/pycopm.utils.html similarity index 62% rename from docs/pycopm.utils.html rename to docs/api/pycopm.utils.html index 2f43a15..adbdcb5 100644 --- a/docs/pycopm.utils.html +++ b/docs/api/pycopm.utils.html @@ -1,265 +1,771 @@ - - - - - - - pycopm.utils package — pycopm documentation - - - - - - - - - - - - - + + + + + + + + + pycopm.utils package — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
- -
- - -
- -
-
-
-
+ + +
+ + + + + + + +
+ +
+ + + - + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/api/pycopm.utils.input_values.html b/docs/api/pycopm.utils.input_values.html new file mode 100644 index 0000000..460bcf4 --- /dev/null +++ b/docs/api/pycopm.utils.input_values.html @@ -0,0 +1,1153 @@ + + + + + + + + + + + pycopm.utils.input_values module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.input_values module#

+

Create configuration objects from command-line arguments and TOML files.

+
+
+create_deck_config(cmdargs)[source]#
+

Create a deck configuration from parsed command arguments.

+
+
Parameters:
+
+
cmdargsargparse.Namespace

Command-line arguments for the deck-based workflow.

+
+
+
+
Returns:
+
+
ConfigViaDeck

Configuration populated from the command-line values.

+
+
+
+
Parameters:
+

cmdargs (Namespace)

+
+
Return type:
+

ConfigViaDeck

+
+
+
+ +
+
+parse_axis_modifications(uniform, localized)[source]#
+

Parse uniform or axis-specific grid modifications.

+

Uniform input contains one value for each axis. Axis-specific coarsening +also accepts one-based indices and inclusive ranges such as 2:4,7.

+
+
Parameters:
+
+
uniformstr

Comma-separated x, y, and z modification values.

+
+
localizedlist

Axis-specific specifications in x, y, and z order.

+
+
+
+
Returns:
+
+
cijk, axis_values

Uniform axis values and the three parsed axis-specific arrays. Only one +representation is populated.

+
+
+
+
Parameters:
+
    +
  • uniform (str)

  • +
  • localized (list)

  • +
+
+
Return type:
+

tuple[NDArray, list]

+
+
+
+ +
+
+_is_finite_number(value)[source]#
+

Check whether a value is a finite non-Boolean number.

+
+
Parameters:
+
+
valueAny

Value to inspect.

+
+
+
+
Returns:
+
+
bool

Whether the value is a finite integer or floating-point number.

+
+
+
+
Parameters:
+

value (Any)

+
+
Return type:
+

TypeGuard[int | float]

+
+
+
+ +
+
+_is_integer(value)[source]#
+

Check whether a value is a non-Boolean integer.

+
+
Parameters:
+
+
valueAny

Value to inspect.

+
+
+
+
Returns:
+
+
bool

Whether the value is an integer and not a Boolean.

+
+
+
+
Parameters:
+

value (Any)

+
+
Return type:
+

TypeGuard[int]

+
+
+
+ +
+
+_add_validation_error(errors, message)[source]#
+

Add a TOML validation error.

+
+
Parameters:
+
+
errorslist[str]

Validation messages collected during the current validation pass.

+
+
messagestr

Human-readable validation message.

+
+
+
+
Parameters:
+
    +
  • errors (list[str])

  • +
  • message (str)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_warn(message)[source]#
+

Display a TOML validation warning.

+
+
Parameters:
+
+
messagestr

Human-readable warning passed to the shared terminal helper.

+
+
+
+
Parameters:
+

message (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+_validate_string(cfg_file, key, errors)[source]#
+

Check that a TOML variable is a non-empty string.

+
+
Parameters:
+
+
cfg_filedict[str, Any]

TOML configuration values.

+
+
keystr

Configuration variable name.

+
+
errorslist[str]

Validation messages collected during the current validation pass.

+
+
+
+
Returns:
+
+
bool

Whether the variable is present and contains a non-empty string.

+
+
+
+
Parameters:
+
    +
  • cfg_file (dict[str, Any])

  • +
  • key (str)

  • +
  • errors (list[str])

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_validate_integer(cfg_file, key, errors, minimum=0)[source]#
+

Check that a TOML variable is an integer within its lower bound.

+
+
Parameters:
+
+
cfg_filedict[str, Any]

TOML configuration values.

+
+
keystr

Configuration variable name.

+
+
errorslist[str]

Validation messages collected during the current validation pass.

+
+
minimumint, optional

Inclusive lower bound for accepted values.

+
+
+
+
Returns:
+
+
bool

Whether the variable is present and satisfies the integer constraint.

+
+
+
+
Parameters:
+
    +
  • cfg_file (dict[str, Any])

  • +
  • key (str)

  • +
  • errors (list[str])

  • +
  • minimum (int)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_validate_number_array(cfg_file, key, errors, *, length=None, minimum=None, maximum=None)[source]#
+

Check the shape and values of a numeric TOML array.

+
+
Parameters:
+
+
cfg_filedict[str, Any]

TOML configuration values.

+
+
keystr

Configuration variable name.

+
+
errorslist[str]

Validation messages collected during the current validation pass.

+
+
lengthint | None, optional

Required number of entries.

+
+
minimumfloat | None, optional

Inclusive lower bound for every entry.

+
+
maximumfloat | None, optional

Inclusive upper bound for every entry.

+
+
+
+
Returns:
+
+
bool

Whether the variable is present and satisfies all array constraints.

+
+
+
+
Parameters:
+
    +
  • cfg_file (dict[str, Any])

  • +
  • key (str)

  • +
  • errors (list[str])

  • +
  • length (int | None)

  • +
  • minimum (float | None)

  • +
  • maximum (float | None)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_validate_coarsening(cfg_file, key, expected_length, errors)[source]#
+

Validate one model-specific axis coarsening array.

+

The array must contain non-negative integers, match the number of grid +boundaries for the selected model, and retain both outer boundaries.

+
+
Parameters:
+
+
cfg_filedict[str, Any]

TOML configuration values.

+
+
keystr

Name of the x, y, or z coarsening variable.

+
+
expected_lengthint

Required number of entries for the selected reference model.

+
+
errorslist[str]

Validation messages collected during the current validation pass.

+
+
+
+
Parameters:
+
    +
  • cfg_file (dict[str, Any])

  • +
  • key (str)

  • +
  • expected_length (int)

  • +
  • errors (list[str])

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_validate_let_parameters(cfg_file, model, errors)[source]#
+

Validate the ordered LET-parameter definitions.

+

Each row contains a coefficient name, initial value, estimation flag, +distribution name, lower bound, and upper bound. The order is checked because +downstream table generation addresses coefficients by position.

+
+
Parameters:
+
+
cfg_filedict[str, Any]

TOML configuration values.

+
+
modelstr | None

Normalized reference-model name, when valid.

+
+
errorslist[str]

Validation messages collected during the current validation pass.

+
+
+
+
Parameters:
+
    +
  • cfg_file (dict[str, Any])

  • +
  • model (str | None)

  • +
  • errors (list[str])

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_validate_rock_properties(cfg_file, errors)[source]#
+

Validate rock-property history matching settings.

+

Each row contains a permeability name, estimation flag, and aggregation +method. Property names are normalized to uppercase after validation.

+
+
Parameters:
+
+
cfg_filedict[str, Any]

TOML configuration values.

+
+
errorslist[str]

Validation messages collected during the current validation pass.

+
+
+
+
Parameters:
+
    +
  • cfg_file (dict[str, Any])

  • +
  • errors (list[str])

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_validate_toml(cfg_file)[source]#
+

Validate and normalize TOML configuration values.

+

Unknown and internally managed variables are reported and removed. Remaining +errors are collected so the user receives one complete validation report.

+
+
Parameters:
+
+
cfg_filedict[str, Any]

Raw values loaded from the TOML configuration.

+
+
+
+
Returns:
+
+
dict[str, Any]

Validated and normalized values suitable for ConfigViaTOML.

+
+
+
+
Raises:
+
+
SystemExit

If one or more configuration values are invalid.

+
+
+
+
Parameters:
+

cfg_file (dict[str, Any])

+
+
Return type:
+

dict[str, Any]

+
+
+
+ +
+
+load_toml_config(input_file, output_directory, resource_directory, significant_digits)[source]#
+

Load, validate, and initialize a TOML configuration.

+

Validation and normalization occur before the reference EGRID is opened and +before ConfigViaTOML is constructed.

+
+
Parameters:
+
+
input_filestr

TOML configuration path.

+
+
output_directorystr

Generated-project directory.

+
+
resource_directorystr

Directory containing templates and reference simulations.

+
+
significant_digitsint

Precision used when writing floating-point values.

+
+
+
+
Returns:
+
+
ConfigViaTOML

Validated configuration populated with reference-grid metadata.

+
+
+
+
Raises:
+
+
SystemExit

If the TOML configuration is invalid.

+
+
+
+
Parameters:
+
    +
  • input_file (str)

  • +
  • output_directory (str)

  • +
  • resource_directory (str)

  • +
  • significant_digits (int)

  • +
+
+
Return type:
+

ConfigViaTOML

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.utils.parser_deck.html b/docs/api/pycopm.utils.parser_deck.html new file mode 100644 index 0000000..3eae622 --- /dev/null +++ b/docs/api/pycopm.utils.parser_deck.html @@ -0,0 +1,1646 @@ + + + + + + + + + + + pycopm.utils.parser_deck module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.parser_deck module#

+

Parse an OPM deck and update records for the modified grid.

+
+
+class _ParserState(dimens=False, grid=False, welspecs=False, welsegs=False, complump=False, compdat=False, compsegs=False, mapaxes=False, multregt=False, process_edit=False, editnnc=False, multiply=False, props=False, operation=False, regions=False, equil=False, faults=False, multflt=False, welldims=False, skip_block=False, aqucon=False, aqunum=False, aquancon=False, bccon=False, bwpr=False, source=False, pinch=False, has_edit=False, previous_completion=<factory>, compsegs_wells=<factory>, retained_wells=<factory>, completion_wells=<factory>, segmented_wells=<factory>, separator='', schedule_keyword='')[source]#
+

Bases: object

+

Store temporary state while parsing an OPM deck.

+

The Boolean fields indicate active keyword blocks. The list fields track wells, +completions, and segmented-well records retained in the generated deck.

+
+
Parameters:
+
    +
  • dimens (bool)

  • +
  • grid (bool)

  • +
  • welspecs (bool)

  • +
  • welsegs (bool)

  • +
  • complump (bool)

  • +
  • compdat (bool)

  • +
  • compsegs (bool)

  • +
  • mapaxes (bool)

  • +
  • multregt (bool)

  • +
  • process_edit (bool)

  • +
  • editnnc (bool)

  • +
  • multiply (bool)

  • +
  • props (bool)

  • +
  • operation (bool)

  • +
  • regions (bool)

  • +
  • equil (bool)

  • +
  • faults (bool)

  • +
  • multflt (bool)

  • +
  • welldims (bool)

  • +
  • skip_block (bool)

  • +
  • aqucon (bool)

  • +
  • aqunum (bool)

  • +
  • aquancon (bool)

  • +
  • bccon (bool)

  • +
  • bwpr (bool)

  • +
  • source (bool)

  • +
  • pinch (bool)

  • +
  • has_edit (bool)

  • +
  • previous_completion (list[str])

  • +
  • compsegs_wells (list[str])

  • +
  • retained_wells (list[str])

  • +
  • completion_wells (list[str])

  • +
  • segmented_wells (list[str])

  • +
  • separator (str)

  • +
  • schedule_keyword (str)

  • +
+
+
+
+
+dimens: bool#
+
+ +
+
+grid: bool#
+
+ +
+
+welspecs: bool#
+
+ +
+
+welsegs: bool#
+
+ +
+
+complump: bool#
+
+ +
+
+compdat: bool#
+
+ +
+
+compsegs: bool#
+
+ +
+
+mapaxes: bool#
+
+ +
+
+multregt: bool#
+
+ +
+
+process_edit: bool#
+
+ +
+
+editnnc: bool#
+
+ +
+
+multiply: bool#
+
+ +
+
+props: bool#
+
+ +
+
+operation: bool#
+
+ +
+
+regions: bool#
+
+ +
+
+equil: bool#
+
+ +
+
+faults: bool#
+
+ +
+
+multflt: bool#
+
+ +
+
+welldims: bool#
+
+ +
+
+skip_block: bool#
+
+ +
+
+aqucon: bool#
+
+ +
+
+aqunum: bool#
+
+ +
+
+aquancon: bool#
+
+ +
+
+bccon: bool#
+
+ +
+
+bwpr: bool#
+
+ +
+
+source: bool#
+
+ +
+
+pinch: bool#
+
+ +
+
+has_edit: bool#
+
+ +
+
+previous_completion: list[str]#
+
+ +
+
+compsegs_wells: list[str]#
+
+ +
+
+retained_wells: list[str]#
+
+ +
+
+completion_wells: list[str]#
+
+ +
+
+segmented_wells: list[str]#
+
+ +
+
+separator: str#
+
+ +
+
+schedule_keyword: str#
+
+ +
+ +
+
+process_deck(dck, vicinity)[source]#
+

Rewrite deck records for the modified grid.

+

The parser updates dimensions, properties, grid-index ranges, wells, aquifers, +faults, and selected schedule records.

+
+
Parameters:
+
+
dck

Deck configuration and axis index mappings.

+
+
vicinity

Vicinity selection used when extracting a submodel.

+
+
+
+
Returns:
+
+
modified_deck, well_cell_indices

Rewritten deck lines and coarse cells containing well completions.

+
+
+
+
Parameters:
+
+
+
Return type:
+

tuple[list[str], list[int]]

+
+
+
+ +
+
+_include_contains_endbox(dck, nrwo)[source]#
+

Return whether an included file contains ENDBOX.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+__handle_schedule_keyword(kwr, modified_deck, nrwo)[source]#
+

Filter supported schedule records by wells retained in the submodel.

+
+
Parameters:
+
    +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_collect_vicinity_well_names(dck, kwr, vicinity)[source]#
+

Identify wells and segmented wells retained in the vicinity model.

+
+
Parameters:
+
+
+
Return type:
+

None

+
+
+
+ +
+
+_collect_segmented_well_names(dck, kwr)[source]#
+

Identify wells requiring segmented-well completion handling.

+
+
Parameters:
+
+
+
Return type:
+

None

+
+
+
+ +
+
+_handle_dimens(dck, kwr, modified_deck, nrwo)[source]#
+

Replace the original DIMENS values with the modified grid dimensions.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_welldims(dck, kwr, modified_deck, nrwo)[source]#
+

Update WELLDIMS for a refined grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_props(dck, vicinity, kwr, modified_deck, nrwo)[source]#
+

Handle the PROPS section and its supported operations.

+
+
Parameters:
+
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_oper(dck, vicinity, kwr, modified_deck, nrwo)[source]#
+

Update supported operation records for the modified grid.

+
+
Parameters:
+
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_bwpr(dck, kwr, modified_deck, nrwo)[source]#
+

Update BWPR grid indices for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_regions(dck, kwr, modified_deck, nrwo)[source]#
+

Replace the REGIONS content with generated include files.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_equil(dck, kwr, modified_deck, nrwo)[source]#
+

Replace EQUIL with explicit initialization include files.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_append_explicit_solution_includes(dck, modified_deck)[source]#
+

Append include statements for the explicit solution properties.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • modified_deck (list[str])

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_handle_grid_props(dck, kwr, modified_deck, nrwo)[source]#
+

Replace GRID properties and preserve supported GRID-section keywords.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_aqunum(dck, kwr, modified_deck, nrwo)[source]#
+

Update AQUNUM grid indices for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_aquancon(dck, kwr, modified_deck, nrwo)[source]#
+

Update AQUANCON grid-index ranges for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_aqucon(dck, kwr, modified_deck, nrwo)[source]#
+

Update AQUCON grid-index ranges for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_multflt(kwr, modified_deck, nrwo)[source]#
+

Preserve fault multiplier records from the input deck.

+
+
Parameters:
+
    +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_mapaxes(kwr, modified_deck, nrwo)[source]#
+

Preserve MAPAXES so the generated grids retain the same map view.

+
+
Parameters:
+
    +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_pinch(kwr, modified_deck, nrwo)[source]#
+

Preserve PINCH records from the input deck.

+
+
Parameters:
+
    +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_multregt(kwr, modified_deck, nrwo)[source]#
+

Preserve MULTREGT records from the GRID section.

+
+
Parameters:
+
    +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_bccon(dck, kwr, modified_deck, nrwo)[source]#
+

Update BCCON grid-index ranges for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_multiply(dck, kwr, modified_deck, nrwo)[source]#
+

Update MULTIPLY grid-index ranges for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_editnnc(dck, kwr, modified_deck, nrwo)[source]#
+

Update EDITNNC grid indices for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_fault(dck, kwr, modified_deck, nrwo)[source]#
+

Update FAULTS grid-index ranges for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_welsegs(kwr, modified_deck, nrwo)[source]#
+

Filter WELSEGS records by wells retained in the submodel.

+
+
Parameters:
+
    +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_compsegs(kwr, modified_deck, nrwo)[source]#
+

Filter COMPSEGS records by wells retained in the submodel.

+
+
Parameters:
+
    +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_segmented_wells(dck, kwr, modified_deck, nrwo, wellcind)[source]#
+

Update COMPDAT, COMPSEGS, and COMPLUMP records for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
  • wellcind (list)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_wells(dck, kwr, modified_deck, nrwo, hv)[source]#
+

Update well-head grid indices and activate completion handlers.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
  • hv (bool)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_handle_source(dck, kwr, modified_deck, nrwo)[source]#
+

Update SOURCE grid indices for the modified grid.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • kwr (_ParserState)

  • +
  • modified_deck (list[str])

  • +
  • nrwo (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+_scan_deck_file(dck, file_path)[source]#
+

Scan a deck file for includes and directional multipliers.

+
+
Parameters:
+
+
dck

Deck configuration providing the file encoding.

+
+
file_path

DATA or include file to scan.

+
+
+
+
Returns:
+
+
includes, has_main_multflt, multipliers

Resolved include paths, whether the main deck contains MULTFLT, and +flags for MULTX, MULTX-, MULTY, MULTY-, MULTZ, and +MULTZ-.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • file_path (str | Path)

  • +
+
+
Return type:
+

tuple[list[str], bool, NDArray]

+
+
+
+ +
+
+_mark_multiplier_keyword(deck_line, mults)[source]#
+

Set the corresponding flag if a multiplier keyword is found.

+
+
Parameters:
+
    +
  • deck_line (str)

  • +
  • mults (NDArray)

  • +
+
+
Return type:
+

NDArray

+
+
+
+ +
+
+find_multiplier_keywords(dck)[source]#
+

Find directional multiplier keywords in nested includes.

+

At most three levels of included files are scanned.

+
+
Parameters:
+
+
dck

Deck configuration identifying the input deck and encoding.

+
+
+
+
Returns:
+
+
has_main_multflt, multipliers

Whether the main deck contains MULTFLT and directional multiplier +flags in x, x-, y, y-, z, and z- order.

+
+
+
+
Parameters:
+

dck (ConfigViaDeck)

+
+
Return type:
+

tuple[bool, NDArray]

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.utils.refinement.html b/docs/api/pycopm.utils.refinement.html new file mode 100644 index 0000000..e7db759 --- /dev/null +++ b/docs/api/pycopm.utils.refinement.html @@ -0,0 +1,944 @@ + + + + + + + + + + + pycopm.utils.refinement module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.refinement module#

+

Refine a corner-point grid and its reservoir properties.

+
+
+class RefinementMaps(x, y, z, refined_cell_counts)[source]#
+

Bases: object

+

Store axis refinement values and subdivision counts.

+
+
Parameters:
+
    +
  • x (NDArray)

  • +
  • y (NDArray)

  • +
  • z (NDArray)

  • +
  • refined_cell_counts (NDArray)

  • +
+
+
+
+
+x: NDArray#
+

Number of additional cells created from each original x interval.

+
+ +
+
+y: NDArray#
+

Number of additional cells created from each original y interval.

+
+ +
+
+z: NDArray#
+

Number of additional cells created from each original z interval.

+
+ +
+
+refined_cell_counts: NDArray#
+

Number of refined cells generated from each original cell, flattened in +(z, y, x) order.

+
+ +
+ +
+
+create_refinement_maps(dck, cmdargs)[source]#
+

Create axis refinement maps and update output dimensions.

+

A refinement value of n divides an original interval into n + 1 +intervals.

+
+
Parameters:
+
+
dck

Deck configuration whose output dimensions are updated.

+
+
cmdargs

Command arguments containing refinement, x_refinement, +y_refinement, and z_refinement.

+
+
+
+
Returns:
+
+
RefinementMaps

Axis values and the number of subdivisions per original cell.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • cmdargs (Namespace)

  • +
+
+
Return type:
+

RefinementMaps

+
+
+
+ +
+
+refine_properties(dck, refinement, modified_deck)[source]#
+

Map reservoir properties onto the refined grid.

+

Properties are copied to generated cells. PORV is divided equally among +them to preserve each original cell's pore volume.

+
+
Parameters:
+
+
dck

Deck configuration containing source properties and output dimensions.

+
+
refinement

Refinement maps created by create_refinement_maps().

+
+
modified_deck

Deck lines updated with generated property includes.

+
+
+
+
Returns:
+
+
generated_files

Names of the written include files.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • refinement (RefinementMaps)

  • +
  • modified_deck (list[str])

  • +
+
+
Return type:
+

list[str]

+
+
+
+ +
+
+create_coord_axis_map(refinement_values)[source]#
+

Create interpolation data for one COORD axis.

+
+
Parameters:
+
+
refinement_values

Number of additional cells in each original interval.

+
+
+
+
Returns:
+
+
source_indices, fractions

Original intervals and relative positions of refined grid points.

+
+
+
+
Parameters:
+

refinement_values (NDArray)

+
+
Return type:
+

tuple[NDArray, NDArray]

+
+
+
+ +
+
+create_zcorn_axis_map(refinement_values)[source]#
+

Create interpolation data for one ZCORN axis.

+
+
Parameters:
+
+
refinement_values

Number of additional cells in each original interval.

+
+
+
+
Returns:
+
+
source_indices, fractions

Original intervals and relative corner positions in ZCORN order.

+
+
+
+
Parameters:
+

refinement_values (NDArray)

+
+
Return type:
+

tuple[NDArray, NDArray]

+
+
+
+ +
+
+refine_zcorn_surface(source_surface, destination_surface, original_nx, original_ny, output_nx, output_ny, zcorn_x_indices, zcorn_y_indices, zcorn_x_fractions, zcorn_y_fractions)[source]#
+

Interpolate one ZCORN surface onto the refined horizontal grid.

+
+
Parameters:
+
+
source_surface

Flattened input surface with 4 * original_nx * original_ny values.

+
+
destination_surface

Preallocated output with 4 * output_nx * output_ny values, modified +in place.

+
+
original_nx, original_ny

Original horizontal grid dimensions.

+
+
output_nx, output_ny

Refined horizontal grid dimensions.

+
+
zcorn_x_indices, zcorn_y_indices

Source interval indices for refined corners.

+
+
zcorn_x_fractions, zcorn_y_fractions

Relative interpolation positions within source intervals.

+
+
+
+
Parameters:
+
    +
  • source_surface (NDArray)

  • +
  • destination_surface (NDArray)

  • +
  • original_nx (int)

  • +
  • original_ny (int)

  • +
  • output_nx (int)

  • +
  • output_ny (int)

  • +
  • zcorn_x_indices (NDArray)

  • +
  • zcorn_y_indices (NDArray)

  • +
  • zcorn_x_fractions (NDArray)

  • +
  • zcorn_y_fractions (NDArray)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+refine_grid(dck, refinement)[source]#
+

Create and write the refined corner-point grid.

+

COORD and ZCORN values are linearly interpolated along the refined +axes.

+
+
Parameters:
+
+
dck

Deck configuration containing original geometry and grid dimensions.

+
+
refinement

Axis refinement maps.

+
+
+
+
Parameters:
+
+
+
Return type:
+

None

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.utils.runs_executer.html b/docs/api/pycopm.utils.runs_executer.html new file mode 100644 index 0000000..ef81180 --- /dev/null +++ b/docs/api/pycopm.utils.runs_executer.html @@ -0,0 +1,751 @@ + + + + + + + + + + + pycopm.utils.runs_executer module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.runs_executer module#

+

Run TOML-based simulation studies and generate postprocessing plots.

+
+
+run_simulations(cfg)[source]#
+

Run the configured OPM Flow or ERT workflow.

+

Job scripts are copied to the output project and made executable before a +single realization or ERT study is started.

+
+
Parameters:
+
+
cfg

TOML configuration containing execution mode and commands.

+
+
+
+
Parameters:
+

cfg (ConfigViaTOML)

+
+
Return type:
+

None

+
+
+
+ +
+
+generate_postprocessing_plots(cfg, elapsed_seconds, number_tables)[source]#
+

Render and execute the postprocessing script.

+
+
Parameters:
+
+
cfg

TOML configuration and plotting settings. let_parameters is sorted in +place before rendering.

+
+
elapsed_seconds

Elapsed preprocessing and simulation time.

+
+
number_tables

Number of generated saturation-function tables.

+
+
+
+
Parameters:
+
    +
  • cfg (ConfigViaTOML)

  • +
  • elapsed_seconds (float)

  • +
  • number_tables (int)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.utils.terminal.html b/docs/api/pycopm.utils.terminal.html new file mode 100644 index 0000000..425ecf8 --- /dev/null +++ b/docs/api/pycopm.utils.terminal.html @@ -0,0 +1,1018 @@ + + + + + + + + + + + pycopm.utils.terminal module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.terminal module#

+

Format command-line messages for pycopm.

+

ANSI colors are applied when supported by the selected stream. The module +provides consistent formatting for invalid, accepted, deprecated, and +informational values, plus fatal errors, warnings, tips, progress messages, and +generated-file reports.

+
+
+_supports_color(stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>)[source]#
+

Check whether an output stream supports ANSI colors.

+
+
Parameters:
+
+
streamobject, optional

Output stream used to determine ANSI-color support.

+
+
+
+
Returns:
+
+
bool

Whether ANSI color output is enabled for the stream.

+
+
+
+
Parameters:
+

stream (object)

+
+
Return type:
+

bool

+
+
+
+ +
+
+_colorize(text, code, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>)[source]#
+

Wrap text in an ANSI color sequence when supported.

+
+
Parameters:
+
+
textstr

Text to format.

+
+
codestr

ANSI Select Graphic Rendition code.

+
+
streamobject, optional

Output stream used to determine ANSI-color support.

+
+
+
+
Returns:
+
+
str

Colored text, or unchanged text when colors are disabled.

+
+
+
+
Parameters:
+
    +
  • text (str)

  • +
  • code (str)

  • +
  • stream (object)

  • +
+
+
Return type:
+

str

+
+
+
+ +
+
+cli_warning_value(value)[source]#
+

Format a deprecated CLI option or value.

+
+
Parameters:
+
+
valuestr

Value to inspect or format.

+
+
+
+
Returns:
+
+
str

Quoted value colored as a warning when supported.

+
+
+
+
Parameters:
+

value (str)

+
+
Return type:
+

str

+
+
+
+ +
+
+cli_correct_value(value)[source]#
+

Format a correct CLI option or value.

+
+
Parameters:
+
+
valuestr

Value to inspect or format.

+
+
+
+
Returns:
+
+
str

Quoted value colored as a valid alternative when supported.

+
+
+
+
Parameters:
+

value (str)

+
+
Return type:
+

str

+
+
+
+ +
+
+cli_error_value(value)[source]#
+

Format an invalid CLI option or value.

+
+
Parameters:
+
+
valuestr

Value to inspect or format.

+
+
+
+
Returns:
+
+
str

Quoted value colored as invalid when supported.

+
+
+
+
Parameters:
+

value (str)

+
+
Return type:
+

str

+
+
+
+ +
+
+cli_info_value(value)[source]#
+

Format an informational CLI option or value.

+
+
Parameters:
+
+
valuestr

Value to inspect or format.

+
+
+
+
Returns:
+
+
str

Quoted value colored as information when supported.

+
+
+
+
Parameters:
+

value (str)

+
+
Return type:
+

str

+
+
+
+ +
+
+pycopm_error(message)[source]#
+

Raise a fatal command-line error.

+
+
Parameters:
+
+
messagestr

Human-readable message to display or append.

+
+
+
+
Raises:
+
+
SystemExit

Always raised with the formatted error message.

+
+
+
+
Parameters:
+

message (str)

+
+
Return type:
+

NoReturn

+
+
+
+ +
+
+pycopm_warning(message)[source]#
+

Display a non-fatal command-line warning.

+
+
Parameters:
+
+
messagestr

Human-readable message to display or append.

+
+
+
+
Parameters:
+

message (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+pycopm_info(message)[source]#
+

Display an informational command-line message.

+
+
Parameters:
+
+
messagestr

Human-readable message to display or append.

+
+
+
+
Parameters:
+

message (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+pycopm_tip(message)[source]#
+

Display a command-line suggestion.

+
+
Parameters:
+
+
messagestr

Human-readable message to display or append.

+
+
+
+
Parameters:
+

message (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+pycopm_success(msg, output_dir, filenames)[source]#
+

Display the generated output location and filenames.

+
+
Parameters:
+
+
msgstr

Optional success text printed before the output location.

+
+
output_dirstr

Directory containing the generated files.

+
+
filenameslist[str]

Generated filenames to report.

+
+
+
+
Parameters:
+
    +
  • msg (str)

  • +
  • output_dir (str)

  • +
  • filenames (list[str])

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+pycopm_name(stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>)[source]#
+

Format the pycopm program name.

+
+
Parameters:
+
+
streamobject, optional

Output stream used to determine ANSI-color support.

+
+
+
+
Returns:
+
+
str

Formatted program name.

+
+
+
+
Parameters:
+

stream (object)

+
+
Return type:
+

str

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.utils.transformation.html b/docs/api/pycopm.utils.transformation.html new file mode 100644 index 0000000..960d8ea --- /dev/null +++ b/docs/api/pycopm.utils.transformation.html @@ -0,0 +1,755 @@ + + + + + + + + + + + pycopm.utils.transformation module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.transformation module#

+

Transform corner-point grid coordinates and rewrite associated properties.

+
+
+transform_grid(dck)[source]#
+

Apply the configured transformation to the corner-point grid.

+

Supported specifications are translate [x,y,z], scale [x,y,z], and +rotatexy, rotatexz, or rotateyz followed by an angle in degrees. +Rotations are performed about the coordinate-system origin.

+
+
Parameters:
+
+
dck

Deck configuration containing grid_transformation and source geometry.

+
+
+
+
Parameters:
+

dck (ConfigViaDeck)

+
+
Return type:
+

None

+
+
+
+ +
+
+transform_properties(dck, modified_deck)[source]#
+

Rewrite reservoir properties for a transformed grid.

+

Property values are unchanged because transformations modify only geometry.

+
+
Parameters:
+
+
dck

Deck configuration containing source properties and output dimensions.

+
+
modified_deck

Deck lines updated with generated property includes.

+
+
+
+
Returns:
+
+
generated_files

Names of the written include files.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • modified_deck (list[str])

  • +
+
+
Return type:
+

list[str]

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/api/pycopm.utils.vicinity.html b/docs/api/pycopm.utils.vicinity.html new file mode 100644 index 0000000..c86182d --- /dev/null +++ b/docs/api/pycopm.utils.vicinity.html @@ -0,0 +1,1305 @@ + + + + + + + + + + + pycopm.utils.vicinity module — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

pycopm.utils.vicinity module#

+

Extract submodels and map pore volume from outside their boundaries.

+
+
+class VicinityMaps(selector, shape, cell_mask, min_i, max_i, min_j, max_j, min_k, max_k, layer_min_i, layer_max_i, layer_min_j, layer_max_j, layer_selected_porv, layer_external_porv, well_cells, well_indices=<factory>, active_counts=<factory>, source_indices=<factory>)[source]#
+

Bases: object

+

Store a vicinity selection, bounds, and pore-volume mappings.

+

Global and per-layer bounds are one-based and inclusive.

+
+
Parameters:
+
    +
  • selector (str)

  • +
  • shape (str | None)

  • +
  • cell_mask (NDArray)

  • +
  • min_i (int)

  • +
  • max_i (int)

  • +
  • min_j (int)

  • +
  • max_j (int)

  • +
  • min_k (int)

  • +
  • max_k (int)

  • +
  • layer_min_i (NDArray)

  • +
  • layer_max_i (NDArray)

  • +
  • layer_min_j (NDArray)

  • +
  • layer_max_j (NDArray)

  • +
  • layer_selected_porv (NDArray)

  • +
  • layer_external_porv (NDArray)

  • +
  • well_cells (list[list[int]])

  • +
  • well_indices (list[int])

  • +
  • active_counts (NDArray)

  • +
  • source_indices (NDArray)

  • +
+
+
+
+
+selector: str#
+

Selected well name, or an empty string for polygon and region +selections.

+
+ +
+
+shape: str | None#
+

Well-vicinity shape: box, diamond, or diamondxy. None is +used for polygon and region selections.

+
+ +
+
+cell_mask: NDArray#
+

Boolean mask identifying selected original-grid cells, flattened in +(z, y, x) order.

+
+ +
+
+min_i: int#
+

Minimum selected i index across all layers.

+
+ +
+
+max_i: int#
+

Maximum selected i index across all layers.

+
+ +
+
+min_j: int#
+

Minimum selected j index across all layers.

+
+ +
+
+max_j: int#
+

Maximum selected j index across all layers.

+
+ +
+
+min_k: int#
+

Minimum selected k index.

+
+ +
+
+max_k: int#
+

Maximum selected k index.

+
+ +
+
+layer_min_i: NDArray#
+

Minimum selected i index in each original layer.

+
+ +
+
+layer_max_i: NDArray#
+

Maximum selected i index in each original layer.

+
+ +
+
+layer_min_j: NDArray#
+

Minimum selected j index in each original layer.

+
+ +
+
+layer_max_j: NDArray#
+

Maximum selected j index in each original layer.

+
+ +
+
+layer_selected_porv: NDArray#
+

Total pore volume of selected active cells in each original layer.

+
+ +
+
+layer_external_porv: NDArray#
+

Total pore volume outside the selection in each original layer.

+
+ +
+
+well_cells: list[list[int]]#
+

Zero-based completion coordinates in [i, j, k] order for the +selected well.

+
+ +
+
+well_indices: list[int]#
+

Zero-based output-grid indices of selected well cells. These cells are +excluded from boundary source remapping.

+
+ +
+
+active_counts: NDArray#
+

Number of selected active cells in each output layer.

+
+ +
+
+source_indices: NDArray#
+

Original-grid source index assigned to each output boundary cell during +pore-volume correction. Zero denotes no assigned source.

+
+ +
+ +
+
+class _BoundaryMapping(pore_volume, active_count, offsets)[source]#
+

Bases: object

+

Store pore-volume mapping results for one submodel boundary.

+
+
Parameters:
+
    +
  • pore_volume (float)

  • +
  • active_count (int)

  • +
  • offsets (NDArray)

  • +
+
+
+
+
+pore_volume: float#
+

Unassigned pore volume collected along the boundary.

+
+ +
+
+active_count: int#
+

Number of active cells receiving pore volume from the boundary.

+
+ +
+
+offsets: NDArray#
+

Distance from the geometric boundary to each receiving cell.

+
+ +
+ +
+
+_submodel_index(dck, column, row, layer)[source]#
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • column (int)

  • +
  • row (int)

  • +
  • layer (int)

  • +
+
+
Return type:
+

int

+
+
+
+ +
+
+_original_index(dck, column, row, layer)[source]#
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • column (int)

  • +
  • row (int)

  • +
  • layer (int)

  • +
+
+
Return type:
+

int

+
+
+
+ +
+
+_add_or_collect_porv(dck, submodel_index, pore_volume)[source]#
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • submodel_index (int)

  • +
  • pore_volume (float)

  • +
+
+
Return type:
+

float

+
+
+
+ +
+
+create_vicinity_maps(dck)[source]#
+

Select submodel cells and calculate their bounds.

+

Selections can use region values, an xy polygon, or a well-centred box, +diamond, or diamondxy neighbourhood.

+
+
Parameters:
+
+
dck

Deck configuration containing the vicinity specification and source grid.

+
+
+
+
Returns:
+
+
VicinityMaps

Selection mask, bounds, well cells, and per-layer pore-volume totals.

+
+
+
+
Parameters:
+

dck (ConfigViaDeck)

+
+
Return type:
+

VicinityMaps

+
+
+
+ +
+
+map_vicinity_properties(dck, vicinity, modified_deck)[source]#
+

Map reservoir properties into the submodel bounding box.

+

Cells inside the bounding box but outside the selection are written as +inactive. The function updates output pore volume and active cells.

+
+
Parameters:
+
+
dck

Deck configuration containing source properties and output dimensions.

+
+
vicinity

Selection and bounds created by create_vicinity_maps().

+
+
modified_deck

Deck lines updated with generated property includes.

+
+
+
+
Returns:
+
+
generated_files

Names of the written include files.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • vicinity (VicinityMaps)

  • +
  • modified_deck (list[str])

  • +
+
+
Return type:
+

list[str]

+
+
+
+ +
+
+extract_vicinity_grid(dck, vicinity)[source]#
+

Extract and write the selected corner-point subgrid.

+
+
Parameters:
+
+
dck

Deck configuration containing source geometry and axis mappings.

+
+
vicinity

Inclusive bounds of the selected submodel.

+
+
+
+
Parameters:
+
+
+
Return type:
+

None

+
+
+
+ +
+
+_map_south_boundary(dck, vicinity, layer_index, original_layer, column_offset, row_offset, trailing_columns)[source]#
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • vicinity (VicinityMaps)

  • +
  • layer_index (int)

  • +
  • original_layer (int)

  • +
  • column_offset (int)

  • +
  • row_offset (int)

  • +
  • trailing_columns (int)

  • +
+
+
Return type:
+

_BoundaryMapping

+
+
+
+ +
+
+_map_north_boundary(dck, vicinity, layer_index, original_layer, column_offset, trailing_rows, trailing_columns)[source]#
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • vicinity (VicinityMaps)

  • +
  • layer_index (int)

  • +
  • original_layer (int)

  • +
  • column_offset (int)

  • +
  • trailing_rows (int)

  • +
  • trailing_columns (int)

  • +
+
+
Return type:
+

_BoundaryMapping

+
+
+
+ +
+
+_map_east_boundary(dck, vicinity, layer_index, original_layer, column_offset, row_offset, trailing_rows)[source]#
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • vicinity (VicinityMaps)

  • +
  • layer_index (int)

  • +
  • original_layer (int)

  • +
  • column_offset (int)

  • +
  • row_offset (int)

  • +
  • trailing_rows (int)

  • +
+
+
Return type:
+

_BoundaryMapping

+
+
+
+ +
+
+_get_well_completions_for_vicinity(dck, optvic)[source]#
+

Collect zero-based completions for a selected well.

+
+
Parameters:
+
+
dck

Deck configuration identifying the source DATA file.

+
+
optvic

Well name from the vicinity specification.

+
+
+
+
Returns:
+
+
list[list[int]]

Completion coordinates in [i, j, k] order.

+
+
+
+
Parameters:
+

dck (ConfigViaDeck)

+
+
Return type:
+

list

+
+
+
+ +
+
+_map_west_boundary(dck, vicinity, layer_index, original_layer, row_offset, trailing_rows, trailing_columns)[source]#
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • vicinity (VicinityMaps)

  • +
  • layer_index (int)

  • +
  • original_layer (int)

  • +
  • row_offset (int)

  • +
  • trailing_rows (int)

  • +
  • trailing_columns (int)

  • +
+
+
Return type:
+

_BoundaryMapping

+
+
+
+ +
+
+_corner_pore_volumes(dck, vicinity, original_layer)[source]#
+

Calculate excluded pore volume in the four layer corners.

+
+
Returns:
+
+
southwest, southeast, northwest, northeast

Corner pore-volume totals for the selected original layer.

+
+
+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • vicinity (VicinityMaps)

  • +
  • original_layer (int)

  • +
+
+
Return type:
+

tuple[float, float, float, float]

+
+
+
+ +
+
+_apply_layer_pore_volume_correction(dck, vicinity, layer_index, column_offset, row_offset, trailing_columns, trailing_rows, south, north, east, west, corner_porv)[source]#
+
+
Parameters:
+
+
+
Return type:
+

None

+
+
+
+ +
+
+_find_nearest_active_corner_cell(dck, layer_index, corner_i, corner_j)[source]#
+

Return the nearest active cell to a corner in a layer.

+
+
Parameters:
+
    +
  • dck (ConfigViaDeck)

  • +
  • layer_index (int)

  • +
  • corner_i (int)

  • +
  • corner_j (int)

  • +
+
+
Return type:
+

int

+
+
+
+ +
+
+_distribute_vertical_pore_volume(dck, vicinity)[source]#
+

Distribute pore volume excluded above and below the submodel.

+
+
Parameters:
+
+
dck

Deck configuration whose output_porv is updated.

+
+
vicinity

Selection bounds and correction settings.

+
+
+
+
Parameters:
+
+
+
Return type:
+

None

+
+
+
+ +
+
+apply_boundary_pore_volume_correction(dck, vicinity)[source]#
+

Map pore volume excluded from the submodel onto active cells.

+

The correction strategy is selected by dck.pore_volume_correction. Depending +on the chosen method, excluded pore volume is assigned to corresponding +boundary cells, nearest corner cells, all boundary cells, or all active cells.

+
+
Parameters:
+
+
dck

Deck configuration whose output_porv is updated.

+
+
vicinity

Selection bounds and pore-volume mapping arrays.

+
+
+
+
Parameters:
+
+
+
Return type:
+

None

+
+
+
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/command-line.html b/docs/command-line.html new file mode 100644 index 0000000..343b9fd --- /dev/null +++ b/docs/command-line.html @@ -0,0 +1,707 @@ + + + + + + + + + + + Command-line reference — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Command-line reference#

+

A pycopm command selects a generic OPM Flow .DATA deck or a legacy TOML +configuration:

+
pycopm -i INPUT [OPTIONS]
+
+
+

The reference is organized by task. On each child page, every flag is an +explicit subsection heading, so the flags appear in the right-hand page table +of contents. The Sphinx option directives remain in place for stable +cross-references.

+
+

Note

+

Generic deck input is the actively developed workflow. All options apply to +.DATA decks. Only -i, -o, -f, and -precision apply to +legacy TOML files.

+
+ +
+

Common commands#

+
pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output
+pycopm -i MODEL.DATA -z 1:10,11:15 -a min,mode -p 1 -m all
+pycopm -i MODEL.DATA -v 'WELLN box [-1,1] [-2,2] [0,3]' -p 1 -m all
+pycopm -i MODEL.DATA -d 'rotatexy 45' -m all
+pycopm -i MODEL6.DATA -z 1:4 -dual 'poro <= 0.1, vertical TF = 0' -m all
+
+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/configuration/complete-reference.html b/docs/configuration/complete-reference.html new file mode 100644 index 0000000..706c9c7 --- /dev/null +++ b/docs/configuration/complete-reference.html @@ -0,0 +1,773 @@ + + + + + + + + + + + Complete legacy configuration reference — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Complete legacy configuration reference#

+

This page preserves the complete original TOML documentation, including the full arrays, parameter explanations, ERT guidance, figures, and tips.

+
+

Note

+

The configuration files allow to set the integrated studies (coarsening and history matching) +only for the drogon and norne model. To use pycopm in any given OPM Flow geological model +to generate modified files (coarsening, refinement, submodels, and transformations), this can +be achieved without a configuration file, but setting the parameters via command lines (see +the Command-line reference or run pycopm -h for the definition of the argument options, as well as +the examples in Examples.)

+
+

Here we use as an example one of the configuration files used in the tests +(see input.toml). +The first input parameter is:

+
1# Set mpirun, the full path to the flow executable, and simulator flags (except --output-dir)
+2flow_command = "flow --newton-min-iterations=1"
+
+
+

If flow is not in your path, then write the full path to the executable, as well as adding mpirun +if this is supported in your machine (e.g., flow_command = "mpirun -np 8 /Users/dmar/Github/opm/build/opm-simulators/bin/flow --newton-min-iterations=1").

+

The next entries define the following parameters:

+
 4# Set the model parameters
+ 5model_name = "norne"  # Geological model (norne or drogon)
+ 6execution_mode = "single-run" # Mode to run (single-run, files, or ert)
+ 7x_coarsening = [0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0] # Array of x-coarsening
+ 8y_coarsening = [0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0] # Array of y-coarsening
+ 9z_coarsening = [0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0] # Array of z-coarsening
+10ensemble_size = 2 # Number of ensembles
+11max_parallel_realizations = 2 # Maximum number of ensembles running in parallel
+12max_realization_runtime_seconds = 600 # Maximum runtime in seconds of a realization. A value of 0 means unlimited runtime.
+13min_successful_realizations = 2 # Minimum number of realizations that must have succeeded for the simulation to be regarded as a success.
+14random_seed = 7 # Set a specific seed for reproducibility. A value of 0 means no seed.
+15saturation_function_method = 0 # Select which coarser deck to use: 0 -> default one or 1 -> LET sat functions
+16satnum_generation_method = 1 # For norne: for the LET coarser deck, select: 0 -> SATNUM=1, 1 -> SATNUM is computed from Sandve et al 2022, 2 -> #SATNUM=#Cells.
+17pore_volume_correction = 0 # 0 -> no corrections for lost porv, 1 -> correct it on all cells
+18initialization_method = 0 # Initialization 0 -> Equil 1->INIT from fine-scale
+19observation_relative_errors = [0.1,0.1,0.1] # Error WWPR, WOPR, and WGPR
+20observation_minimum_errors = [100,100,100000] # Minimum error of WWPR, WOPR, and WGPR
+21history_matching_end_date = 2005-03-01 # Last date to HM
+22cleanup_file_suffixes = ["PRT"] # Delete files with this suffix to save storage
+
+
+

The single-run mode results in Flow running only one simulation and the information shown in +the terminal is the one from the Flow executable instead of the ERT one. The ert option calls the ERT +executable and the command values are given after in the same input file which are discussed later. The +files option only writes the needed input files for ERT.

+

The x-, y, and z-coarsening inputs define which pillars are removed (value 2) in the x, y, and z direction respectively.

+
+../_images/index.png +
+
+../_images/index_plopm.png +
+

I, J, and K cell index in the standard Norne data set (top figures from ResInsight and bottom figures from plopm, see/run docs_configs_views.sh).#

+
+
+

Values of 0 do not remove the pilar.

+
+

Tip

+

The 0 values are used to keep pillars in order to honor the main Norne shape. Then from the +example provided here, change the 2's to 0's to add back the pillars.

+
+

The remaining lines define the number of ensembles, maximum number of ensembles running in parallel, the maximum runtime in seconds to stop a realization +(0 means unlimited time), the minimum number of realizations for the simulation to be regarded as a success, +the random seed for reproducibility (0 means no seed), to use the saturation +functions from opm-tests norne or to use the LET saturation functions, defining if each of the coarser cells is +considered as only one region, regions as in Sandve et al 2022 or as a different SATNUM region, and the cporv entry +sets if the ntg and poro properties on the cells on the boundary are modified to account for lost PV respect to the +default simulation or if the porosity of all cells is modified to match this value. The last entry set the type of files to +delete after each realization is completed to save memory.

+

If the ert option is activated, then the following input:

+
25# Set the command line for the ert executable (gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda) and flags
+26ert_arguments = "es_mda --weights 1"
+
+
+

sets the type of ert option and command flags. Currently, the options supported are gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda. +Confer to the ERT documentation for a full description of these options.

+

The LET saturation function parameters for each of the coarsened cells are given in the following entry:

+
28# Properties LET saturation functions: name, value, use dist in hm?, dist, distpara, distpara
+29let_parameters = [["lw", 3, 1, "UNIFORM", 1.1,   5],
+30["ew",        1, 1, "UNIFORM", -1,    2],
+31["tw",        3, 1, "UNIFORM", 1.1,   5],
+32["lo",        3, 1, "UNIFORM", 1.1,   5],
+33["eo",        1, 1, "UNIFORM", -1,    2],
+34["to",        3, 1, "UNIFORM", 1.1,   5],
+35["lg",        3, 1, "UNIFORM", 1.1,   5],
+36["eg",        1, 1, "UNIFORM", -1,    2],
+37["tg",        3, 1, "UNIFORM", 1.1,   5],
+38["log",       3, 1, "UNIFORM", 1.1,   5],
+39["eog",       1, 1, "UNIFORM", -1,    2],
+40["tog",       1, 1, "UNIFORM", 1.1,   5],
+41["lmlto",   1.5, 1, "UNIFORM", 1,     2],
+42["emlto",     1, 1, "UNIFORM", 0.9, 2.1],
+43["tmlto",   1.5, 1, "UNIFORM", 1,     2],
+44["lmltg",   1.5, 1, "UNIFORM", 1,     2],
+45["emltg",     1, 1, "UNIFORM", 0.9, 2.1],
+46["tmltg",   1.5, 1, "UNIFORM",   1,   2]]
+
+
+

The 'use dist in hm?' defines if the property will be history match (1) or the provided value in the second entry will be used (0).

+

Finally, we set if the permeabilities will be considered for the hm:

+
48# Coarser rock properties: name, use dist in hm?, coarsing approach (max or mean)
+49rock_property_settings = [["PERMX", 1, "max"],
+50["PERMY",         0, "max"],
+51["PERMZ",         0, "max"]]
+
+
+

If option 1 is selected, then the distributions are UNIFORM with interval values of [perm_min, perm_max], corresponding to the +minimum and maximum values in each of the coarser cells.

+
+

Tip

+

By setting the "mode = 'files'" in the toml configuration file, only the needed files to run a history matching using +ERT are generated. Then one can inspect those files and do additional modifications before running the history matching by +calling directly the ert executable. If you are not familiar with the format of the files to use ERT, visit +the ERT documentation.

+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/configuration_file.html b/docs/configuration_file.html index 6f4612a..f1afb8f 100644 --- a/docs/configuration_file.html +++ b/docs/configuration_file.html @@ -1,233 +1,666 @@ - - - - - - - Configuration file — pycopm documentation - - - - - - - - - - - + + + + + + + + + Legacy TOML configuration — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + +
+ + +
+
+ +
+ + + - + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/contributing.html b/docs/contributing.html index 84efd54..855e46c 100644 --- a/docs/contributing.html +++ b/docs/contributing.html @@ -1,99 +1,543 @@ - - - - - - - Contributing — pycopm documentation - - - - - - - - - - - + + + + + + + + + Contributing — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + +
+ + + + + + +
- - -
- -
-
-
-
    -
  • - -
  • - View page source -
  • + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + + +
    -
    -
    - + + + + + +
    +
    -

    Contributing

    +

    Contributing#

    Contributions are more than welcome using the fork and pull request approach 🙂 (if you are not familiar with this approach, please visit GitHub Docs PRs for an extended documentation about collaborating with pull request; also, looking at previous merged pull requests helps to get familiar with this).

    -

    Ground Rules

    +

    Ground Rules#

    • We use Black code formatting

    • We use Pylint and Ruff code analysis tools

    • @@ -101,7 +545,7 @@

      Ground Rules -

      Contribute to the software

      +

      Contribute to the software#

      1. Work on your own fork of the main repo

      2. In the main repo execute:

        @@ -120,7 +564,7 @@

        Contribute to the softwareTip

        See the ci_pycopm_ubuntu.yml script and the Actions for installation of pycopm, OPM Flow (binary packages), and dependencies, as well as the execution of the seven previous steps in Ubuntu 26.04 using Python 3.14. For macOS users, see the ci_pycopm_macos.yml script and the OPM-Flow_macOS Actions for installation of pycopm, OPM Flow (source build), and dependencies, as well as running the tests and the hello world example in macOS 26 using Python3.14. -Note that if you do not add the directory containing the OPM Flow executable to your system’s PATH environment variable (e.g., export PATH=$PATH:/Users/yourname/pycopm/build/opm-simulators/bin), then you can pass this in the execution of the tests and pycopm using the flags -f/--flow (see this script).

        +Note that if you do not add the directory containing the OPM Flow executable to your system's PATH environment variable (e.g., export PATH=$PATH:/Users/yourname/pycopm/build/opm-simulators/bin), then you can pass this in the execution of the tests and pycopm using the flags -f/--flow (see this script).

    @@ -130,14 +574,14 @@

    Contribute to the software -

    Report issues or problems

    +

    Report issues or problems#

    1. Issues or problems can be raised by creating a new issue in the repository GitHub page (if you are not familiar with this approach, please visit GitHub Docs Issues)

    2. We will try to answer as soon as possible, but also any user is more than welcome to answer

-

Seek support

+

Seek support#

  1. The preferred approach to seek support is to raise an Issue as described in the previous lines

  2. We will try to answer as soon as possible, but also any user is more than welcome to answer

  3. @@ -149,34 +593,122 @@

    Seek support - - -

+ + + + + + + + + + + + + +
+ + + +
+ + + +
+ +
+ + + + + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/examples.html b/docs/examples.html index 1d21d9c..e042fba 100644 --- a/docs/examples.html +++ b/docs/examples.html @@ -1,101 +1,555 @@ - - - - - - - Examples — pycopm documentation - - - - - - - - - - - + + + + + + + + + Examples — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + - -
- - -
- -
-
-
-
    -
  • - -
  • - View page source -
  • + +
    + + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + +
    -
    -
    - +
    + + + + + +
    +
    -

    Examples

    +

    Examples#

    +

    Deck-based examples are the recommended and actively developed path. The legacy configuration example is retained for existing Norne and Drogon studies.

    +
    +

    Warning

    +

    For new applications, start with Hello World: generic deck workflow and the deck-based examples. TOML workflows are legacy and model-specific.

    +
    +
    +

    Visualization and reproducibility#

    For additional examples demonstrating the applicability of pycopm, see the tests.

    ResInsight and plopm are used for the visualization of the results.

    @@ -121,314 +575,216 @@

    Examples

    -
    -

    Via configuration files

    -

    The examples folder contains configuration files -to perform HM studies in drogon and norne using ERT. For example, by executing inside the example folder for drogon:

    -
    # From inside the main pycopm folder
    -cd examples/configurations/drogon
    -pycopm -i input.toml -o drogon_coarser
    -
    -
    -

    The following are the drogon model from opm-tests and coarsened model generated using pycopm using ResInsight for the visualization:

    -
    -_images/drogon_coarser.png -
    -
    -_images/drogon_coarser_plopm.png -
    -

    Initial oil saturation from the input (left) and coarsened (right) models (top figures using ResInsight and bottom figures using plopm, see/run docs_via_config_drogon.sh).

    -
    -
    -
    -

    Note

    -

    For Drogon, a scored is printed after the run to compared the error to the results available at -https://webviz-subsurface-example.azurewebsites.net/history-match. While input.toml -only runs one HM iteration with two ensemble members that is used in testing pycopm, hm.toml runs a history matching -with a better score (i.e., less error compare to the observation data). This configuration file is also an example of how to use mpi to run Flow built from source -(set the flow path to your flow location; if you do not have mpi, you can remove it and still run the example).

    -
    -
    -

    Via OPM Flow decks

    +
    +

    Deck workflow scope#

    The current development of pycopm focuses on creating tailored models (grid refinement, grid coarsening, submodels, and transformations) by using input decks. While in the Hello world example these four different options are demonstrated, for the latter examples the focus is on the grid coarsening functionality, and the SPE10 also shows the submodel functionality.

    -
    -

    Hello world

    -

    For the HELLO_WORLD.DATA deck, by executing:

    -
    # From inside the main pycopm folder
    -cd examples/decks
    -pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output
    -
    +
    +
    +
    +
    + +
    +
    +Legacy configuration workflows
    +

    Existing Norne and Drogon TOML and ERT studies.

    -
    -

    Note

    -

    If the folder to flow is not added to your path, then pass the full path to the flow executable using the flag -f /path/to/flow.

    -
    -

    Using plopm, then we can visualize the generated files in the output folder:

    -
    -_images/hello_world_1.png -
    -

    Dry run from the input cloned deck (left) and (right) coarsened model. Adding the flag -p 1 would add the remove pore volume to the neighbouring cells (figures using plopm, see/run docs_via_deck_hello_world.sh).

    -
    -
    -

    As mentioned above, if you do not have ResInsight, then to visualize the results in ParaView run

    -
    flow HELLO_WORLD.DATA --enable-vtk-output=true
    -flow HELLO_WORLD_PYCOPM.DATA --enable-vtk-output=true
    -
    +Legacy configuration workflows
    -

    To make active the coarsened cell where there is only one active cell, this can be achieved by:

    -
    pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -a max
    -
    +
    +
    + +
    +
    +Smeaheia
    +

    Coarsen the Smeaheia simulation model.

    -
    -_images/hello_world_2.png -
    -

    Dry run from the input cloned deck (left) and (right) coarsened model. The region numbers by default are given by the mode, e.g., use the flag -n max to keep the maximum integer (figures using plopm, see/run docs_via_deck_hello_world.sh).

    -
    -
    -

    As described in the theory, pycopm can be not only used for grid coarsening, but also to apply grid refinements, submodels, and transformations. -Then, with the following commands first we substract a submodel around the isolated grid cell proyecting the outside pore volume on the boundaries, after -we apply a grid refinement on the cells in the middle x and y location, and finally we rotate the model 45 degrees.

    -
    pycopm -i HELLO_WORLD.DATA -v 'xypolygon [4,8.5] [4,16.5] [11.5,16.5] [11.5,8.5] [4,8.5]' -p 1 -m all
    -pycopm -i HELLO_WORLD_PYCOPM.DATA -rx 0,0,0,2,0,0,0 -ry 0,0,0,2,0,0,0 -m all
    -pycopm -i HELLO_WORLD_PYCOPM_PYCOPM.DATA -d 'rotatexy 45' -m all
    -
    +Smeaheia
    -
    -_images/hello_world_3.png -
    -

    Extracted region with the projected pore volumes (bottom left), refinement around the center cells (top right), and rotation (bottom right). -The text in the legends highlight that the pore volume is conserved (35.58) and the number of active cells is reduced from 351 to 25 in the -submodel and after increased to 41 due to the grid refinement (figures using plopm, see/run docs_via_deck_hello_world.sh).

    -
    -
    -
    -

    Note

    -

    To write the cell values for the SOLUTION section instead of using the EQUIL keyword, this can be achieved by the flag -explicit 1; the -only requirement is that the EQUIL keyword needs to be in the main input DATA file and no via INCLUDE files.

    +
    +
    + +
    +
    +Drogon
    +

    Prepare, coarsen, conserve fluids in place, compare, and animate Drogon.

    -
    -
    -

    Smeaheia

    -

    By downloading the Smeaheia simulation model (dataset part Simulation models), -then:

    -
    # From the download folders
    -cd Simulation_Models/data
    -pycopm -c 5,4,1 -a min -m all -i Statoil_Feasibility_sim_model_with_depletion_KROSS_INJ_SECTOR_20.DATA -o .
    -
    +Drogon
    -

    will generate a coarser model five times in the x direction and four in the y direction, where the coarse cell is -made inactive if at least one cell is inactive (-a min).

    -

    We use our plopm friend to generate PNG figures:

    -
    plopm -i 'STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PREP_PYCOPM_DRYRUN STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PYCOPM' -s ,,1 -v poro -subfigs 1,2 -save smeaheia -t 'Smeaheia  Coarsened Smeaheia' -delax 1 -xunits km -xformat .0f -yunits km -yformat .0f -d 5,4.5 -suptitle 0 -c cet_rainbow_bgyrm_35_85_c69 -cbsfax 0.2,0.95,0.6,0.02 -cformat .2f
    -
    +
    +
    + +
    +
    +Norne
    +

    Preserve the Norne geometry with directional coarsening.

    -
    -_images/smeaheia.png -
    -

    Top view of porosity values for the (left) original and (right) coarsened model (figures using plopm, see/run docs_via_deck_smeaheia.sh).

    -
    -
    -
-
-

Drogon

-
-

Note

-

In the current implementation of the pycopm tool, the handling of properties that requires definitions of i,j,k indices -(e.g., FAULTS, WELLSPECS) are assumed to be defined in the main .DATA deck. Then, in order to use pycopm for simulation models -where these properties are define via include files, replace those includes in the .DATA deck with the actual content of the include files. -Here are some relevant keywords per deck section that need to be in the main input deck and not via include files:

-

SECTION GRID: MAPAXES, FAULTS, MULTREGT (other keywords like MULTZ, NTG, or definitions/operations for perms and poro can be in included files since -permx, permy, permz, poro, porv, multx, multy, multz are read from the .INIT file)

-

SECTION PROPS: EQUALS, COPY, ADD, and MULTIPLY since this involve i,j,k indices and are applied to properties such as saturation functions parameters that -are still given in the same input format in the generated deck. In addition, SWATINIT if used in the deck, is read from the .INIT file and output for the -modified deck in a new file, then one might need to give the right include path to this special case.

-

SECTION SCHEDULE: All keywords in this section must be in the input deck and no via include viles.

-
-

Following the note above, then by downloading the DROGON model, adding the MAPAXES -to the deck, replacing the lines in DROGON_HIST.DATA for the FAULTS (L127-128) and SCHEDULE (L242-243) with the actual content of those include files, then by executing:

-
pycopm -i DROGON_HIST.DATA -c 1,1,3 -p 1 -q 1 -l C1
-pycopm -i DROGON_HIST_PYCOPM.DATA -c 1,3,1 -p 1 -q 1 -j 2.5 -l C2 -m all
-
+Norne
-

this would generate the following coarsened model:

-
-_images/drogon_generic.png -
-
-_images/drogon_generic_plopm.png -
-

Note that the total pore volume is conserved for the coarsened model (right). The properties of the standard model (left) can be visualized using the DROGON_HIST_PREP_PYCOPM_DRYRUN generated files (top figures using ResInsight and bottom figures using plopm, see/run docs_via_deck_drogon.sh).

-
-
-

Here, we first coarse in the z direction, which reduces the number of cells from 31 to 11, and after we coarse in the y direction. -After trial and error, the jump (-j) is set to 2.5 to avoid generated connections across the faults. For geological models with a lot of -inactive cells and faults, this divide and conquer apporach is recommended, i.e., coarsening first in the z direction and after coarsening -in the x and y directions. Also, we add labels (-l) C1 and C2 to differentiate between the coarse include files. In addition, we use the -flags -p 1 -q 1 to add the remove pore volume to the closest coarser cells and to redistribute the pore volume in the locations with -gas and oil, this results in the coarsened model having the same total pore volume, field gas in place, and practically same oil and water in -place as the input model.

-
-

Note

-

Add to the generated deck the removed include files in the grid section related to the region operations (e.g., -../include/grid/drogon.multregt for this case).

+
+
+ +
+
+SPE10
+

Coarsen SPE10 Model 2 and extract a well-centred submodel.

-

Now, we also show a two times coarsened model in all directions (referring to the previous comment about divide and conquer, for the Drogon model -it seems still ok to do a two times coarsening in one go):

-
pycopm -i DROGON_HIST.DATA -c 2,2,2 -p 1 -q 1 -j 4 -w DROGON_2TIMES_COARSER -m all
-
+SPE10
-

Here, we use the -w flag to give a specific name to the generated coarsened deck, as well as using a higher value of -j to avoid generated connections across the faults.

-
-

Tip

-

To use a different approach from the default ones (see the theory) to coarse one of the properties (e.g., permeabilities), this can -be achieve by the -s flag, e.g., -s pvmean to coarse the permeabilities using a pv-weighted mean. In addition, one could add a different label --l pvweightedperms to identify the generated .INC files with the permeabilities, and rename these files in order to be used in the coarserned model with the rest -of the properties using the default aproaches or a combination of them (e.g., -s max -l maxpermz and keep the maximum values of permz).

-
-

If we run these three models using OPM Flow:

-
flow DROGON_HIST.DATA
-flow DROGON_HIST_PYCOPM_PYCOPM
-flow DROGON_2TIMES_COARSER
-
+
+
+ +
+
+Dual coarsening
+

Compare standard and net/non-net-aware coarsening.

-

then we can compare the summary vectors. To this end, we use our good old friend plopm:

-
plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v 'FOIP,FOPR,TCPU' -tunits y -f 14 -subfigs 2,2 -delax 1 -loc empty,empty,empty,center -d 10,5 -xformat '.1f' -xlnum 6 -ylabel 'sm$^3$  sm$^3$/day  seconds' -t 'Field oil in place  Field oil production rate  Simulation time' -labels 'DROGON  DROGON 3XZ COARSER  DROGON 2XYZ COARSER' -save drogon_pycopm_comparison -yformat '.2e,.0f,.0f'
-
+Dual coarsening
-
-_images/drogon_pycopm_comparison.png -
-

Note that the coarsened models have the same initial field oil in place as the input model. It seems the coarsened properties (e.g., permeabilities) -are good initial inputs to use in a history matching framework (e.g., to history match saturation function parameters), and the lower simulation -time for the coarsened models allow for more ensemble members and more iterations (figures using plopm, see/run docs_via_deck_drogon.sh).

-
-
-

We can also make a nice GIF by executing:

-
plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v sgas -subfigs 1,3  -d 15,11 -cnum 5 -m gif -xlnum 4 -ylnum 4 -dpi 300 -t "DROGON  DROGON 3XZ COARSER  DROGON 2XYZ COARSER" -f 16 -interval 2000 -loop 1 -cformat .2f -cbsfax 0.15,0.93,0.7,0.02 -s ,,1 -rotate '-30' -xunits km -yunits km -xformat .0f -yformat .0f -c cet_rainbow_bgyrm_35_85_c69 -delax 1 -tunits tstep
-
+
+
+ +
+
+Graphical abstract
+

Reproduce the coarsening, submodel, refinement, and transformation illustrations.

-
-_images/sgas.gif -
-

Top view of the Drogon and the two coarsened models (figures using plopm, see/run docs_via_deck_drogon.sh).

-
-
- -
-

Norne

-

By downloading the Norne model (and replacing the needed include files as described in the previous -example, specially the include file ./INCLUDE/BC0407_HIST01122006.SCH at the end of NORNE_ATW2013.DATA to run the example without errors), -then here we create a coarsened model by removing certain pilars in order to keep the main features of the geological model:

-
pycopm -i NORNE_ATW2013.DATA -s pvmean -x 0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0 -y 0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0 -z 0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0 -a min -p 1 -q 1 -m all
-
+Graphical abstract
-

this would generate the following coarsened model:

-
-_images/norne_vec.png -
-
-_images/norne_plopm.png -
-

Top view of Norne for the (top) pore volume and (bottom) gas saturation (top figures using ResInsight and bottom figures using plopm, see/run docs_via_deck_norne.sh).

-
-
- -
-

SPE10

-

By downloading the SPE10_MODEL2 model, then:

-
pycopm -i SPE10_MODEL2.DATA -s pvmean -c 4,8,2 -m all
-
-

generates a coarsened model from ca. 1 million cells to ca. 20 thousands cells.

-
-_images/spe10_model2_coarser.png -
-

Porosity values for the (left) original and (right) coarsened SPE10 model.

-
-
-

To generate a submodel from the coarsened model around the injector ‘INJ’, this can be achieved by executing:

-
pycopm -i SPE10_MODEL2_PYCOPM.DATA -p 1 -v 'INJ diamondxy 5' -m all -w vicinity -l sub -m all
-
-
-_images/vicinity.png -
-
-_images/spe10_plopm.png -
-

Pore volume values for the coarsened and vicinity around the well INJ in the SPE10 model (top figures using ResInsight and bottom figures using plopm (running pycopm with the flag -p 0 instead of -p 1), see/run docs_via_deck_spe10.sh).

-
-
-
-
-

Dual coarsening

-

The flag -dual allows to perform a coarsening by differentiating between net and non-net cells, resulting in -two coarsened grids. For example, using the MODEL6.DATA:

-
pycopm -i MODEL6.DATA -z 1:4 -w STANDARD -l S -t 2 -a max
-pycopm -i MODEL6.DATA -z 1:4 -w DUAL -dual 'poro <= 0.1' -l D -t 2 -a max
-flow MODEL6.DATA
-flow STANDARD.DATA
-flow DUAL.DATA
-plopm -i 'MODEL6 STANDARD DUAL' -v 'pressure - 0pressure' -subfigs 1,3 -delax 1 -cbsfax 0.1,0.95,0.8,0.02 -d 12,4  -suptitle 0 -z 0 -clabel 'Pressure increase end of simulation [bar]' -grid 'black,1e-2'
-
+

The complete former Hello World example is now the Hello World: generic deck workflow.

+
-

This results in the following figure, where the pressure on the most right cell compares better using the dual coarsening than the standard:

-
-_images/dual_pressure-0pressure_i%2C1%2Ck_t2.png -
-

Figures using plopm, see/run docs_via_deck_dual_coarsening.sh.

-
-
-
- -
-

Graphical abstract

-

Here we describe how to generate the geological model ilustrations in the graphical abstract. -These five ilustrations are generated from the DROGON_HIST.DATA model, and the -visualization is achieve using ResInsight.

-
    -
  • Top figure: By running the DROGON_HIST.DATA using opm flow and visaluazing the static property pore volume.

  • -
  • Coarsenings: This corresponds to the generated DROGON_HIST_PYCOPM_PYCOPM.DATA deck in Drogon.

  • -
  • Submodels: pycopm -i DROGON_HIST.DATA -v ‘xypolygon [463739,5931508] [464872,5932123] [464401,5932862] [463284,5932209] [463739,5931508]’ -w SUBMODELS -m all

  • -
  • Refinements: pycopm -i SUBMODELS.DATA -g 2,2,2 -w REFINEMENTS -m all

  • -
  • Transformations: pycopm -i DROGON_HIST.DATA -d ‘rotatexy 45’ -w TRANSFORMATIONS -m all

  • -
-

Note that for ResInsight to show the wells, one needs to run the decks (e.g., flow REFINEMENTS.DATA).

-
-
-
+ + + + +
+ + + +
+ + +
+
+ +
+ + +
- + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/examples/configuration-workflows.html b/docs/examples/configuration-workflows.html new file mode 100644 index 0000000..f2bfee5 --- /dev/null +++ b/docs/examples/configuration-workflows.html @@ -0,0 +1,693 @@ + + + + + + + + + + + Legacy configuration workflows — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Legacy configuration workflows#

+
+

Warning

+

This page describes the retained, model-specific Norne and Drogon TOML workflow. It is not the recommended starting point for new pycopm applications.

+
+

The examples folder contains configuration files +to perform HM studies in drogon and norne using ERT. For example, by executing inside the example folder for drogon:

+
# From inside the main pycopm folder
+cd examples/configurations/drogon
+pycopm -i input.toml -o drogon_coarser
+
+
+

The following are the drogon model from opm-tests and coarsened model generated using pycopm using ResInsight for the visualization:

+
+../_images/drogon_coarser.png +
+
+../_images/drogon_coarser_plopm.png +
+

Initial oil saturation from the input (left) and coarsened (right) models (top figures using ResInsight and bottom figures using plopm, see/run docs_via_config_drogon.sh).#

+
+
+
+

Note

+

For Drogon, a scored is printed after the run to compared the error to the results available at +https://webviz-subsurface-example.azurewebsites.net/history-match. While input.toml +only runs one HM iteration with two ensemble members that is used in testing pycopm, hm.toml runs a history matching +with a better score (i.e., less error compare to the observation data). This configuration file is also an example of how to use mpi to run Flow built from source +(set the flow path to your flow location; if you do not have mpi, you can remove it and still run the example).

+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/examples/drogon.html b/docs/examples/drogon.html new file mode 100644 index 0000000..5ed0cf7 --- /dev/null +++ b/docs/examples/drogon.html @@ -0,0 +1,746 @@ + + + + + + + + + + + Drogon — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Drogon#

+
+

Note

+

In the current implementation of the pycopm tool, the handling of properties that requires definitions of i,j,k indices +(e.g., FAULTS, WELLSPECS) are assumed to be defined in the main .DATA deck. Then, in order to use pycopm for simulation models +where these properties are define via include files, replace those includes in the .DATA deck with the actual content of the include files. +Here are some relevant keywords per deck section that need to be in the main input deck and not via include files:

+

SECTION GRID: MAPAXES, FAULTS, MULTREGT (other keywords like MULTZ, NTG, or definitions/operations for perms and poro can be in included files since +permx, permy, permz, poro, porv, multx, multy, multz are read from the .INIT file)

+

SECTION PROPS: EQUALS, COPY, ADD, and MULTIPLY since this involve i,j,k indices and are applied to properties such as saturation functions parameters that +are still given in the same input format in the generated deck. In addition, SWATINIT if used in the deck, is read from the .INIT file and output for the +modified deck in a new file, then one might need to give the right include path to this special case.

+

SECTION SCHEDULE: All keywords in this section must be in the input deck and no via include viles.

+
+

Following the note above, then by downloading the DROGON model, adding the MAPAXES +to the deck, replacing the lines in DROGON_HIST.DATA for the FAULTS (L127-128) and SCHEDULE (L242-243) with the actual content of those include files, then by executing:

+
pycopm -i DROGON_HIST.DATA -c 1,1,3 -p 1 -q 1 -l C1
+pycopm -i DROGON_HIST_PYCOPM.DATA -c 1,3,1 -p 1 -q 1 -j 2.5 -l C2 -m all
+
+
+

this would generate the following coarsened model:

+
+../_images/drogon_generic.png +
+
+../_images/drogon_generic_plopm.png +
+

Note that the total pore volume is conserved for the coarsened model (right). The properties of the standard model (left) can be visualized using the DROGON_HIST_PREP_PYCOPM_DRYRUN generated files (top figures using ResInsight and bottom figures using plopm, see/run docs_via_deck_drogon.sh).#

+
+
+

Here, we first coarse in the z direction, which reduces the number of cells from 31 to 11, and after we coarse in the y direction. +After trial and error, the jump (-j) is set to 2.5 to avoid generated connections across the faults. For geological models with a lot of +inactive cells and faults, this divide and conquer apporach is recommended, i.e., coarsening first in the z direction and after coarsening +in the x and y directions. Also, we add labels (-l) C1 and C2 to differentiate between the coarse include files. In addition, we use the +flags -p 1 -q 1 to add the remove pore volume to the closest coarser cells and to redistribute the pore volume in the locations with +gas and oil, this results in the coarsened model having the same total pore volume, field gas in place, and practically same oil and water in +place as the input model.

+
+

Note

+

Add to the generated deck the removed include files in the grid section related to the region operations (e.g., +../include/grid/drogon.multregt for this case).

+
+

Now, we also show a two times coarsened model in all directions (referring to the previous comment about divide and conquer, for the Drogon model +it seems still ok to do a two times coarsening in one go):

+
pycopm -i DROGON_HIST.DATA -c 2,2,2 -p 1 -q 1 -j 4 -w DROGON_2TIMES_COARSER -m all
+
+
+

Here, we use the -w flag to give a specific name to the generated coarsened deck, as well as using a higher value of -j to avoid generated connections across the faults.

+
+

Tip

+

To use a different approach from the default ones (see the theory) to coarse one of the properties (e.g., permeabilities), this can +be achieve by the -s flag, e.g., -s pvmean to coarse the permeabilities using a pv-weighted mean. In addition, one could add a different label +-l pvweightedperms to identify the generated .INC files with the permeabilities, and rename these files in order to be used in the coarserned model with the rest +of the properties using the default aproaches or a combination of them (e.g., -s max -l maxpermz and keep the maximum values of permz).

+
+

If we run these three models using OPM Flow:

+
flow DROGON_HIST.DATA
+flow DROGON_HIST_PYCOPM_PYCOPM
+flow DROGON_2TIMES_COARSER
+
+
+

then we can compare the summary vectors. To this end, we use our good old friend plopm:

+
plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v 'FOIP,FOPR,TCPU' -tu y -fz 14 -sg 2,2 -rdl 1 -ll empty,empty,empty,center -fs 10,5 -xf '.1f' -xnt 6 -yl 'sm$^3$  sm$^3$/day  seconds' -t 'Field oil in place  Field oil production rate  Simulation time' -llb 'DROGON  DROGON 3XZ COARSER  DROGON 2XYZ COARSER' -fn drogon_pycopm_comparison -yf '.2e,.0f,.0f'
+
+
+
+../_images/drogon_pycopm_comparison.png +
+

Note that the coarsened models have the same initial field oil in place as the input model. It seems the coarsened properties (e.g., permeabilities) +are good initial inputs to use in a history matching framework (e.g., to history match saturation function parameters), and the lower simulation +time for the coarsened models allow for more ensemble members and more iterations (figures using plopm, see/run docs_via_deck_drogon.sh).#

+
+
+

We can also make a nice GIF by executing:

+
plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v sgas -sg 1,3  -fs 15,11 -cbn 5 -m gif -xnt 4 -ynt 4 -dpi 300 -t "DROGON  DROGON 3XZ COARSER  DROGON 2XYZ COARSER" -fz 16 -gi 2000 -gl 1 -cbf .2f -cbp 0.15,0.93,0.7,0.02 -s ,,1 -rot '-30' -xu km -yu km -xf .0f -yf .0f -c cet_rainbow_bgyrm_35_85_c69 -rdl 1 -tu tstep
+
+
+
+../_images/sgas.gif +
+

Top view of the Drogon and the two coarsened models (figures using plopm, see/run docs_via_deck_drogon.sh).#

+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/examples/dual-coarsening.html b/docs/examples/dual-coarsening.html new file mode 100644 index 0000000..846833c --- /dev/null +++ b/docs/examples/dual-coarsening.html @@ -0,0 +1,681 @@ + + + + + + + + + + + Dual coarsening — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Dual coarsening#

+

The flag -dual allows to perform a coarsening by differentiating between net and non-net cells, resulting in +two coarsened grids. For example, using the MODEL6.DATA:

+
pycopm -i MODEL6.DATA -z 1:4 -w STANDARD -l S -t 2 -a max
+pycopm -i MODEL6.DATA -z 1:4 -w DUAL -dual 'poro <= 0.1' -l D -t 2 -a max
+flow MODEL6.DATA
+flow STANDARD.DATA
+flow DUAL.DATA
+plopm -i 'MODEL6 STANDARD DUAL' -v 'pressure - 0pressure' -sg 1,3 -rdl 1 -cbp 0.1,0.95,0.8,0.02 -fs 12,4  -st 0 -asp 0 -cbl 'Pressure increase end of simulation [bar]' -ge 'black,1e-2'
+
+
+

This results in the following figure, where the pressure on the most right cell compares better using the dual coarsening than the standard:

+
+../_images/dual_pressure-0pressure_i%2C1%2Ck_t2.png +
+

Figures using plopm, see/run docs_via_deck_dual_coarsening.sh.#

+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/examples/graphical-abstract.html b/docs/examples/graphical-abstract.html new file mode 100644 index 0000000..a153f5d --- /dev/null +++ b/docs/examples/graphical-abstract.html @@ -0,0 +1,678 @@ + + + + + + + + + + + Graphical abstract — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Graphical abstract#

+
+../_images/pycopm.png +
+

Here we describe how to generate the geological model ilustrations in the graphical abstract. +These five ilustrations are generated from the DROGON_HIST.DATA model, and the +visualization is achieve using ResInsight.

+
    +
  • Top figure: By running the DROGON_HIST.DATA using opm flow and visaluazing the static property pore volume.

  • +
  • Coarsenings: This corresponds to the generated DROGON_HIST_PYCOPM_PYCOPM.DATA deck in Drogon.

  • +
  • Submodels: pycopm -i DROGON_HIST.DATA -v 'xypolygon [463739,5931508] [464872,5932123] [464401,5932862] [463284,5932209] [463739,5931508]' -w SUBMODELS -m all

  • +
  • Refinements: pycopm -i SUBMODELS.DATA -g 2,2,2 -w REFINEMENTS -m all

  • +
  • Transformations: pycopm -i DROGON_HIST.DATA -d 'rotatexy 45' -w TRANSFORMATIONS -m all

  • +
+

Note that for ResInsight to show the wells, one needs to run the decks (e.g., flow REFINEMENTS.DATA).

+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/examples/norne.html b/docs/examples/norne.html new file mode 100644 index 0000000..ea84e9e --- /dev/null +++ b/docs/examples/norne.html @@ -0,0 +1,680 @@ + + + + + + + + + + + Norne — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Norne#

+

By downloading the Norne model (and replacing the needed include files as described in the previous +example, specially the include file ./INCLUDE/BC0407_HIST01122006.SCH at the end of NORNE_ATW2013.DATA to run the example without errors), +then here we create a coarsened model by removing certain pilars in order to keep the main features of the geological model:

+
pycopm -i NORNE_ATW2013.DATA -s pvmean -x 0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0 -y 0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0 -z 0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0 -a min -p 1 -q 1 -m all
+
+
+

this would generate the following coarsened model:

+
+../_images/norne_vec.png +
+
+../_images/norne_plopm.png +
+

Top view of Norne for the (top) pore volume and (bottom) gas saturation (top figures using ResInsight and bottom figures using plopm, see/run docs_via_deck_norne.sh).#

+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/examples/smeaheia.html b/docs/examples/smeaheia.html new file mode 100644 index 0000000..90f94a4 --- /dev/null +++ b/docs/examples/smeaheia.html @@ -0,0 +1,683 @@ + + + + + + + + + + + Smeaheia — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Smeaheia#

+

By downloading the Smeaheia simulation model (dataset part Simulation models), +then:

+
# From the download folders
+cd Simulation_Models/data
+pycopm -c 5,4,1 -a min -m all -i Statoil_Feasibility_sim_model_with_depletion_KROSS_INJ_SECTOR_20.DATA -o .
+
+
+

will generate a coarser model five times in the x direction and four in the y direction, where the coarse cell is +made inactive if at least one cell is inactive (-a min).

+

We use our plopm friend to generate PNG figures:

+
plopm -i 'STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PREP_PYCOPM_DRYRUN STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PYCOPM' -s ,,1 -v poro -sg 1,2 -fn smeaheia -t 'Smeaheia  Coarsened Smeaheia' -rdl 1 -xu km -xf .0f -yu km -yf .0f -fs 5,4.5 -st 0 -c cet_rainbow_bgyrm_35_85_c69 -cbp 0.2,0.95,0.6,0.02 -cbf .2f
+
+
+
+../_images/smeaheia.png +
+

Top view of porosity values for the (left) original and (right) coarsened model (figures using plopm, see/run docs_via_deck_smeaheia.sh).#

+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/examples/spe10.html b/docs/examples/spe10.html new file mode 100644 index 0000000..b277039 --- /dev/null +++ b/docs/examples/spe10.html @@ -0,0 +1,688 @@ + + + + + + + + + + + SPE10 — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

SPE10#

+

By downloading the SPE10_MODEL2 model, then:

+
pycopm -i SPE10_MODEL2.DATA -s pvmean -c 4,8,2 -m all
+
+
+

generates a coarsened model from ca. 1 million cells to ca. 20 thousands cells.

+
+../_images/spe10_model2_coarser.png +
+

Porosity values for the (left) original and (right) coarsened SPE10 model.#

+
+
+

To generate a submodel from the coarsened model around the injector 'INJ', this can be achieved by executing:

+
pycopm -i SPE10_MODEL2_PYCOPM.DATA -p 1 -v 'INJ diamondxy 5' -m all -w vicinity -l sub -m all
+
+
+
+../_images/vicinity.png +
+
+../_images/spe10_plopm.png +
+

Pore volume values for the coarsened and vicinity around the well INJ in the SPE10 model (top figures using ResInsight and bottom figures using plopm (running pycopm with the flag -p 0 instead of -p 1), see/run docs_via_deck_spe10.sh).#

+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/genindex.html b/docs/genindex.html index 9e646b6..1e6d409 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -1,262 +1,1112 @@ + + + + + + + + + Index — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+ + + + + +
+ + +

Index

+ +
+ Symbols + | _ + | A + | B + | C + | D + | E + | F + | G + | H + | L + | M + | N + | O + | P + | R + | S + | T + | V + | W + | X + | Y + | Z + +
+

Symbols

+ + + +
-
- _ - | A - | B - | C - | D - | E - | F - | G - | H - | L - | M - | N - | O - | P - | R - | S - | T - | V - | W - | X - | Y - | Z - -

_

@@ -264,19 +1114,19 @@

_

A

@@ -284,13 +1134,13 @@

A

B

@@ -298,51 +1148,59 @@

B

C

@@ -350,15 +1208,15 @@

C

D

@@ -366,13 +1224,13 @@

D

E

@@ -380,13 +1238,13 @@

E

F

@@ -394,11 +1252,11 @@

F

G

@@ -406,7 +1264,7 @@

G

H

@@ -414,21 +1272,21 @@

H

L

@@ -436,66 +1294,68 @@

L

M

@@ -503,7 +1363,7 @@

M

N

@@ -511,11 +1371,11 @@

N

O

@@ -523,142 +1383,282 @@

O

P

+ -

R

@@ -666,23 +1666,23 @@

R

S

@@ -690,11 +1690,11 @@

S

T

@@ -702,11 +1702,11 @@

T

V

@@ -714,35 +1714,35 @@

V

W

@@ -750,10 +1750,10 @@

W

X

@@ -762,10 +1762,10 @@

X

Y

@@ -774,10 +1774,10 @@

Y

Z

@@ -785,31 +1785,67 @@

Z

- + + + + + + +
+ +
+
+
+ + + + + + -
- -
- -
-

© Copyright 2024-2026, NORCE Research AS.

+
+ +
+ + +
+ + + + - Built with Sphinx using a - theme - provided by Read the Docs. - +
+
- - - - - + + + + + + + + + + - +
+ \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 702f2b6..6f8e0ee 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,202 +1,782 @@ - - - - - - - Welcome to pycopm’s documentation! — pycopm documentation - - - - - - - - - - - + + + + + + + + + pycopm — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + - - + + + + + - - -
- -
-
-
-
+ +
+ + + + + + + +
- + + + + + + + +
+ + +
+
+ +
+ + + + + +
+ +
+

pycopm#

+

Tailor OPM Flow geological models by coarsening, refining, extracting submodels, and transforming grids.

+

pycopm creates modified, simulation-ready OPM Flow models from existing +.DATA decks. Generic deck workflows are the active development path. +Legacy TOML workflows remain available for existing Norne and Drogon +coarsening and ERT studies.

+
+
+
+
+
+
+ Get started
+

Understand the deck-first workflow and choose a model operation.

+
+Introduction
+
+
+
+
+
+ Install
+

Install pycopm, OPM Flow, and visualization tools.

+
+Installation
+
+
+
+
+
+ Follow the tutorial
+

Work through the complete Hello World deck workflow.

+
+Tutorial
+
+
+
+
+
+ CLI reference
+

Look up options, defaults, formats, and compatibility rules.

+
+Command-line reference
+
+
+
+
+

Quick installation#

+

Install the current development version:

+
pip install git+https://github.com/cssr-tools/pycopm.git
+
+
+

See Installation for supported Python versions, OPM Flow, visualization +tools, virtual environments, platform-specific dependencies, and installation +from source.

+
+
+

Quick start#

+

Coarsen the bundled Hello World deck and run the complete workflow:

+
pycopm -i examples/decks/HELLO_WORLD.DATA -c 5,5,1 -m all -o output
+
+
+

Display the available command-line options:

+
pycopm --help
+
+
+

See Tutorial for a guided workflow, Examples for larger +applications, and Command-line reference for exact syntax, accepted values, +defaults, and option compatibility.

+
+
+

What can pycopm do?#

+
+
+
+
+
+
+Coarsen geological models
+

Remove selected pillars and grid lines, aggregate continuous and +discrete properties, preserve pore volume, and optionally upscale +transmissibilities and non-neighbouring connections.

+
+
+
+
+
+
+
+Refine simulation grids
+

Add cells globally or in selected x, y, and z intervals while mapping +properties, regions, wells, faults, and other indexed deck data to the +refined grid.

+
+
+
+
+
+
+
+Extract focused submodels
+

Select cells by region values, xy polygons, or well-centred box, +diamond, and diamondxy neighbourhoods, with configurable boundary +pore-volume handling.

+
+
+
+
+
+
+
+Transform grid geometry
+

Translate, scale, or rotate OPM Flow grids to reposition models, modify +dimensions, or align geological descriptions from different sources.

+
+
+
+
+
+
+
+Generate simulation-ready files
+

Write modified OPM Flow decks and supporting include files, run +preparation and dry-run stages, and control names, precision, +initialization, and completion rewriting.

+
+
+
+
+
+
+
+Support advanced coarsening workflows
+

Control active-cell aggregation, region and property upscaling, +fluid-in-place correction, fault-related jump handling, and dual +net/non-net coarsening.

+
+
+
+
+
+
+
+Integrate with other tools
+

Use generated decks directly with OPM Flow or adapt them for ERT, PET, +visualization tools, history matching, optimization, proxy modelling, +and uncertainty studies.

+
+
+
+
+
+
+
+Maintain legacy integrated studies
+

Continue existing Norne and Drogon coarsening and ERT workflows through +the retained TOML configuration interface while new development focuses +on generic deck input.

+
+
+
+
+
+
-
-

Indices and tables

-
-
-
- - - -
+ + - + + + + + + + + +
+ +
+ + + + + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/installation.html b/docs/installation.html index 2112ea1..ab733f9 100644 --- a/docs/installation.html +++ b/docs/installation.html @@ -1,98 +1,539 @@ - - - - - - - Installation — pycopm documentation - - - - - - - - - - - + + + + + + + + + Installation — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
- -
-
+ + - - -
- -
-
-
-
+ + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
-
-
- + + + + + +
+
-

Installation

+

Installation#

The following steps work installing the dependencies in Ubuntu via apt-get or in macOS using brew or macports. While using package managers such as Anaconda, Miniforge, or Mamba might work, these are not tested. The supported Python versions are 3.12 to 3.14.

@@ -104,7 +545,7 @@

Installation -

Python package

+

Python package#

To install the pycopm executable from the development version:

-

OPM Flow

+

OPM Flow#

You also need to install:

-

Binary packages

+

Binary packages#

See the downloading and installing OPM Flow online documentation for instructions to install the binary packages in Ubuntu and Red Hat Enterprise Linux, and for other platforms which are supported either via source builds or through running a virtual machine.

@@ -160,11 +601,11 @@

Binary packages -

Source build in Linux/Windows

+

Source build in Linux/Windows#

If you are a Linux user (including the Windows subsystem for Linux, see this link for a nice tutorial for setting Python environments in WSL), then you could try to build Flow (after installing the prerequisites) from the master branches with mpi support by running in the terminal the following lines (which in turn should build flow in the folder ./build/opm-simulators/bin/flow):

-
CURRENT_DIRECTORY="$PWD"
+
CURRENT_DIRECTORY="$PWD"
 
 mkdir build
 
@@ -188,9 +629,9 @@ 

Source build in Linux/Windows -

Brew formula for macOS

+

Brew formula for macOS#

For macOS, there are no available binary packages, so OPM Flow needs to be built from source. Recently, a formula to build flow using brew has -been added in https://github.com/cssr-tools/homebrew-opm. +been added in cssr-tools/homebrew-opm. Then, you can try to install flow (v2026.07 interim release) by simply typing:

brew tap cssr-tools/opm
 brew trust cssr-tools/opm
@@ -204,41 +645,159 @@ 

Source build in Linux/Windows -

Source build in macOS

+

Source build in macOS#

See this repository dedicated to build OPM Flow from source in macOS 26 (GitHub actions), and tested with pycopm

+
+

Verify the installation#

+
pycopm --help
+flow --help
+
+
+
+
+

Next steps#

+ +
- - - - - - + + + + + + + +
+ +
+ + + - + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/introduction.html b/docs/introduction.html index e500be9..3d16dc8 100644 --- a/docs/introduction.html +++ b/docs/introduction.html @@ -1,237 +1,778 @@ - - - - - - - Introduction — pycopm documentation - - - - - - - - - - - + + + + + + + + + Introduction — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + +
+ + + + + + +
- - -
- -
-
-
-
    -
  • - -
  • - View page source -
  • + +
    + + +
    +
    + +
    +
    + +
    + +
    + +
    -
    -
    - -
    -

    Introduction

    -_images/pycopm.png -

    This documentation describes the pycopm tool hosted in https://github.com/cssr-tools/pycopm.

    + +
    + + +
    +
    + + + + + +
    + + Geological-model operations supported by pycopm +
    +

    Introduction#

    +

    pycopm is an open-source tool for creating tailored OPM Flow geological +models from existing simulation decks. It can coarsen and refine grids, +extract submodels, and apply geometric transformations while generating the +files needed to run the modified model with OPM Flow.

    -

    Concept

    -

    User-friendly creation of OPM Flow geological models from provided input decks with options for grid refinement, grid coarsening, submodels, and transformations including scalings, rotations, and translations.

    +

    Concept#

    +

    The main goal is the user-friendly creation of OPM Flow geological models from +provided input decks. The generic deck workflow supports grid refinement, grid +coarsening, submodel extraction, and transformations including scaling, +rotation, and translation.

    +

    The generated decks can be used directly with OPM Flow or adapted for a +history-matching or optimization framework such as ERT or PET. This supports applications +such as proxy modelling, non-conforming grids, upscaling studies, model +comparison, and uncertainty analysis.

    -
    -

    Roadmap

    -

    In the initial development of the pycopm repository, the focus were two available models in opm-tests: norne -and drogon, where the coarsened models were used to perform history matching studies using -the Ensemble based reservoir tool ERT, via a configuration file.

    -

    The current development of pycopm focuses on creating models (i.e., all needed input files to run OPM Flow such as the grid) by only providing the OPM Flow input files -(i.e., avoiding the manual work to create templates as it was done for drogon and norne). This allows for flexibility to adapt the generated decks in your -favourite history matching/optimization tool (e.g., ERT, PET), as well as to -test different approaches in diverse applications (e.g., proxy models, non-conforming grids, upscaling techniques).

    -
    -
    -

    Overview

    -

    The current implementation supports the following executable with the argument options:

    -
    pycopm -i name_of_input_file
    +
    +
    +

    Core workflows#

    +
      +
    • Grid coarsening: Remove selected pillars and grid lines, aggregate model +properties, preserve pore volume, and optionally upscale +transmissibilities.

    • +
    • Grid refinement: Add pillars and grid lines globally or in selected +intervals while mapping grid-dependent properties and deck keywords.

    • +
    • Submodel extraction: Select a region by property values, an xy polygon, +or a well-centred box, diamond, or xy-diamond neighbourhood.

    • +
    • Geometric transformations: Translate, scale, or rotate grid coordinates.

    • +
    • Simulation-ready output: Write a modified OPM Flow deck and supporting +include files for direct simulation or use in another workflow.

    • +
    • Legacy integrated studies: Generate coarsened Norne or Drogon projects +and optionally run OPM Flow or ERT from a TOML configuration.

    • +
    +
    +
    +

    Project evolution#

    +

    The initial development of pycopm focused on two public models from +opm-tests: Norne and Drogon. Coarsened versions of +these models were used in history-matching studies with ERT through a +configuration file.

    +

    The current generic workflow removes the need to prepare model-specific +templates. It creates modified models directly from OPM Flow input decks, +which makes pycopm applicable to a broader range of geological models and +workflows.

    +
    +
    +

    About the project#

    +

    pycopm is developed within the Center for Sustainable Subsurface +Resources (CSSR, project number 331841) and has also been +funded by Expansion of Resources for CO2 Storage on the Horda Platform +(ExpReCCS, project number 336294).

    +

    The source code, issue tracker, and development history are available in the +pycopm GitHub repository. +Contributions are welcome through the fork and pull-request workflow. Request +new features or report problems through the repository issue tracker.

    +
    +
    +

    Citation#

    +

    If pycopm contributes to published work, cite the project paper:

    +
    +

    Landa-Marbán, D. (2026). pycopm: An open-source tool to tailor OPM Flow +geological models. arXiv:2602.11777. +https://doi.org/10.48550/arXiv.2602.11777.

    +
    +
    +
    +

    Where to continue#

    +
      +
    • Complete Installation and verify both pycopm and OPM Flow.

    • +
    • Follow the Hello World tutorial to coarsen, +extract, refine, transform, run, and visualize a small model.

    • +
    • Use Command-line reference for exact flag syntax, accepted values, defaults, and +compatibility rules.

    • +
    • Read Theory and methods for the complete coarsening, refinement, submodel, and +transformation methods.

    • +
    • Browse Examples for Smeaheia, Drogon, Norne, SPE10, dual coarsening, +and reproducible visualization commands.

    • +
    • Review Output folder to understand generated decks, include files, +simulation results, and legacy ERT projects.

    • +
    • Consult Legacy TOML configuration for retained Norne and Drogon TOML studies.

    • +
    • Browse Python API for the Python modules, classes, and functions.

    • +
    • See Contributing to contribute code, run checks, report issues, or seek +support.

    • +
    • Explore Related projects for complementary open-source subsurface tools.

    • +
    -
    -
    -
    + + + + +
+ + +
+ + + + + + + +
+ +
+ + + - + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/modules.html b/docs/modules.html deleted file mode 100644 index 656a28e..0000000 --- a/docs/modules.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - pycopm — pycopm documentation - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
- -
- -
- -
-

© Copyright 2024-2026, NORCE Research AS.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/objects.inv b/docs/objects.inv index a92b845..3409048 100644 Binary files a/docs/objects.inv and b/docs/objects.inv differ diff --git a/docs/options/aggregation.html b/docs/options/aggregation.html new file mode 100644 index 0000000..a02cdc1 --- /dev/null +++ b/docs/options/aggregation.html @@ -0,0 +1,711 @@ + + + + + + + + + + + Property aggregation and conservation — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Property aggregation and conservation#

+

Control active cells, discrete regions, continuous properties, pore +volume, fluids in place, transmissibilities, and fault-related jump handling.

+
+

-a/--active_cell_methods <METHODS>#

+

In coarsening, use 'min', 'max', or 'mode' to scale the actnum, e.g., 'min' makes the new coarser cell inactive if at least one cell is inactive, while 'max' makes it active it at least one cell is active ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:10,11:15 -a min,mode'.

+
+
+

-n/--discrete_aggregation_method <METHODS>#

+

In coarsening, use 'min', 'max', or 'mode' to scale endnum, eqlnum, fipnum, fluxnum, imbnum, miscnum, multnum, opernum, pvtnum, rocknum, and satnum ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:3,4 -a max,min'.

+
+
+

-s/--continuous_aggregation_method <METHODS>#

+

In coarsening, use 'min', 'max', 'mean', or 'pvmean' to scale permx, permy, permz, poro, swatinit, disperc, thconr, and all mult(-)xyz ('' by default, i.e., using the arithmetic average for permx/permy, harmonic average for permz, volume weighted mean for mult(-)xyz, and the pore volume weighted mean ('pvmean') for the rest). . For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:40,41:43,44:50 -a pvmean,max,pvmean'.

+
+
+

-p/--pore_volume_correction <0|1|2|3|4>#

+

In coarsening, set to '1' to add the removed pore volume to the closest coarser cells, while in submodels '1' adds the porv from outside on the boundary of the submodel, '2' adds the corner regions (e.g., below the mini and minj from the input model) to the corners in the submodel, '3' distributes the porv uniformly along the boundary, and '4' distributes it on the whole submodel ('0' by default, i.e., no porv correction).

+
+
+

-q/--correct_fluid_in_place <0|1>#

+

Adjust the pv to the initial FGIP and FOIP from the input deck; use this option only for systems with initial oil, gas, and water, e.g., norne or drogon, but no in Smeaheia ('0' by default, '1' to enable).

+
+
+

-t/--transmissibility_coarsening_method <0|1|2>#

+

In coarsening, write and use upscaled transmissibilities by ('1') armonic averaging and summing the transmissibilities in the corresponding coarsening direction and ('2') scaling the face transmissibily on the coarse faces ('0' by default, i.e., transmissibilities are not used).

+
+
+

-j/--jump_thresholds <VALUES>#

+

In coarsening, tuning parameter to avoid creation of neighbouring connections in the coarsened model where there are discontinuities between cells along the z direction, e.g., around faults ('' by default, i.e., nothing corrected; if need it, try with values of the order of 1).

+
+
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/options/compatibility.html b/docs/options/compatibility.html new file mode 100644 index 0000000..b75f803 --- /dev/null +++ b/docs/options/compatibility.html @@ -0,0 +1,678 @@ + + + + + + + + + + + Option compatibility and validation — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Option compatibility and validation#

+

pycopm validates input formats and rejects incompatible workflows before +processing the model.

+

The implementation validates the following combinations before processing a deck:

+
    +
  • Use either uniform -c or directional -x, -y, and -z coarsening.

  • +
  • Use either uniform -g or directional -rx, -ry, and -rz refinement.

  • +
  • Coarsening and refinement are mutually exclusive.

  • +
  • -v cannot be combined with -d, refinement, or -q 1.

  • +
  • -d cannot be combined with refinement.

  • +
  • Non-default -a and -n, and any -s, -t, -j, or -dual, require coarsening.

  • +
  • -p 1 requires coarsening or submodel extraction. -p 2, -p 3, and -p 4 require submodel extraction.

  • +
  • Multiple aggregation methods require range-based -z coarsening and one method per z group.

  • +
  • An xypolygon must have at least four points and repeat its first point at the end.

  • +
  • Scale factors cannot be zero, jump thresholds must be positive, and requested i,j,k indices must be positive.

  • +
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/options/grid.html b/docs/options/grid.html new file mode 100644 index 0000000..714d96f --- /dev/null +++ b/docs/options/grid.html @@ -0,0 +1,716 @@ + + + + + + + + + + + Grid coarsening and refinement — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Grid coarsening and refinement#

+

Define uniform or directional coarsening and refinement. The detailed +array and range examples from the original introduction are retained below.

+
+

-c/--coarsening <X,Y,Z>#

+

Level of coarsening in the x, y, and z dir (',' by default; either use this flag or the -x, -y, and -z ones).

+
+
+

-x/--x_coarsening <VALUES>#

+

Array of x-coarsening, e.g., if the grid has 6 cells in the x direction, then '0,2,0,2,0,2,0' would generate a coarsened model with 3 cells, while '0,2,2,2,2,2,0' would generate a coarser model with 1 cell, i.e., 0 keeps the pillars while 2 removes them. As an alternative, the range of the cells to coarse can be given separate them by commas, e.g., '1:3,5:6' generates a coarsened model with 3 cells where the cells with the first three and two last i indices are coarsened to one ('' by default),

+
+
+

-y/--y_coarsening <VALUES>#

+

Array of y-coarsening, see the description for -x ('' by default).

+
+
+

-z/--z_coarsening <VALUES>#

+

Array of z-coarsening, see the description for -x ('' by default).

+
+
+

-g/--refinement <X,Y,Z>#

+

Level of grid refinement in the x, y, and z dir ('' by default).

+
+
+

-rx/--x_refinement <VALUES>#

+

Array of x-refinement, e.g., if the grid has 6 cells in the x direction, then '0,1,0,2,0,4' would generate a refined model with 13 cells, while 0,0,0,1,0,0 would generate a refined model with 7 cells ('' by default).

+
+
+

-ry/--y_refinement <VALUES>#

+

Array of y-refinement, see the description for -rx ('' by default).

+
+
+

-rz/--z_refinement <VALUES>#

+

Array of z-refinement, see the description for -rx ('' by default).

+
+
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/options/output.html b/docs/options/output.html new file mode 100644 index 0000000..4914f34 --- /dev/null +++ b/docs/options/output.html @@ -0,0 +1,711 @@ + + + + + + + + + + + Deck rewriting and output — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Deck rewriting and output#

+

Control completion rewriting, generated names, include prefixes, input +encoding, index mapping, explicit initialization, and numeric precision.

+
+

-r/--completion_removal_level <0|1|2>#

+

Remove CONFACT and KH from COMPDAT ('1') and also remove PEQVR ('2') (ITEM 13, the last entry) to compute the well transmisibility connections internally in OPM Flow using the grid properties ('2' by default; set to '0' to not remove).

+
+
+

-w/--output_deck_name <NAME>#

+

Name of the generated deck ('' by default, i.e., the name of the input deck plus _PYCOPM.DATA).

+
+
+

-l/--include_prefix <PREFIX>#

+

Added text before each generated .INC ('PYCOPM_' by default, i.e., the modified porv is saved in PYCOPM_PORV.INC; set to '' to generate PORV.INC, PERMX.INC, etc).

+
+
+

-e/--deck_encoding <ENCODING>#

+

Use 'utf8' or 'ISO-8859-1' encoding to read the deck ('ISO-8859-1' by default).

+
+
+

-ijk/--requested_ijk <I,J,K>#

+

Given i,j,k indices in the input model, return the modified i,j,k corresponding positions ('' by default; if not empty, e.g., '1,2,3', then there will not be generation of modified files, only the i,j,k mapped indices in the terminal).

+
+
+

-explicit/--write_explicit_solution <0|1>#

+

Set to 1 to explicitly write the cell values in the SOLUTION section in the deck ('0' by default).

+
+
+

-precision/--significant_digits <0..15>#

+

Set the number of significant digits used when writing floating-point values, or 0 to use machine precision

+
+
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/options/submodels-transformations.html b/docs/options/submodels-transformations.html new file mode 100644 index 0000000..221569d --- /dev/null +++ b/docs/options/submodels-transformations.html @@ -0,0 +1,691 @@ + + + + + + + + + + + Submodels, transformations, and dual coarsening — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Submodels, transformations, and dual coarsening#

+

Extract submodels by region, polygon, or well neighbourhood; transform +grid coordinates; or separate net and non-net cells during coarsening.

+
+

-v/--vicinity_specification <SPECIFICATION>#

+

The location to extract the sub model which can be assigned by region values, e.g., 'fipnum 2,4' extracts the cells with fipnums equal to 2 or 4, by a polygon given the xy locations in meters, e.g., 'xypolygon [0,0] [30,0] [30,30] [0,0]', or by the name of the well and three different options for the neighbourhood: box, diamond, and diamondxy, where for box the i, j, and k interval around the connections are given, e.g., 'welln box [-1,1] [-2,2] [0,3]' results in a vicinity with 1 pm cell in the x direction, 2 pm cells in the y direction and only 3 cells in the k positive direction, while the diamond considers only the given number of cells around the well connections (e.g., 'welln diamond 2') and diamondxy it is restricted to the xy plane ('' by default).

+
+
+

-d/--grid_transformation <SPECIFICATION>#

+

Options to transform the x,y,z coordinates: 'translate [10,-5,4]' adds the values in meters to the coordinates, 'scale [1,2,3]' multiplies the coordinates by the given values respectively, and 'rotatexy 45' applies a rotation in degrees in the xy plane (rotatexz and rotateyz applies a rotation around the y and x axis respectively) ('' by default).

+
+
+

-dual/--dual_porosity_criterion <CRITERION>#

+

Set the criterium to differentiate net and non-net in coarsening using a static variable, e.g., 'poro <= 0.1' ('' by default).

+
+
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/options/workflow.html b/docs/options/workflow.html new file mode 100644 index 0000000..d16ce71 --- /dev/null +++ b/docs/options/workflow.html @@ -0,0 +1,695 @@ + + + + + + + + + + + Input and workflow — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Input and workflow#

+

Select the input, output location, OPM Flow command, and processing stages.

+
+

-i/--input_deck_path <INPUT>#

+

The base name of the toml configuration file or the name of the deck, e.g., 'DROGON.DATA', ('input.toml' by default).

+
+
+

-o/--output_directory <FOLDER>#

+

The base name of the output folder ('.' by default, i.e., the folder where pycopm is executed).

+
+
+

-f/--flow_command <COMMAND>#

+

OPM Flow path to executable (e.g., /home/pycopm/build/opm-simulators/bin/flow), or just 'flow' if this was installed via binaries or if the folder where flow is built has been added to your path ('flow' by default).

+
+
+

-m/--execution_mode <MODE>#

+

Execute a dry run of the input deck to generate the static properties ('prep'), generate only the modified files ('deck'), only exectute a dry run on the generated model ('dry'), 'prep_deck', 'deck_dry', or do all ('all') ('prep_deck' by default).

+
+
+ + +
+ + + + + + + +
+ + + + + + + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/output_folder.html b/docs/output_folder.html index 6e92012..6517cf0 100644 --- a/docs/output_folder.html +++ b/docs/output_folder.html @@ -1,114 +1,561 @@ - - - - - - - Output folder — pycopm documentation - - - - - - - - - - - + + + + + + + + + Output folder — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + - -
-
+ + + + + + + + + + + + + + + + - - -
- -
-
-
-
    -
  • - -
  • - View page source -
  • + +
    + + +
    +
    + +
    +
    + +
    + +
    + +
    -
    -
    - + +
    + + +
    +
    + + + + + +
    +
    -

    Output folder

    -
    -

    Via configuration files

    +

    Output folder#

    +
    +

    Legacy TOML workflow#

    As described in the introduction, in the early development of pycopm the focus was on history matching studies using ERT for the norne and drogon geological models via a configuration file.

    The following screenshot shows the generated ERT configuration file and folders in the selected output folder after executing pycopm on the drogon model.

    -
    +
    _images/output.png
    -

    (Left) example of generated files after executing pycopm and (right) some of the figures in the postprocessing folder.

    +

    (Left) example of generated files after executing pycopm and (right) some of the figures in the postprocessing folder.#

    The generate ert.ert file can be run directly calling ERT for further studies, and some useful plots and files are generated in the postprocessing folder. The OPM simulation results can be visualized using ResInsight.

    -
    -

    Via an OPM Flow input deck

    +
    + + + + + + + +
    + + + + +
+ + + + + + + + + + +
+ +
+ + + + + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/py-modindex.html b/docs/py-modindex.html index 8803155..34caf33 100644 --- a/docs/py-modindex.html +++ b/docs/py-modindex.html @@ -1,85 +1,494 @@ - - - - - - Python Module Index — pycopm documentation - - - - - - - - - - - + + + + + + + + Python Module Index — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + Ctrl+K + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + +
+ + +
- pycopm + pycopm +
    + pycopm.core
    - pycopm.core + pycopm.core.pycopm
    - pycopm.core.pycopm + pycopm.utils
    - pycopm.utils + pycopm.utils.coarsening
    - pycopm.utils.coarsening + pycopm.utils.files_writer
    - pycopm.utils.files_writer + pycopm.utils.generate_decks
    - pycopm.utils.generate_decks + pycopm.utils.input_values
    - pycopm.utils.input_values + pycopm.utils.parser_deck
    - pycopm.utils.parser_deck + pycopm.utils.refinement
    - pycopm.utils.refinement + pycopm.utils.runs_executer
    - pycopm.utils.runs_executer + pycopm.utils.terminal
    - pycopm.utils.transformation + pycopm.utils.transformation
    - pycopm.utils.vicinity + pycopm.utils.vicinity
-
+ + + + + + +
+ +
+
+
+ +
+ + + +
-
- -
- -
-

© Copyright 2024-2026, NORCE Research AS.

+
+ +
+ + +
+ + + + - Built with Sphinx using a - theme - provided by Read the Docs. - +
+
- - - - - + + + + + + + + + + - + + \ No newline at end of file diff --git a/docs/pycopm.core.html b/docs/pycopm.core.html deleted file mode 100644 index 3c2d2b5..0000000 --- a/docs/pycopm.core.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - pycopm.core package — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.core package

-
-

Submodules

- -
-
-

Module contents

-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.core.pycopm.html b/docs/pycopm.core.pycopm.html deleted file mode 100644 index 60a532e..0000000 --- a/docs/pycopm.core.pycopm.html +++ /dev/null @@ -1,300 +0,0 @@ - - - - - - - - - pycopm.core.pycopm module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.core.pycopm module

-

Command-line entry point and top-level workflow coordination for pycopm.

-

pycopm supports two input workflows:

-
    -
  • OPM .DATA decks can be coarsened, refined, transformed, or reduced to a -submodel.

  • -
  • TOML configurations generate coarsened Norne or Drogon cases and can -optionally run OPM Flow or ERT studies.

  • -
-

This module parses and validates command-line arguments, selects the appropriate -workflow, and coordinates its major processing steps. The numerical and -file-generation details are implemented in the utility modules.

-
-
-pycopm.core.pycopm._check_cmdargs(cmdargs: Namespace) → None
-

Validate command-line arguments and incompatible operations.

-

The checks cover input type, Flow availability, coarsening and refinement -syntax, aggregation methods, vicinity selections, transformations, and -options restricted to particular workflows.

-
-
Parameters:
-

cmdargs – Parsed arguments returned by _parse_arguments().

-
-
Raises:
-

SystemExit – If an argument is invalid or an incompatible combination is requested.

-
-
-
- -
-
-pycopm.core.pycopm._check_flow(flow_cmdargs: str, flow_toml: str, input_file: str) → str
-

Select an available OPM Flow command for a TOML workflow.

-
-
Parameters:
-
    -
  • flow_cmdargs – Flow command supplied through the command line.

  • -
  • flow_toml – Flow command read from the TOML configuration.

  • -
  • input_file – TOML filename used in validation messages.

  • -
-
-
Returns:
-

The selected Flow command.

-
-
Return type:
-

str

-
-
Raises:
-

SystemExit – If neither command identifies a working Flow executable.

-
-
-
- -
-
-pycopm.core.pycopm._parse_arguments(argv: list[str] | None = None) → Namespace
-

Parse supported command-line arguments.

-

Unknown arguments are left unprocessed for compatibility with external -launchers.

-
-
Parameters:
-

argv – Command-line arguments. If omitted, arguments are read from sys.argv.

-
-
Returns:
-

Arguments keyed by their destination names.

-
-
Return type:
-

dict[str, str]

-
-
-
- -
-
-pycopm.core.pycopm.main(argv: list[str] | None = None) → None
-

Run the deck-based or TOML-based pycopm workflow.

-

OPM .DATA decks can be coarsened, refined, transformed, or reduced to -a submodel. TOML configurations generate coarsened Norne or Drogon cases -and can optionally run OPM Flow or ERT studies.

-
-
Parameters:
-
    -
  • argv – Command-line arguments. If omitted, arguments are read from -sys.argv.

  • -
  • -i – Input .DATA deck or TOML configuration file.

  • -
  • --input_deck_path – Input .DATA deck or TOML configuration file.

  • -
  • -o – Directory for generated decks, include files, and simulation results.

  • -
  • --output_directory – Directory for generated decks, include files, and simulation results.

  • -
  • -f – Command or path used to run OPM Flow.

  • -
  • --flow_command – Command or path used to run OPM Flow.

  • -
  • -m – Deck-processing stages to run: prep, deck, dry, -prep_deck, deck_dry, or all.

  • -
  • --execution_mode – Deck-processing stages to run: prep, deck, dry, -prep_deck, deck_dry, or all.

  • -
  • -v – Submodel selection based on region values, an xy polygon, or a -well-centred box, diamond, or diamondxy neighbourhood.

  • -
  • --vicinity_specification – Submodel selection based on region values, an xy polygon, or a -well-centred box, diamond, or diamondxy neighbourhood.

  • -
  • -c – Uniform coarsening factors in the x, y, and z directions.

  • -
  • --coarsening – Uniform coarsening factors in the x, y, and z directions.

  • -
  • -x – Cell-specific coarsening specification along the x axis.

  • -
  • --x_coarsening – Cell-specific coarsening specification along the x axis.

  • -
  • -y – Cell-specific coarsening specification along the y axis.

  • -
  • --y_coarsening – Cell-specific coarsening specification along the y axis.

  • -
  • -z – Cell-specific coarsening specification along the z axis.

  • -
  • --z_coarsening – Cell-specific coarsening specification along the z axis.

  • -
  • -g – Uniform numbers of additional cells along the x, y, and z axes.

  • -
  • --refinement – Uniform numbers of additional cells along the x, y, and z axes.

  • -
  • -rx – Number of additional cells for each original x interval.

  • -
  • --x_refinement – Number of additional cells for each original x interval.

  • -
  • -ry – Number of additional cells for each original y interval.

  • -
  • --y_refinement – Number of additional cells for each original y interval.

  • -
  • -rz – Number of additional cells for each original z interval.

  • -
  • --z_refinement – Number of additional cells for each original z interval.

  • -
  • -a – Aggregation method for active-cell values: min, max, or -mode.

  • -
  • --active_cell_methods – Aggregation method for active-cell values: min, max, or -mode.

  • -
  • -n – Aggregation method for discrete properties: min, max, or -mode.

  • -
  • --discrete_aggregation_method – Aggregation method for discrete properties: min, max, or -mode.

  • -
  • -s – Aggregation method for continuous properties: min, max, -mean, or pore-volume-weighted mean (pvmean). If omitted, -property-specific physical aggregation is used.

  • -
  • --continuous_aggregation_method – Aggregation method for continuous properties: min, max, -mean, or pore-volume-weighted mean (pvmean). If omitted, -property-specific physical aggregation is used.

  • -
  • -p – Pore-volume correction method. The available values are 0 through -4; supported methods depend on the selected workflow.

  • -
  • --pore_volume_correction – Pore-volume correction method. The available values are 0 through -4; supported methods depend on the selected workflow.

  • -
  • -q – Set to 1 to adjust pore volume to match the initial oil and gas in -place of the input model.

  • -
  • --correct_fluid_in_place – Set to 1 to adjust pore volume to match the initial oil and gas in -place of the input model.

  • -
  • -t – Transmissibility coarsening method: 0, 1, or 2.

  • -
  • --transmissibility_coarsening_method – Transmissibility coarsening method: 0, 1, or 2.

  • -
  • -r – Level of COMPDAT data removed after coarsening: 0, 1, or -2.

  • -
  • --completion_removal_level – Level of COMPDAT data removed after coarsening: 0, 1, or -2.

  • -
  • -j – Positive depth-jump thresholds used to prevent unwanted connections -between cells grouped during coarsening.

  • -
  • --jump_thresholds – Positive depth-jump thresholds used to prevent unwanted connections -between cells grouped during coarsening.

  • -
  • -w – Name of the generated OPM deck.

  • -
  • --output_deck_name – Name of the generated OPM deck.

  • -
  • -l – Prefix added to generated include filenames.

  • -
  • --include_prefix – Prefix added to generated include filenames.

  • -
  • -e – Character encoding used to read the input deck: ISO-8859-1 or -utf8.

  • -
  • --deck_encoding – Character encoding used to read the input deck: ISO-8859-1 or -utf8.

  • -
  • -ijk – One-based input-grid i,j,k indices to map to the modified grid.

  • -
  • --requested_ijk – One-based input-grid i,j,k indices to map to the modified grid.

  • -
  • -d – Coordinate transformation: translate [x,y,z], scale [x,y,z], -or rotatexy, rotatexz, or rotateyz followed by an angle -in degrees.

  • -
  • --grid_transformation – Coordinate transformation: translate [x,y,z], scale [x,y,z], -or rotatexy, rotatexz, or rotateyz followed by an angle -in degrees.

  • -
  • -explicit – Set to 1 to write initial solution properties explicitly instead -of retaining EQUIL initialization.

  • -
  • --write_explicit_solution – Set to 1 to write initial solution properties explicitly instead -of retaining EQUIL initialization.

  • -
  • -dual – Static-property criterion used to separate matrix and fracture or -non-net cells during coarsening.

  • -
  • --dual_porosity_criterion – Static-property criterion used to separate matrix and fracture or -non-net cells during coarsening.

  • -
  • -precision – Number of significant digits used when writing floating-point values. -Set to 0 to preserve machine precision.

  • -
  • --significant_digits – Number of significant digits used when writing floating-point values. -Set to 0 to preserve machine precision.

  • -
-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.coarsening.html b/docs/pycopm.utils.coarsening.html deleted file mode 100644 index 0eaa530..0000000 --- a/docs/pycopm.utils.coarsening.html +++ /dev/null @@ -1,516 +0,0 @@ - - - - - - - - - pycopm.utils.coarsening module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.coarsening module

-

Coarsen corner-point grids and aggregate reservoir properties.

-

The module supports deck-based coarsening and the TOML workflows used to -generate reduced Norne and Drogon models.

-
-
-class pycopm.utils.coarsening.CoarseningMaps(x: NDArray, y: NDArray, z: NDArray, cell_groups: NDArray, coarsened_axes: str, matrix_mask: NDArray, vertical_transfer_enabled: bool, reference_to_coarse: list[int] = <factory>, nnc_text: str = 'NNC\n', coarse_tranx: NDArray = <factory>, coarse_trany: NDArray = <factory>, dual_tranx: NDArray = <factory>, dual_trany: NDArray = <factory>, dual_defaults: dict[str, float]=<factory>)
-

Bases: object

-

Store mappings and intermediate values used during coarsening.

-
-
-cell_groups: NDArray
-

One-based coarse-cell identifier for each original cell, flattened in -(z, y, x) order.

-
- -
-
-coarse_tranx: NDArray
-

Horizontal x-direction transmissibilities for the matrix or -single-porosity coarse grid.

-
- -
-
-coarse_trany: NDArray
-

Horizontal y-direction transmissibilities for the matrix or -single-porosity coarse grid.

-
- -
-
-coarsened_axes: str
-

Concatenated names of the coarsened axes, for example "xz".

-
- -
-
-dual_defaults: dict[str, float]
-

Default property values inserted into separator rows of the extended -dual-porosity grid.

-
- -
-
-dual_tranx: NDArray
-

Horizontal x-direction transmissibilities for the fracture continuum of -a dual-porosity grid.

-
- -
-
-dual_trany: NDArray
-

Horizontal y-direction transmissibilities for the fracture continuum of -a dual-porosity grid.

-
- -
-
-matrix_mask: NDArray
-

Per-cell mask separating matrix cells (one) from fracture or non-net -cells (zero) in dual-porosity models.

-
- -
-
-nnc_text: str
-

NNC include-file content accumulated while mapping non-neighbouring and -matrix-fracture connections.

-
- -
-
-reference_to_coarse: list[int]
-

Coarse-cell identifier for each reference-grid cell, populated while -properties are coarsened.

-
- -
-
-vertical_transfer_enabled: bool
-

Whether vertical matrix-fracture transfer connections are retained.

-
- -
-
-x: NDArray
-

Axis array marking boundaries removed by coarsening in the x direction. -Values greater than one identify intervals merged with the preceding -interval.

-
- -
-
-y: NDArray
-

Axis array marking boundaries removed by coarsening in the y direction. -Values greater than one identify intervals merged with the preceding -interval.

-
- -
-
-z: NDArray
-

Axis array marking boundaries removed by coarsening in the z direction. -Values greater than one identify intervals merged with the preceding -interval.

-
- -
- -
-
-pycopm.utils.coarsening._collect_removed_zcorn_indices(dck: ConfigViaDeck, coa_z: NDArray, removal_indices: list[int]) → list[int]
-

Add the ZCORN indices removed by vertical coarsening.

-
- -
-
-pycopm.utils.coarsening._compact_permeability_properties(dck: ConfigViaDeck, permx: NDArray, permy: NDArray, permz: NDArray, modified_deck: list[str]) → None
-

Use COPY and MULTIPLY if PERMY and PERMZ can be generated from PERMX.

-
- -
-
-pycopm.utils.coarsening._find_active_neighbors(dck: ConfigViaDeck, neighbor_indices: list[int], cluster_id: int, distance: int, offset: int, ijk: list) → list[int]
-

Find active neighbouring cells for pore-volume redistribution.

-
- -
-
-pycopm.utils.coarsening._find_include_statement(modified_deck: list[str], include_line: str) → tuple[int, int]
-

Return the list interval containing an INCLUDE statement.

-
- -
-
-pycopm.utils.coarsening._global_index_to_ijk(dck: ConfigViaDeck, global_index: int) → tuple[int, int, int]
-

Return the i, j, and k indices from a zero-based global cell index.

-
- -
-
-pycopm.utils.coarsening._group_maximum_zero_based(values: NDArray, groups: NDArray, number_groups: int) → NDArray
-
- -
-
-pycopm.utils.coarsening._group_minimum_zero_based(values: NDArray, groups: NDArray, number_groups: int) → NDArray
-
- -
-
-pycopm.utils.coarsening._group_sum_zero_based(values: NDArray, groups: NDArray, number_groups: int) → NDArray
-
- -
-
-pycopm.utils.coarsening._grouped_count(values: NDArray, groups: NDArray, size: int | None = None) → NDArray
-

Return the number of non-NaN values for each one-based group.

-
- -
-
-pycopm.utils.coarsening._grouped_first(values: NDArray, groups: NDArray, size: int | None = None) → NDArray
-

Return the first non-NaN value for each one-based group.

-
- -
-
-pycopm.utils.coarsening._grouped_last(values: NDArray, groups: NDArray, size: int | None = None) → NDArray
-

Return the last non-NaN value for each one-based group.

-
- -
-
-pycopm.utils.coarsening._grouped_max(values: NDArray, groups: NDArray, size: int | None = None) → NDArray
-

Return the maximum value for each one-based group, ignoring NaNs.

-
- -
-
-pycopm.utils.coarsening._grouped_mean(values: NDArray, groups: NDArray, size: int | None = None) → NDArray
-

Return the mean value for each one-based group, ignoring NaNs.

-
- -
-
-pycopm.utils.coarsening._grouped_min(values: NDArray, groups: NDArray, size: int | None = None) → NDArray
-

Return the minimum value for each one-based group, ignoring NaNs.

-
- -
-
-pycopm.utils.coarsening._grouped_mode(values: NDArray, group_codes: NDArray, number_groups: int) → NDArray
-

Return the smallest mode for each zero-based group, ignoring NaNs.

-
- -
-
-pycopm.utils.coarsening._grouped_sum(values: NDArray, groups: NDArray, size: int | None = None) → NDArray
-

Return the sum of values for each one-based group.

-
- -
-
-pycopm.utils.coarsening._interleave_dual_property(property_values: NDArray, dual_values: NDArray, nx: int, nz: int, default_value: float = 0) → NDArray
-

Interleave property and dual-property layers with separator rows.

-
- -
-
-pycopm.utils.coarsening._read_satnum(cfg: ConfigViaTOML, actnum: NDArray, nxyz: int, satnum_opm: NDArray) → NDArray
-

Read or generate fine-grid SATNUM values.

-
-
Parameters:
-
    -
  • cfg – TOML configuration controlling the SATNUM source.

  • -
  • actnum – Fine-grid active-cell mask.

  • -
  • nxyz – Number of fine-grid cells.

  • -
  • satnum_opm – SATNUM values read from the reference INIT file.

  • -
-
-
Returns:
-

SATNUM value for every fine-grid cell.

-
-
Return type:
-

NDArray

-
-
-
- -
-
-pycopm.utils.coarsening.build_dual_porosity_grid(dck: ConfigViaDeck, coarsening: CoarseningMaps, cr: NDArray, zc: NDArray) → tuple[NDArray, NDArray]
-

Extend a coarsened grid with a second porosity continuum.

-

The matrix and fracture grids are separated in the j direction, and their -connections are added to coarsening.nnc_text.

-
-
Parameters:
-
    -
  • dck – Deck configuration for the coarsened model.

  • -
  • coarsening – Coarsening data containing continuum masks and transmissibilities.

  • -
  • cr – Coarsened COORD and ZCORN arrays.

  • -
  • zc – Coarsened COORD and ZCORN arrays.

  • -
-
-
Returns:
-

Extended dual-porosity grid arrays.

-
-
Return type:
-

coord, zcorn

-
-
-
- -
-
-pycopm.utils.coarsening.coarsen_and_write_properties(cfg: ConfigViaTOML, coa_map: NDArray) → int
-

Aggregate and write properties for a TOML-generated model.

-
-
Parameters:
-
    -
  • cfg – TOML configuration and reference-case settings.

  • -
  • coa_map – One-based fine-to-coarse cell mapping.

  • -
-
-
Returns:
-

Highest generated SATNUM value, used as the number of saturation tables.

-
-
Return type:
-

int

-
-
-
- -
-
-pycopm.utils.coarsening.coarsen_corner_point_grid(dck: ConfigViaDeck, coarsening: CoarseningMaps) → tuple[NDArray, NDArray]
-

Remove selected pillars and ZCORN surfaces from the grid.

-
-
Parameters:
-
    -
  • dck – Deck configuration containing the original corner-point grid.

  • -
  • coarsening – Axis mappings defining the removed rows, columns, and layers.

  • -
-
-
Returns:
-

Coarsened arrays when dual porosity is enabled; otherwise empty arrays.

-
-
Return type:
-

coord, zcorn

-
-
-
- -
-
-pycopm.utils.coarsening.coarsen_properties(dck: ConfigViaDeck, coarsening: CoarseningMaps, modified_deck: list[str], wellcind: list[int]) → tuple[NDArray, NDArray, NDArray]
-

Aggregate reservoir properties onto the coarsened grid.

-

Continuous properties use their configured or property-specific aggregation; -discrete properties use min, max, or mode. The function writes -property include files and updates output pore volume and active cells.

-
-
Parameters:
-
    -
  • dck – Deck configuration and source INIT or restart properties.

  • -
  • coarsening – Cell groups and masks created by create_coarsening_maps().

  • -
  • modified_deck – Deck lines updated with generated property includes.

  • -
  • wellcind – Coarse-cell indices containing well completions.

  • -
-
-
Returns:
-

Activity summaries and the mask used to remove depth-jump cells.

-
-
Return type:
-

cluster_minimum, cluster_maximum, removal_mask

-
-
-
- -
-
-pycopm.utils.coarsening.create_coarsening_map(cfg: ConfigViaTOML) → NDArray
-

Map each fine-grid cell to a one-based coarse-cell identifier.

-

The output dimensions and original-to-output axis mappings in cfg are also -updated.

-
-
Parameters:
-

cfg – TOML configuration containing the axis coarsening arrays.

-
-
Returns:
-

One-based coarse-cell identifier for every fine-grid cell.

-
-
Return type:
-

NDArray

-
-
-
- -
-
-pycopm.utils.coarsening.create_coarsening_maps(dck: ConfigViaDeck, cmdargs: Namespace) → CoarseningMaps
-

Create axis mappings and assign original cells to coarse cells.

-
-
Parameters:
-
    -
  • dck – Deck configuration whose output dimensions are updated.

  • -
  • cmdargs – Command arguments containing coarsening, x_coarsening, -y_coarsening, and z_coarsening.

  • -
-
-
Returns:
-

Axis mappings, cell groups, and dual-porosity masks.

-
-
Return type:
-

CoarseningMaps

-
-
-
- -
-
-pycopm.utils.coarsening.map_nnc_transmissibilities(dck: ConfigViaDeck, coarsening: CoarseningMaps) → None
-

Map original non-neighbouring transmissibilities to the coarse grid.

-

Connections that become Cartesian neighbours are accumulated in TRANX or -TRANY; remaining connections are written as NNC records.

-
-
Parameters:
-
    -
  • dck – Deck configuration and source NNC data.

  • -
  • coarsening – Coarse mapping updated with transmissibilities and NNC text.

  • -
-
-
-
- -
-
-pycopm.utils.coarsening.redistribute_removed_pore_volume(dck: ConfigViaDeck, con: NDArray, cluster_minimum: NDArray, cluster_maximum: NDArray, removal_mask: NDArray) → None
-

Redistribute pore volume from removed coarse cells.

-

Pore volume is divided among the nearest active neighbours without changing -the total pore volume.

-
-
Parameters:
-
    -
  • dck – Deck configuration whose output_porv is updated.

  • -
  • con – One-based coarse-cell identifier for each original cell.

  • -
  • cluster_minimum – Aggregated activity values used to identify changed clusters.

  • -
  • cluster_maximum – Aggregated activity values used to identify changed clusters.

  • -
  • removal_mask – Mask identifying retained coarse cells.

  • -
-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.files_writer.html b/docs/pycopm.utils.files_writer.html deleted file mode 100644 index 3bc95e0..0000000 --- a/docs/pycopm.utils.files_writer.html +++ /dev/null @@ -1,337 +0,0 @@ - - - - - - - - - pycopm.utils.files_writer module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.files_writer module

-

Write OPM Flow decks, corner-point grids, properties, and ERT files.

-
-
-pycopm.utils.files_writer._identify_removed_pillars(cfg: ConfigViaTOML) → tuple[NDArray, NDArray]
-

Find COORD and ZCORN indices removed by TOML coarsening.

-
-
Parameters:
-

cfg – TOML configuration containing axis coarsening arrays.

-
-
Returns:
-

Indices to remove from the reference grid arrays.

-
-
Return type:
-

coord_indices, zcorn_indices

-
-
-
- -
-
-pycopm.utils.files_writer._render_template(template_path: Path, output_path: Path, **variables: Any) → None
-

Render a Mako template to a UTF-8 file.

-
-
Parameters:
-
    -
  • template_path – Source template path.

  • -
  • output_path – Destination path.

  • -
  • **variables – Values passed to the template.

  • -
-
-
-
- -
-
-pycopm.utils.files_writer._write_ert_files(cfg: ConfigViaTOML, number_tables: int) → None
-

Write the ERT configuration, parameter, job, and observation files.

-
- -
-
-pycopm.utils.files_writer._write_grid_files(cfg: ConfigViaTOML) → None
-

Write the OPM grid-related files.

-
- -
-
-pycopm.utils.files_writer._write_let_tables(cfg: ConfigViaTOML, number_tables: int) → None
-

Write the LET saturation-function tables.

-
- -
-
-pycopm.utils.files_writer.format_opm_compact_values(v: NDArray) → list[str]
-

Convert values to OPM repeated-value notation.

-
-
Parameters:
-

v – One-dimensional values to compact.

-
-
Returns:
-

Values formatted as n*value where consecutive values repeat.

-
-
Return type:
-

list[str]

-
-
-
- -
-
-pycopm.utils.files_writer.round_like_e(values: NDArray | list, significant_digits: int) → NDArray
-

Round values to a number of significant digits.

-
-
Parameters:
-
    -
  • values – Values to round.

  • -
  • significant_digits – Number of significant digits, or zero to preserve machine precision.

  • -
-
-
Returns:
-

Rounded values with the original shape.

-
-
Return type:
-

NDArray

-
-
-
- -
-
-pycopm.utils.files_writer.write_coarsened_model_files(cfg: ConfigViaTOML, number_tables: int) → None
-

Write grid, deck, schedule, job, observation, and ERT files.

-
-
Parameters:
-
    -
  • cfg – TOML configuration for the generated project.

  • -
  • number_tables – Number of saturation-function tables to generate.

  • -
-
-
-
- -
-
-pycopm.utils.files_writer.write_compact_property_file(preprocessing_path: Path, property_name: str, values: NDArray, num_dig) → None
-

Write one compacted OPM property to a file.

-
- -
-
-pycopm.utils.files_writer.write_dual_properties(dck: ConfigViaDeck, coarsening, number_values: int, modified_deck: list[str]) → None
-

Finalize property files for a dual-porosity grid.

-
-
Parameters:
-
    -
  • dck – Deck configuration and generated property names.

  • -
  • coarsening – Coarsening data containing matrix and fracture transmissibilities.

  • -
  • number_values – Number of cells in the extended dual grid.

  • -
  • modified_deck – Deck lines that may receive inlined properties.

  • -
-
-
-
- -
-
-pycopm.utils.files_writer.write_grid(cfg: ConfigViaDeck | ConfigViaTOML, cr: NDArray, zc: NDArray, dual: bool) → None
-

Write a corner-point grid in GRDECL syntax.

-
-
Parameters:
-
    -
  • cfg – Deck or TOML configuration containing output dimensions and precision.

  • -
  • cr – Flattened COORD and ZCORN arrays.

  • -
  • zc – Flattened COORD and ZCORN arrays.

  • -
  • dual – Extend the j dimension for a dual-porosity grid.

  • -
-
-
-
- -
-
-pycopm.utils.files_writer.write_include(output_path: Path, text: str) → None
-

Write text to an OPM include file with the pycopm header.

-
-
Parameters:
-
    -
  • output_path – Destination include path.

  • -
  • text – OPM deck text written after the header.

  • -
-
-
-
- -
-
-pycopm.utils.files_writer.write_porv(dck: ConfigViaDeck, modified_deck: list[str]) → None
-

Write the OPM grid-related files.

-
- -
-
-pycopm.utils.files_writer.write_property(output_path: Path, keyword: str, values: NDArray, num_dig) → None
-

Write one compact OPM property.

-
-
Parameters:
-
    -
  • output_path – Destination include path.

  • -
  • keyword – OPM property keyword.

  • -
  • values – Property values in global-cell order.

  • -
  • num_dig – Number of significant digits.

  • -
-
-
-
- -
-
-pycopm.utils.files_writer.write_property_inc(dck: ConfigViaDeck, property_name: str, property_values: NDArray, number_values: int, modified_deck: list[str], allow_inline: bool = False, file_suffix: str = '') → bool
-

Write a property include or inline a constant property.

-
-
Parameters:
-
    -
  • dck – Deck configuration controlling paths, prefixes, and precision.

  • -
  • property_name – OPM property name.

  • -
  • property_values – Values in output-grid order.

  • -
  • number_values – Expected number of output values.

  • -
  • modified_deck – Deck lines in which a constant property may be inlined.

  • -
  • allow_inline – Inline a property when all values are equal.

  • -
  • file_suffix – Optional suffix added before .INC.

  • -
-
-
Returns:
-

True when the property was inlined, otherwise False.

-
-
Return type:
-

bool

-
-
-
- -
-
-pycopm.utils.files_writer.write_reference_to_coarse_map(dck: ConfigViaDeck, reftocoa: NDArray) → None
-

Write the reference-to-coarse mapping as OPERNUM.

-
-
Parameters:
-
    -
  • dck – Deck configuration controlling the output path and precision.

  • -
  • reftocoa – Coarse-cell identifier for every reference-grid cell.

  • -
-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.generate_decks.html b/docs/pycopm.utils.generate_decks.html deleted file mode 100644 index d89b903..0000000 --- a/docs/pycopm.utils.generate_decks.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - - pycopm.utils.generate_decks module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.generate_decks module

-

Coordinate coarsening, refinement, submodel extraction, and grid transformations.

-
-
-pycopm.utils.generate_decks._correct_fluid_in_place(dck: ConfigViaDeck, modified_deck: list[str]) → None
-

Adjust output pore volume to match input oil and gas in place.

-

Short Flow runs provide the fluid-in-place values used for two successive pore -volume corrections.

-
-
Parameters:
-
    -
  • dck – Deck configuration whose output_porv is updated.

  • -
  • modified_deck – Generated deck lines used to create the correction case.

  • -
-
-
-
- -
-
-pycopm.utils.generate_decks._create_index_mappings(dck: ConfigViaDeck, vicinity: VicinityMaps, refinement: RefinementMaps, coarsening: CoarseningMaps) → None
-

Create original-to-output mappings for each grid axis.

-

Depending on the selected workflow, the mappings represent coarse cells, -submodel indices, or the first and last cells created by refinement.

-
-
Parameters:
-
    -
  • dck – Deck configuration updated with the index mappings.

  • -
  • vicinity – Vicinity bounds for submodel extraction.

  • -
  • refinement – Per-axis refinement values.

  • -
  • coarsening – Per-axis coarsening values.

  • -
-
-
-
- -
-
-pycopm.utils.generate_decks._initialize_deck_data(dck: ConfigViaDeck) → None
-

Load dry-run grid and property data into the deck configuration.

-

The function opens EGRID, INIT, and optional restart files, determines grid -dimensions, and collects available property keywords.

-
-
Parameters:
-

dck – Deck configuration updated with OPM files, dimensions, and keyword lists.

-
-
-
- -
-
-pycopm.utils.generate_decks.create_deck(dck: ConfigViaDeck, cmdargs: Namespace) → None
-

Generate a modified OPM deck and its include files.

-

The selected workflow can preprocess the input deck, coarsen or refine the -grid, extract a vicinity submodel, transform coordinates, and optionally run -a validation dry run.

-
-
Parameters:
-
    -
  • dck – Deck configuration populated from command-line arguments.

  • -
  • cmdargs – Parsed command arguments used to build coarsening or refinement maps.

  • -
-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.input_values.html b/docs/pycopm.utils.input_values.html deleted file mode 100644 index 2725597..0000000 --- a/docs/pycopm.utils.input_values.html +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - pycopm.utils.input_values module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.input_values module

-

Create configuration objects from command-line arguments and TOML files.

-
-
-pycopm.utils.input_values.create_deck_config(cmdargs: Namespace) → ConfigViaDeck
-

Create a deck configuration from parsed command arguments.

-
-
Parameters:
-

cmdargs – Arguments returned by the command-line parser.

-
-
Returns:
-

Configuration for a deck-based workflow.

-
-
Return type:
-

ConfigViaDeck

-
-
-
- -
-
-pycopm.utils.input_values.load_toml_config(input_file: str, output_directory: str, resource_directory: str, significant_digits: int) → ConfigViaTOML
-

Load a TOML configuration and derive reference-grid dimensions.

-
-
Parameters:
-
    -
  • input_file – TOML configuration path.

  • -
  • output_directory – Generated-project directory.

  • -
  • resource_directory – Directory containing templates and reference simulations.

  • -
  • significant_digits – Precision used when writing floating-point values.

  • -
-
-
Returns:
-

Validated configuration populated with reference-grid metadata.

-
-
Return type:
-

ConfigViaTOML

-
-
-
- -
-
-pycopm.utils.input_values.parse_axis_modifications(uniform: str, localized: list) → tuple[NDArray, list]
-

Parse uniform or axis-specific grid modifications.

-

Uniform input contains one value for each axis. Axis-specific input can contain -explicit arrays; coarsening also accepts one-based indices and inclusive ranges -such as 2:4,7.

-
-
Parameters:
-
    -
  • uniform – Comma-separated x, y, and z modification values.

  • -
  • localized – Axis-specific specifications in x, y, and z order.

  • -
-
-
Returns:
-

Uniform axis values and the three parsed axis-specific arrays. Only one -representation is populated.

-
-
Return type:
-

cijk, axis_values

-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.parser_deck.html b/docs/pycopm.utils.parser_deck.html deleted file mode 100644 index 0960bc1..0000000 --- a/docs/pycopm.utils.parser_deck.html +++ /dev/null @@ -1,550 +0,0 @@ - - - - - - - - - pycopm.utils.parser_deck module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.parser_deck module

-

Parse an OPM deck and update records for the modified grid.

-
-
-class pycopm.utils.parser_deck._ParserState(dimens: bool = False, grid: bool = False, welspecs: bool = False, welsegs: bool = False, complump: bool = False, compdat: bool = False, compsegs: bool = False, mapaxes: bool = False, multregt: bool = False, process_edit: bool = False, editnnc: bool = False, multiply: bool = False, props: bool = False, operation: bool = False, regions: bool = False, equil: bool = False, faults: bool = False, multflt: bool = False, welldims: bool = False, skip_block: bool = False, aqucon: bool = False, aqunum: bool = False, aquancon: bool = False, bccon: bool = False, bwpr: bool = False, source: bool = False, pinch: bool = False, has_edit: bool = False, previous_completion: list[str] = <factory>, compsegs_wells: list[str] = <factory>, retained_wells: list[str] = <factory>, completion_wells: list[str] = <factory>, segmented_wells: list[str] = <factory>, separator: str = '', schedule_keyword: str = '')
-

Bases: object

-

Store temporary state while parsing an OPM deck.

-

The Boolean fields indicate active keyword blocks. The list fields track wells, -completions, and segmented-well records retained in the generated deck.

-
-
-aquancon: bool
-
- -
-
-aqucon: bool
-
- -
-
-aqunum: bool
-
- -
-
-bccon: bool
-
- -
-
-bwpr: bool
-
- -
-
-compdat: bool
-
- -
-
-completion_wells: list[str]
-
- -
-
-complump: bool
-
- -
-
-compsegs: bool
-
- -
-
-compsegs_wells: list[str]
-
- -
-
-dimens: bool
-
- -
-
-editnnc: bool
-
- -
-
-equil: bool
-
- -
-
-faults: bool
-
- -
-
-grid: bool
-
- -
-
-has_edit: bool
-
- -
-
-mapaxes: bool
-
- -
-
-multflt: bool
-
- -
-
-multiply: bool
-
- -
-
-multregt: bool
-
- -
-
-operation: bool
-
- -
-
-pinch: bool
-
- -
-
-previous_completion: list[str]
-
- -
-
-process_edit: bool
-
- -
-
-props: bool
-
- -
-
-regions: bool
-
- -
-
-retained_wells: list[str]
-
- -
-
-schedule_keyword: str
-
- -
-
-segmented_wells: list[str]
-
- -
-
-separator: str
-
- -
-
-skip_block: bool
-
- -
-
-source: bool
-
- -
-
-welldims: bool
-
- -
-
-welsegs: bool
-
- -
-
-welspecs: bool
-
- -
- -
-
-pycopm.utils.parser_deck.__handle_schedule_keyword(kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Filter supported schedule records by wells retained in the submodel.

-
- -
-
-pycopm.utils.parser_deck._append_explicit_solution_includes(dck: ConfigViaDeck, modified_deck: list[str]) → None
-

Append include statements for the explicit solution properties.

-
- -
-
-pycopm.utils.parser_deck._collect_segmented_well_names(dck: ConfigViaDeck, kwr: _ParserState) → None
-

Identify wells requiring segmented-well completion handling.

-
- -
-
-pycopm.utils.parser_deck._collect_vicinity_well_names(dck: ConfigViaDeck, kwr: _ParserState, vicinity: VicinityMaps) → None
-

Identify wells and segmented wells retained in the vicinity model.

-
- -
-
-pycopm.utils.parser_deck._handle_aquancon(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update AQUANCON grid-index ranges for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_aqucon(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update AQUCON grid-index ranges for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_aqunum(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update AQUNUM grid indices for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_bccon(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update BCCON grid-index ranges for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_bwpr(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update BWPR grid indices for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_compsegs(kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Filter COMPSEGS records by wells retained in the submodel.

-
- -
-
-pycopm.utils.parser_deck._handle_dimens(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Replace the original DIMENS values with the modified grid dimensions.

-
- -
-
-pycopm.utils.parser_deck._handle_editnnc(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update EDITNNC grid indices for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_equil(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Replace EQUIL with explicit initialization include files.

-
- -
-
-pycopm.utils.parser_deck._handle_fault(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update FAULTS grid-index ranges for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_grid_props(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Replace GRID properties and preserve supported GRID-section keywords.

-
- -
-
-pycopm.utils.parser_deck._handle_mapaxes(kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Preserve MAPAXES so the generated grids retain the same map view.

-
- -
-
-pycopm.utils.parser_deck._handle_multflt(kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Preserve fault multiplier records from the input deck.

-
- -
-
-pycopm.utils.parser_deck._handle_multiply(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update MULTIPLY grid-index ranges for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_multregt(kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Preserve MULTREGT records from the GRID section.

-
- -
-
-pycopm.utils.parser_deck._handle_oper(dck: ConfigViaDeck, vicinity: VicinityMaps, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update supported operation records for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_pinch(kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Preserve PINCH records from the input deck.

-
- -
-
-pycopm.utils.parser_deck._handle_props(dck: ConfigViaDeck, vicinity: VicinityMaps, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Handle the PROPS section and its supported operations.

-
- -
-
-pycopm.utils.parser_deck._handle_regions(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Replace the REGIONS content with generated include files.

-
- -
-
-pycopm.utils.parser_deck._handle_segmented_wells(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str, wellcind: list) → bool
-

Update COMPDAT, COMPSEGS, and COMPLUMP records for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_source(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update SOURCE grid indices for the modified grid.

-
- -
-
-pycopm.utils.parser_deck._handle_welldims(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Update WELLDIMS for a refined grid.

-
- -
-
-pycopm.utils.parser_deck._handle_wells(dck: ConfigViaDeck, kwr: _ParserState, modified_deck: list[str], nrwo: str, hv: bool) → bool
-

Update well-head grid indices and activate completion handlers.

-
- -
-
-pycopm.utils.parser_deck._handle_welsegs(kwr: _ParserState, modified_deck: list[str], nrwo: str) → bool
-

Filter WELSEGS records by wells retained in the submodel.

-
- -
-
-pycopm.utils.parser_deck._include_contains_endbox(dck: ConfigViaDeck, nrwo: str) → bool
-

Return whether an included file contains ENDBOX.

-
- -
-
-pycopm.utils.parser_deck._mark_multiplier_keyword(deck_line: str, mults: NDArray) → NDArray
-

Set the corresponding flag if a multiplier keyword is found.

-
- -
-
-pycopm.utils.parser_deck._scan_deck_file(dck: ConfigViaDeck, file_path: str | Path) → tuple[list[str], bool, NDArray]
-

Scan a deck file for includes and directional multipliers.

-
-
Parameters:
-
    -
  • dck – Deck configuration providing the file encoding.

  • -
  • file_path – DATA or include file to scan.

  • -
-
-
Returns:
-

Resolved include paths, whether the main deck contains MULTFLT, and -flags for MULTX, MULTX-, MULTY, MULTY-, MULTZ, and -MULTZ-.

-
-
Return type:
-

includes, has_main_multflt, multipliers

-
-
-
- -
-
-pycopm.utils.parser_deck.find_multiplier_keywords(dck: ConfigViaDeck) → tuple[bool, NDArray]
-

Find directional multiplier keywords in nested includes.

-

At most three levels of included files are scanned.

-
-
Parameters:
-

dck – Deck configuration identifying the input deck and encoding.

-
-
Returns:
-

Whether the main deck contains MULTFLT and directional multiplier -flags in x, x-, y, y-, z, and z- order.

-
-
Return type:
-

has_main_multflt, multipliers

-
-
-
- -
-
-pycopm.utils.parser_deck.process_deck(dck: ConfigViaDeck, vicinity: VicinityMaps) → tuple[list[str], list[int]]
-

Rewrite deck records for the modified grid.

-

The parser updates dimensions, properties, grid-index ranges, wells, aquifers, -faults, and selected schedule records.

-
-
Parameters:
-
    -
  • dck – Deck configuration and axis index mappings.

  • -
  • vicinity – Vicinity selection used when extracting a submodel.

  • -
-
-
Returns:
-

Rewritten deck lines and coarse cells containing well completions.

-
-
Return type:
-

modified_deck, well_cell_indices

-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.refinement.html b/docs/pycopm.utils.refinement.html deleted file mode 100644 index dca11e1..0000000 --- a/docs/pycopm.utils.refinement.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - - - - pycopm.utils.refinement module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.refinement module

-

Refine a corner-point grid and its reservoir properties.

-
-
-class pycopm.utils.refinement.RefinementMaps(x: NDArray, y: NDArray, z: NDArray, refined_cell_counts: NDArray)
-

Bases: object

-

Store axis refinement values and subdivision counts.

-
-
-refined_cell_counts: NDArray
-

Number of refined cells generated from each original cell, flattened in -(z, y, x) order.

-
- -
-
-x: NDArray
-

Number of additional cells created from each original x interval.

-
- -
-
-y: NDArray
-

Number of additional cells created from each original y interval.

-
- -
-
-z: NDArray
-

Number of additional cells created from each original z interval.

-
- -
- -
-
-pycopm.utils.refinement.create_coord_axis_map(refinement_values: NDArray) → tuple[NDArray, NDArray]
-

Create interpolation data for one COORD axis.

-
-
Parameters:
-

refinement_values – Number of additional cells in each original interval.

-
-
Returns:
-

Original intervals and relative positions of refined grid points.

-
-
Return type:
-

source_indices, fractions

-
-
-
- -
-
-pycopm.utils.refinement.create_refinement_maps(dck: ConfigViaDeck, cmdargs: Namespace) → RefinementMaps
-

Create axis refinement maps and update output dimensions.

-

A refinement value of n divides an original interval into n + 1 -intervals.

-
-
Parameters:
-
    -
  • dck – Deck configuration whose output dimensions are updated.

  • -
  • cmdargs – Command arguments containing refinement, x_refinement, -y_refinement, and z_refinement.

  • -
-
-
Returns:
-

Axis values and the number of subdivisions per original cell.

-
-
Return type:
-

RefinementMaps

-
-
-
- -
-
-pycopm.utils.refinement.create_zcorn_axis_map(refinement_values: NDArray) → tuple[NDArray, NDArray]
-

Create interpolation data for one ZCORN axis.

-
-
Parameters:
-

refinement_values – Number of additional cells in each original interval.

-
-
Returns:
-

Original intervals and relative corner positions in ZCORN order.

-
-
Return type:
-

source_indices, fractions

-
-
-
- -
-
-pycopm.utils.refinement.refine_grid(dck: ConfigViaDeck, refinement: RefinementMaps) → None
-

Create and write the refined corner-point grid.

-

COORD and ZCORN values are linearly interpolated along the refined -axes.

-
-
Parameters:
-
    -
  • dck – Deck configuration containing original geometry and grid dimensions.

  • -
  • refinement – Axis refinement maps.

  • -
-
-
-
- -
-
-pycopm.utils.refinement.refine_properties(dck: ConfigViaDeck, refinement: RefinementMaps, modified_deck: list[str]) → None
-

Map reservoir properties onto the refined grid.

-

Properties are copied to generated cells. PORV is divided equally among -them to preserve each original cell’s pore volume.

-
-
Parameters:
-
    -
  • dck – Deck configuration containing source properties and output dimensions.

  • -
  • refinement – Refinement maps created by create_refinement_maps().

  • -
  • modified_deck – Deck lines updated with generated property includes.

  • -
-
-
-
- -
-
-pycopm.utils.refinement.refine_zcorn_surface(source_surface: NDArray, destination_surface: NDArray, original_nx: int, original_ny: int, output_nx: int, output_ny: int, zcorn_x_indices: NDArray, zcorn_y_indices: NDArray, zcorn_x_fractions: NDArray, zcorn_y_fractions: NDArray) → None
-

Interpolate one ZCORN surface onto the refined horizontal grid.

-
-
Parameters:
-
    -
  • source_surface – Flattened input surface with 4 * original_nx * original_ny values.

  • -
  • destination_surface – Preallocated output with 4 * output_nx * output_ny values, modified -in place.

  • -
  • original_nx – Original horizontal grid dimensions.

  • -
  • original_ny – Original horizontal grid dimensions.

  • -
  • output_nx – Refined horizontal grid dimensions.

  • -
  • output_ny – Refined horizontal grid dimensions.

  • -
  • zcorn_x_indices – Source interval indices for refined corners.

  • -
  • zcorn_y_indices – Source interval indices for refined corners.

  • -
  • zcorn_x_fractions – Relative interpolation positions within source intervals.

  • -
  • zcorn_y_fractions – Relative interpolation positions within source intervals.

  • -
-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.runs_executer.html b/docs/pycopm.utils.runs_executer.html deleted file mode 100644 index 0585db8..0000000 --- a/docs/pycopm.utils.runs_executer.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - pycopm.utils.runs_executer module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.runs_executer module

-

Run TOML-based simulation studies and generate postprocessing plots.

-
-
-pycopm.utils.runs_executer.generate_postprocessing_plots(cfg: ConfigViaTOML, elapsed_seconds: float, number_tables: int) → None
-

Render and execute the postprocessing script.

-
-
Parameters:
-
    -
  • cfg – TOML configuration and plotting settings. let_parameters is sorted in -place before rendering.

  • -
  • elapsed_seconds – Elapsed preprocessing and simulation time.

  • -
  • number_tables – Number of generated saturation-function tables.

  • -
-
-
-
- -
-
-pycopm.utils.runs_executer.run_simulations(cfg: ConfigViaTOML) → None
-

Run the configured OPM Flow or ERT workflow.

-

Job scripts are copied to the output project and made executable before a -single realization or ERT study is started.

-
-
Parameters:
-

cfg – TOML configuration containing execution mode and commands.

-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.transformation.html b/docs/pycopm.utils.transformation.html deleted file mode 100644 index 91376f6..0000000 --- a/docs/pycopm.utils.transformation.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - pycopm.utils.transformation module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.transformation module

-

Transform corner-point grid coordinates and rewrite associated properties.

-
-
-pycopm.utils.transformation.transform_grid(dck: ConfigViaDeck) → None
-

Apply the configured transformation to the corner-point grid.

-

Supported specifications are translate [x,y,z], scale [x,y,z], and -rotatexy, rotatexz, or rotateyz followed by an angle in degrees. -Rotations are performed about the coordinate-system origin.

-
-
Parameters:
-

dck – Deck configuration containing grid_transformation and source geometry.

-
-
-
- -
-
-pycopm.utils.transformation.transform_properties(dck: ConfigViaDeck, modified_deck: list[str]) → None
-

Rewrite reservoir properties for a transformed grid.

-

Property values are unchanged because transformations modify only geometry.

-
-
Parameters:
-
    -
  • dck – Deck configuration containing source properties and output dimensions.

  • -
  • modified_deck – Deck lines updated with generated property includes.

  • -
-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/pycopm.utils.vicinity.html b/docs/pycopm.utils.vicinity.html deleted file mode 100644 index 61bae1b..0000000 --- a/docs/pycopm.utils.vicinity.html +++ /dev/null @@ -1,442 +0,0 @@ - - - - - - - - - pycopm.utils.vicinity module — pycopm documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

pycopm.utils.vicinity module

-

Extract submodels and map pore volume from outside their boundaries.

-
-
-class pycopm.utils.vicinity.VicinityMaps(selector: str, shape: str | None, cell_mask: ~numpy._typing._array_like.NDArray, min_i: int, max_i: int, min_j: int, max_j: int, min_k: int, max_k: int, layer_min_i: ~numpy._typing._array_like.NDArray, layer_max_i: ~numpy._typing._array_like.NDArray, layer_min_j: ~numpy._typing._array_like.NDArray, layer_max_j: ~numpy._typing._array_like.NDArray, layer_selected_porv: ~numpy._typing._array_like.NDArray, layer_external_porv: ~numpy._typing._array_like.NDArray, well_cells: list[list[int]], well_indices: list[int] = <factory>, active_counts: ~numpy._typing._array_like.NDArray = <factory>, source_indices: ~numpy._typing._array_like.NDArray = <factory>)
-

Bases: object

-

Store a vicinity selection, bounds, and pore-volume mappings.

-

Global and per-layer bounds are one-based and inclusive.

-
-
-active_counts: NDArray
-

Number of selected active cells in each output layer.

-
- -
-
-cell_mask: NDArray
-

Boolean mask identifying selected original-grid cells, flattened in -(z, y, x) order.

-
- -
-
-layer_external_porv: NDArray
-

Total pore volume outside the selection in each original layer.

-
- -
-
-layer_max_i: NDArray
-

Maximum selected i index in each original layer.

-
- -
-
-layer_max_j: NDArray
-

Maximum selected j index in each original layer.

-
- -
-
-layer_min_i: NDArray
-

Minimum selected i index in each original layer.

-
- -
-
-layer_min_j: NDArray
-

Minimum selected j index in each original layer.

-
- -
-
-layer_selected_porv: NDArray
-

Total pore volume of selected active cells in each original layer.

-
- -
-
-max_i: int
-

Maximum selected i index across all layers.

-
- -
-
-max_j: int
-

Maximum selected j index across all layers.

-
- -
-
-max_k: int
-

Maximum selected k index.

-
- -
-
-min_i: int
-

Minimum selected i index across all layers.

-
- -
-
-min_j: int
-

Minimum selected j index across all layers.

-
- -
-
-min_k: int
-

Minimum selected k index.

-
- -
-
-selector: str
-

Selected well name, or an empty string for polygon and region -selections.

-
- -
-
-shape: str | None
-

box, diamond, or diamondxy. None is -used for polygon and region selections.

-
-
Type:
-

Well-vicinity shape

-
-
-
- -
-
-source_indices: NDArray
-

Original-grid source index assigned to each output boundary cell during -pore-volume correction. Zero denotes no assigned source.

-
- -
-
-well_cells: list[list[int]]
-

Zero-based completion coordinates in [i, j, k] order for the -selected well.

-
- -
-
-well_indices: list[int]
-

Zero-based output-grid indices of selected well cells. These cells are -excluded from boundary source remapping.

-
- -
- -
-
-class pycopm.utils.vicinity._BoundaryMapping(pore_volume: float, active_count: int, offsets: NDArray)
-

Bases: object

-

Store pore-volume mapping results for one submodel boundary.

-
-
-active_count: int
-

Number of active cells receiving pore volume from the boundary.

-
- -
-
-offsets: NDArray
-

Distance from the geometric boundary to each receiving cell.

-
- -
-
-pore_volume: float
-

Unassigned pore volume collected along the boundary.

-
- -
- -
-
-pycopm.utils.vicinity._add_or_collect_porv(dck: ConfigViaDeck, submodel_index: int, pore_volume: float) → float
-
- -
-
-pycopm.utils.vicinity._apply_layer_pore_volume_correction(dck: ConfigViaDeck, vicinity: VicinityMaps, layer_index: int, column_offset: int, row_offset: int, trailing_columns: int, trailing_rows: int, south: _BoundaryMapping, north: _BoundaryMapping, east: _BoundaryMapping, west: _BoundaryMapping, corner_porv: tuple[float, float, float, float]) → None
-
- -
-
-pycopm.utils.vicinity._corner_pore_volumes(dck: ConfigViaDeck, vicinity: VicinityMaps, original_layer: int) → tuple[float, float, float, float]
-

Calculate excluded pore volume in the four layer corners.

-
-
Returns:
-

Corner pore-volume totals for the selected original layer.

-
-
Return type:
-

southwest, southeast, northwest, northeast

-
-
-
- -
-
-pycopm.utils.vicinity._distribute_vertical_pore_volume(dck: ConfigViaDeck, vicinity: VicinityMaps) → None
-

Distribute pore volume excluded above and below the submodel.

-
-
Parameters:
-
    -
  • dck – Deck configuration whose output_porv is updated.

  • -
  • vicinity – Selection bounds and correction settings.

  • -
-
-
-
- -
-
-pycopm.utils.vicinity._find_nearest_active_corner_cell(dck: ConfigViaDeck, layer_index: int, corner_i: int, corner_j: int) → int
-

Return the nearest active cell to a corner in a layer.

-
- -
-
-pycopm.utils.vicinity._get_well_completions_for_vicinity(dck: ConfigViaDeck, optvic) → list
-

Collect zero-based completions for a selected well.

-
-
Parameters:
-
    -
  • dck – Deck configuration identifying the source DATA file.

  • -
  • optvic – Well name from the vicinity specification.

  • -
-
-
Returns:
-

Completion coordinates in [i, j, k] order.

-
-
Return type:
-

list[list[int]]

-
-
-
- -
-
-pycopm.utils.vicinity._map_east_boundary(dck: ConfigViaDeck, vicinity: VicinityMaps, layer_index: int, original_layer: int, column_offset: int, row_offset: int, trailing_rows: int) → _BoundaryMapping
-
- -
-
-pycopm.utils.vicinity._map_north_boundary(dck: ConfigViaDeck, vicinity: VicinityMaps, layer_index: int, original_layer: int, column_offset: int, trailing_rows: int, trailing_columns: int) → _BoundaryMapping
-
- -
-
-pycopm.utils.vicinity._map_south_boundary(dck: ConfigViaDeck, vicinity: VicinityMaps, layer_index: int, original_layer: int, column_offset: int, row_offset: int, trailing_columns: int) → _BoundaryMapping
-
- -
-
-pycopm.utils.vicinity._map_west_boundary(dck: ConfigViaDeck, vicinity: VicinityMaps, layer_index: int, original_layer: int, row_offset: int, trailing_rows: int, trailing_columns: int) → _BoundaryMapping
-
- -
-
-pycopm.utils.vicinity._original_index(dck: ConfigViaDeck, column: int, row: int, layer: int) → int
-
- -
-
-pycopm.utils.vicinity._submodel_index(dck: ConfigViaDeck, column: int, row: int, layer: int) → int
-
- -
-
-pycopm.utils.vicinity.apply_boundary_pore_volume_correction(dck: ConfigViaDeck, vicinity: VicinityMaps) → None
-

Map pore volume excluded from the submodel onto active cells.

-

The correction strategy is selected by dck.pore_volume_correction. Depending -on the chosen method, excluded pore volume is assigned to corresponding -boundary cells, nearest corner cells, all boundary cells, or all active cells.

-
-
Parameters:
-
    -
  • dck – Deck configuration whose output_porv is updated.

  • -
  • vicinity – Selection bounds and pore-volume mapping arrays.

  • -
-
-
-
- -
-
-pycopm.utils.vicinity.create_vicinity_maps(dck: ConfigViaDeck) → VicinityMaps
-

Select submodel cells and calculate their bounds.

-

Selections can use region values, an xy polygon, or a well-centred box, -diamond, or diamondxy neighbourhood.

-
-
Parameters:
-

dck – Deck configuration containing the vicinity specification and source grid.

-
-
Returns:
-

Selection mask, bounds, well cells, and per-layer pore-volume totals.

-
-
Return type:
-

VicinityMaps

-
-
-
- -
-
-pycopm.utils.vicinity.extract_vicinity_grid(dck: ConfigViaDeck, vicinity: VicinityMaps) → None
-

Extract and write the selected corner-point subgrid.

-
-
Parameters:
-
    -
  • dck – Deck configuration containing source geometry and axis mappings.

  • -
  • vicinity – Inclusive bounds of the selected submodel.

  • -
-
-
-
- -
-
-pycopm.utils.vicinity.map_vicinity_properties(dck: ConfigViaDeck, vicinity: VicinityMaps, modified_deck: list[str]) → None
-

Map reservoir properties into the submodel bounding box.

-

Cells inside the bounding box but outside the selection are written as -inactive. The function updates output pore volume and active cells.

-
-
Parameters:
-
    -
  • dck – Deck configuration containing source properties and output dimensions.

  • -
  • vicinity – Selection and bounds created by create_vicinity_maps().

  • -
  • modified_deck – Deck lines updated with generated property includes.

  • -
-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/related.html b/docs/related.html index 7d16b29..ef59bcf 100644 --- a/docs/related.html +++ b/docs/related.html @@ -1,169 +1,727 @@ - - - - - - - Related — pycopm documentation - - - - - - - - - - - + + + + + + + + + Related projects — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + + - + - +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/search.html b/docs/search.html index a3ee8dc..88c8961 100644 --- a/docs/search.html +++ b/docs/search.html @@ -1,130 +1,580 @@ - - - - - - Search — pycopm documentation - - - - - - - - - - - - - - + + + + + + + Search - pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • + + + - + + + -
    + -
    +
+ + + + +
+
+ + + + + + + + + + + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
+ +
+ + +
+
+ +
+ + +
+

Search

+ + + +
- + + + + + +
+ +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+ + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js index aab231f..9a16f0a 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"About pycopm":[[0,null]],"Binary packages":[[6,"binary-packages"]],"Brew formula for macOS":[[6,"brew-formula-for-macos"]],"Concept":[[7,"concept"]],"Configuration file":[[2,null]],"Contribute to the software":[[3,"contribute-to-the-software"]],"Contributing":[[3,null]],"Drogon":[[4,"drogon"]],"Dual coarsening":[[4,"dual-coarsening"]],"Examples":[[4,null]],"Graphical abstract":[[4,"graphical-abstract"]],"Grid coarsening":[[24,"grid-coarsening"]],"Grid refinement":[[24,"grid-refinement"]],"Ground Rules":[[3,"ground-rules"]],"Hello world":[[4,"hello-world"]],"Indices and tables":[[5,"indices-and-tables"]],"Installation":[[6,null]],"Introduction":[[7,null]],"Module contents":[[10,"module-pycopm"],[11,"module-pycopm.core"],[13,"module-pycopm.utils"]],"Norne":[[4,"norne"]],"OPM Flow":[[6,"opm-flow"]],"Output folder":[[9,null]],"Overview":[[7,"overview"]],"Python package":[[6,"python-package"]],"Related":[[23,null]],"Report issues or problems":[[3,"report-issues-or-problems"]],"Roadmap":[[7,"roadmap"]],"SPE10":[[4,"spe10"]],"Seek support":[[3,"seek-support"]],"Smeaheia":[[4,"smeaheia"]],"Source build in Linux/Windows":[[6,"source-build-in-linux-windows"]],"Source build in macOS":[[6,"source-build-in-macos"]],"Submodels":[[24,"submodels"]],"Submodules":[[11,"submodules"],[13,"submodules"]],"Subpackages":[[10,"subpackages"]],"Theory":[[24,null]],"Transformations":[[24,"transformations"]],"Via OPM Flow decks":[[4,"via-opm-flow-decks"]],"Via an OPM Flow input deck":[[9,"via-an-opm-flow-input-deck"]],"Via configuration files":[[4,"via-configuration-files"],[9,"via-configuration-files"]],"Welcome to pycopm\u2019s documentation!":[[5,null]],"ad-micp":[[23,"ad-micp"]],"expreccs":[[23,"expreccs"]],"plopm":[[23,"plopm"]],"pofff":[[23,"pofff"]],"pycopm":[[1,"pycopm"],[8,null]],"pycopm Python API":[[1,null]],"pycopm package":[[10,null]],"pycopm.core package":[[11,null]],"pycopm.core.pycopm module":[[12,null]],"pycopm.utils package":[[13,null]],"pycopm.utils.coarsening module":[[14,null]],"pycopm.utils.files_writer module":[[15,null]],"pycopm.utils.generate_decks module":[[16,null]],"pycopm.utils.input_values module":[[17,null]],"pycopm.utils.parser_deck module":[[18,null]],"pycopm.utils.refinement module":[[19,null]],"pycopm.utils.runs_executer module":[[20,null]],"pycopm.utils.transformation module":[[21,null]],"pycopm.utils.vicinity module":[[22,null]],"pymm":[[23,"pymm"]],"pyopmnearwell":[[23,"pyopmnearwell"]],"pyopmspe11":[[23,"pyopmspe11"]]},"docnames":["about","api","configuration_file","contributing","examples","index","installation","introduction","modules","output_folder","pycopm","pycopm.core","pycopm.core.pycopm","pycopm.utils","pycopm.utils.coarsening","pycopm.utils.files_writer","pycopm.utils.generate_decks","pycopm.utils.input_values","pycopm.utils.parser_deck","pycopm.utils.refinement","pycopm.utils.runs_executer","pycopm.utils.transformation","pycopm.utils.vicinity","related","theory"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["about.rst","api.rst","configuration_file.rst","contributing.rst","examples.rst","index.rst","installation.rst","introduction.rst","modules.rst","output_folder.rst","pycopm.rst","pycopm.core.rst","pycopm.core.pycopm.rst","pycopm.utils.rst","pycopm.utils.coarsening.rst","pycopm.utils.files_writer.rst","pycopm.utils.generate_decks.rst","pycopm.utils.input_values.rst","pycopm.utils.parser_deck.rst","pycopm.utils.refinement.rst","pycopm.utils.runs_executer.rst","pycopm.utils.transformation.rst","pycopm.utils.vicinity.rst","related.rst","theory.rst"],"indexentries":{},"objects":{"":[[10,0,0,"-","pycopm"]],"pycopm":[[11,0,0,"-","core"],[13,0,0,"-","utils"]],"pycopm.core":[[12,0,0,"-","pycopm"]],"pycopm.core.pycopm":[[12,1,1,"","_check_cmdargs"],[12,1,1,"","_check_flow"],[12,1,1,"","_parse_arguments"],[12,1,1,"","main"]],"pycopm.utils":[[14,0,0,"-","coarsening"],[15,0,0,"-","files_writer"],[16,0,0,"-","generate_decks"],[17,0,0,"-","input_values"],[18,0,0,"-","parser_deck"],[19,0,0,"-","refinement"],[20,0,0,"-","runs_executer"],[21,0,0,"-","transformation"],[22,0,0,"-","vicinity"]],"pycopm.utils.coarsening":[[14,2,1,"","CoarseningMaps"],[14,1,1,"","_collect_removed_zcorn_indices"],[14,1,1,"","_compact_permeability_properties"],[14,1,1,"","_find_active_neighbors"],[14,1,1,"","_find_include_statement"],[14,1,1,"","_global_index_to_ijk"],[14,1,1,"","_group_maximum_zero_based"],[14,1,1,"","_group_minimum_zero_based"],[14,1,1,"","_group_sum_zero_based"],[14,1,1,"","_grouped_count"],[14,1,1,"","_grouped_first"],[14,1,1,"","_grouped_last"],[14,1,1,"","_grouped_max"],[14,1,1,"","_grouped_mean"],[14,1,1,"","_grouped_min"],[14,1,1,"","_grouped_mode"],[14,1,1,"","_grouped_sum"],[14,1,1,"","_interleave_dual_property"],[14,1,1,"","_read_satnum"],[14,1,1,"","build_dual_porosity_grid"],[14,1,1,"","coarsen_and_write_properties"],[14,1,1,"","coarsen_corner_point_grid"],[14,1,1,"","coarsen_properties"],[14,1,1,"","create_coarsening_map"],[14,1,1,"","create_coarsening_maps"],[14,1,1,"","map_nnc_transmissibilities"],[14,1,1,"","redistribute_removed_pore_volume"]],"pycopm.utils.coarsening.CoarseningMaps":[[14,3,1,"","cell_groups"],[14,3,1,"","coarse_tranx"],[14,3,1,"","coarse_trany"],[14,3,1,"","coarsened_axes"],[14,3,1,"","dual_defaults"],[14,3,1,"","dual_tranx"],[14,3,1,"","dual_trany"],[14,3,1,"","matrix_mask"],[14,3,1,"","nnc_text"],[14,3,1,"","reference_to_coarse"],[14,3,1,"","vertical_transfer_enabled"],[14,3,1,"","x"],[14,3,1,"","y"],[14,3,1,"","z"]],"pycopm.utils.files_writer":[[15,1,1,"","_identify_removed_pillars"],[15,1,1,"","_render_template"],[15,1,1,"","_write_ert_files"],[15,1,1,"","_write_grid_files"],[15,1,1,"","_write_let_tables"],[15,1,1,"","format_opm_compact_values"],[15,1,1,"","round_like_e"],[15,1,1,"","write_coarsened_model_files"],[15,1,1,"","write_compact_property_file"],[15,1,1,"","write_dual_properties"],[15,1,1,"","write_grid"],[15,1,1,"","write_include"],[15,1,1,"","write_porv"],[15,1,1,"","write_property"],[15,1,1,"","write_property_inc"],[15,1,1,"","write_reference_to_coarse_map"]],"pycopm.utils.generate_decks":[[16,1,1,"","_correct_fluid_in_place"],[16,1,1,"","_create_index_mappings"],[16,1,1,"","_initialize_deck_data"],[16,1,1,"","create_deck"]],"pycopm.utils.input_values":[[17,1,1,"","create_deck_config"],[17,1,1,"","load_toml_config"],[17,1,1,"","parse_axis_modifications"]],"pycopm.utils.parser_deck":[[18,2,1,"","_ParserState"],[18,1,1,"","__handle_schedule_keyword"],[18,1,1,"","_append_explicit_solution_includes"],[18,1,1,"","_collect_segmented_well_names"],[18,1,1,"","_collect_vicinity_well_names"],[18,1,1,"","_handle_aquancon"],[18,1,1,"","_handle_aqucon"],[18,1,1,"","_handle_aqunum"],[18,1,1,"","_handle_bccon"],[18,1,1,"","_handle_bwpr"],[18,1,1,"","_handle_compsegs"],[18,1,1,"","_handle_dimens"],[18,1,1,"","_handle_editnnc"],[18,1,1,"","_handle_equil"],[18,1,1,"","_handle_fault"],[18,1,1,"","_handle_grid_props"],[18,1,1,"","_handle_mapaxes"],[18,1,1,"","_handle_multflt"],[18,1,1,"","_handle_multiply"],[18,1,1,"","_handle_multregt"],[18,1,1,"","_handle_oper"],[18,1,1,"","_handle_pinch"],[18,1,1,"","_handle_props"],[18,1,1,"","_handle_regions"],[18,1,1,"","_handle_segmented_wells"],[18,1,1,"","_handle_source"],[18,1,1,"","_handle_welldims"],[18,1,1,"","_handle_wells"],[18,1,1,"","_handle_welsegs"],[18,1,1,"","_include_contains_endbox"],[18,1,1,"","_mark_multiplier_keyword"],[18,1,1,"","_scan_deck_file"],[18,1,1,"","find_multiplier_keywords"],[18,1,1,"","process_deck"]],"pycopm.utils.parser_deck._ParserState":[[18,3,1,"","aquancon"],[18,3,1,"","aqucon"],[18,3,1,"","aqunum"],[18,3,1,"","bccon"],[18,3,1,"","bwpr"],[18,3,1,"","compdat"],[18,3,1,"","completion_wells"],[18,3,1,"","complump"],[18,3,1,"","compsegs"],[18,3,1,"","compsegs_wells"],[18,3,1,"","dimens"],[18,3,1,"","editnnc"],[18,3,1,"","equil"],[18,3,1,"","faults"],[18,3,1,"","grid"],[18,3,1,"","has_edit"],[18,3,1,"","mapaxes"],[18,3,1,"","multflt"],[18,3,1,"","multiply"],[18,3,1,"","multregt"],[18,3,1,"","operation"],[18,3,1,"","pinch"],[18,3,1,"","previous_completion"],[18,3,1,"","process_edit"],[18,3,1,"","props"],[18,3,1,"","regions"],[18,3,1,"","retained_wells"],[18,3,1,"","schedule_keyword"],[18,3,1,"","segmented_wells"],[18,3,1,"","separator"],[18,3,1,"","skip_block"],[18,3,1,"","source"],[18,3,1,"","welldims"],[18,3,1,"","welsegs"],[18,3,1,"","welspecs"]],"pycopm.utils.refinement":[[19,2,1,"","RefinementMaps"],[19,1,1,"","create_coord_axis_map"],[19,1,1,"","create_refinement_maps"],[19,1,1,"","create_zcorn_axis_map"],[19,1,1,"","refine_grid"],[19,1,1,"","refine_properties"],[19,1,1,"","refine_zcorn_surface"]],"pycopm.utils.refinement.RefinementMaps":[[19,3,1,"","refined_cell_counts"],[19,3,1,"","x"],[19,3,1,"","y"],[19,3,1,"","z"]],"pycopm.utils.runs_executer":[[20,1,1,"","generate_postprocessing_plots"],[20,1,1,"","run_simulations"]],"pycopm.utils.transformation":[[21,1,1,"","transform_grid"],[21,1,1,"","transform_properties"]],"pycopm.utils.vicinity":[[22,2,1,"","VicinityMaps"],[22,2,1,"","_BoundaryMapping"],[22,1,1,"","_add_or_collect_porv"],[22,1,1,"","_apply_layer_pore_volume_correction"],[22,1,1,"","_corner_pore_volumes"],[22,1,1,"","_distribute_vertical_pore_volume"],[22,1,1,"","_find_nearest_active_corner_cell"],[22,1,1,"","_get_well_completions_for_vicinity"],[22,1,1,"","_map_east_boundary"],[22,1,1,"","_map_north_boundary"],[22,1,1,"","_map_south_boundary"],[22,1,1,"","_map_west_boundary"],[22,1,1,"","_original_index"],[22,1,1,"","_submodel_index"],[22,1,1,"","apply_boundary_pore_volume_correction"],[22,1,1,"","create_vicinity_maps"],[22,1,1,"","extract_vicinity_grid"],[22,1,1,"","map_vicinity_properties"]],"pycopm.utils.vicinity.VicinityMaps":[[22,3,1,"","active_counts"],[22,3,1,"","cell_mask"],[22,3,1,"","layer_external_porv"],[22,3,1,"","layer_max_i"],[22,3,1,"","layer_max_j"],[22,3,1,"","layer_min_i"],[22,3,1,"","layer_min_j"],[22,3,1,"","layer_selected_porv"],[22,3,1,"","max_i"],[22,3,1,"","max_j"],[22,3,1,"","max_k"],[22,3,1,"","min_i"],[22,3,1,"","min_j"],[22,3,1,"","min_k"],[22,3,1,"","selector"],[22,3,1,"","shape"],[22,3,1,"","source_indices"],[22,3,1,"","well_cells"],[22,3,1,"","well_indices"]],"pycopm.utils.vicinity._BoundaryMapping":[[22,3,1,"","active_count"],[22,3,1,"","offsets"],[22,3,1,"","pore_volume"]]},"objnames":{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:function","2":"py:class","3":"py:attribute"},"terms":{"0f":4,"0pressur":4,"1e":4,"1f":4,"2d":24,"2e":4,"2f":4,"2xyz":4,"3h":24,"3xz":4,"A":[2,19,23,24],"About":5,"After":4,"All":4,"An":[3,23],"As":[4,7,9,24],"At":18,"Below":23,"By":[2,4],"For":[0,2,3,4,6,7,9,24],"From":[4,24],"Here":[2,4,24],"I":2,"If":[2,4,6,12,24],"In":[3,4,6,7,24],"It":4,"The":[0,1,2,3,4,6,7,9,12,14,16,18,22,24],"Then":[2,4,6,9,24],"There":4,"These":[4,22,24],"This":[4,7,12,24],"To":[2,4,6,24],"We":[3,4,24],"While":[4,6],"You":[4,6],"_":24,"__handle_schedule_keyword":[10,13,18],"_add_or_collect_porv":[10,13,22],"_append_explicit_solution_includ":[10,13,18],"_apply_layer_pore_volume_correct":[10,13,22],"_array_lik":22,"_boundarymap":[10,13,22],"_build":3,"_check_cmdarg":[10,11,12],"_check_flow":[10,11,12],"_collect_removed_zcorn_indic":[10,13,14],"_collect_segmented_well_nam":[10,13,18],"_collect_vicinity_well_nam":[10,13,18],"_compact_permeability_properti":[10,13,14],"_corner_pore_volum":[10,13,22],"_correct_fluid_in_plac":[10,13,16],"_create_index_map":[10,13,16],"_distribute_vertical_pore_volum":[10,13,22],"_find_active_neighbor":[10,13,14],"_find_include_stat":[10,13,14],"_find_nearest_active_corner_cel":[10,13,22],"_get_well_completions_for_vicin":[10,13,22],"_global_index_to_ijk":[10,13,14],"_group_maximum_zero_bas":[10,13,14],"_group_minimum_zero_bas":[10,13,14],"_group_sum_zero_bas":[10,13,14],"_grouped_count":[10,13,14],"_grouped_first":[10,13,14],"_grouped_last":[10,13,14],"_grouped_max":[10,13,14],"_grouped_mean":[10,13,14],"_grouped_min":[10,13,14],"_grouped_mod":[10,13,14],"_grouped_sum":[10,13,14],"_handle_aquancon":[10,13,18],"_handle_aqucon":[10,13,18],"_handle_aqunum":[10,13,18],"_handle_bccon":[10,13,18],"_handle_bwpr":[10,13,18],"_handle_compseg":[10,13,18],"_handle_dimen":[10,13,18],"_handle_editnnc":[10,13,18],"_handle_equil":[10,13,18],"_handle_fault":[10,13,18],"_handle_grid_prop":[10,13,18],"_handle_mapax":[10,13,18],"_handle_multflt":[10,13,18],"_handle_multipli":[10,13,18],"_handle_multregt":[10,13,18],"_handle_op":[10,13,18],"_handle_pinch":[10,13,18],"_handle_prop":[10,13,18],"_handle_region":[10,13,18],"_handle_segmented_wel":[10,13,18],"_handle_sourc":[10,13,18],"_handle_wel":[10,13,18],"_handle_welldim":[10,13,18],"_handle_welseg":[10,13,18],"_identify_removed_pillar":[10,13,15],"_include_contains_endbox":[10,13,18],"_initialize_deck_data":[10,13,16],"_interleave_dual_properti":[10,13,14],"_map_east_boundari":[10,13,22],"_map_north_boundari":[10,13,22],"_map_south_boundari":[10,13,22],"_map_west_boundari":[10,13,22],"_mark_multiplier_keyword":[10,13,18],"_original_index":[10,13,22],"_parse_argu":[10,11,12],"_parserst":[10,13,18],"_pycopm":7,"_read_satnum":[10,13,14],"_render_templ":[10,13,15],"_scan_deck_fil":[10,13,18],"_submodel_index":[10,13,22],"_type":22,"_write_ert_fil":[10,13,15],"_write_grid_fil":[10,13,15],"_write_let_t":[10,13,15],"abl":4,"abov":[4,22,24],"abstract":5,"accept":[3,17],"accord":24,"account":2,"accumul":14,"accuraci":24,"achiev":[2,4,24],"across":[4,22,24],"action":[3,6],"activ":[2,4,6,7,12,14,18,22,24],"active_cell_method":12,"active_count":[13,22],"actnum":[7,14],"actual":4,"ad":5,"adapt":[7,9],"add":[2,3,4,6,7,12,14,15,24],"addiiton":24,"addit":[2,4,12,19,24],"adjust":[7,12,16],"adopt":24,"affin":24,"aggreg":[12,14],"al":2,"align":24,"allow":[2,4,7,24],"allow_inlin":15,"along":[7,12,19,22,24],"also":[3,4,6,7,14,17,24],"altern":[3,7],"among":[14,19,24],"anaconda":6,"analys":3,"analysi":3,"angl":[12,21],"ani":[2,3,15,24],"answer":3,"api":5,"appear":9,"append":18,"appli":[4,7,21,24],"applic":[4,7,9,24],"apply_boundary_pore_volume_correct":[10,13,22],"apporach":4,"approach":[0,2,3,4,7,9,24],"appropri":[12,24],"aproach":4,"apt":6,"aquancon":[13,18],"aqucon":[13,18],"aquif":18,"aqunum":[13,18],"area":24,"argument":[2,7,12,14,16,17,19],"argv":12,"arithmet":[7,24],"armon":[7,24],"around":[4,7,24],"array":[2,7,14,15,17,22,24],"assign":[7,14,22,24],"associ":21,"assum":4,"assur":24,"auto":3,"avail":[4,6,7,12,16],"averag":[7,24],"avoid":[4,7],"axe":[12,14,19],"axi":[7,12,14,15,16,17,18,19,22],"axis_valu":17,"azurewebsit":4,"back":2,"bar":4,"base":[1,7,12,14,17,18,19,20,22,23,24],"basetemp":3,"bash":4,"bc0407_hist01122006":4,"bccon":[13,18],"becaus":21,"becom":14,"befor":[2,3,6,7,15,20],"benchmark":23,"better":[4,24],"betweens":24,"bin":[2,3,6,7],"binari":[3,4,5,7],"black":[3,4],"block":18,"bool":[14,15,18],"boolean":[18,22],"bottom":[2,4],"bound":[16,22],"boundari":[2,4,7,14,22,24],"box":[7,12,22,24],"branch":6,"break":24,"brew":[4,5],"build":[2,3,5,7,16],"build_dual_porosity_grid":[10,13,14],"build_opm_mpi":6,"built":[4,6,7],"bwpr":[13,18],"c":[3,4,7,12,24],"c1":4,"c2":4,"ca":4,"calcit":23,"calcul":22,"call":[1,2,9],"can":[2,3,4,6,7,9,12,14,16,17,22,24],"capillari":24,"care":24,"cartesian":[14,24],"case":[1,4,12,14,16,24],"cbsfax":4,"cd":[4,6],"cell":[2,4,7,12,14,15,16,18,19,22,24],"cell_group":[13,14],"cell_mask":[13,22],"center":[0,4],"centr":[12,22],"certain":4,"cet_rainbow_bgyrm_35_85_c69":4,"cfd":23,"cfg":[14,15,20],"cformat":4,"chang":[2,3,14],"charact":12,"character":24,"check":[3,6,12,23],"checker":3,"checkout":6,"chosen":22,"ci_pycopm_maco":3,"ci_pycopm_ubuntu":[3,6],"cijk":17,"clabel":4,"class":[14,18,19,22],"cleanup_file_suffix":2,"clone":[4,6],"closest":[4,7],"cluster":[14,24],"cluster_id":14,"cluster_maximum":14,"cluster_minimum":14,"cmake":6,"cmdarg":[12,14,16,17,19],"cnum":4,"co2":[0,23],"coa_map":14,"coa_z":14,"coars":[2,4,7,14,15,16,18,24],"coarse_trani":[13,14],"coarse_tranx":[13,14],"coarsen":[1,2,5,7,9,10,12,13,15,16,17],"coarsen_and_write_properti":[10,13,14],"coarsen_corner_point_grid":[10,13,14],"coarsen_properti":[10,13,14],"coarsened_ax":[13,14],"coarseningmap":[10,13,14,16],"coarser":[2,4,7,9,24],"coarsern":4,"code":[3,6,10],"collabor":3,"collect":[16,22],"column":[14,22],"column_offset":22,"com":[4,6,7],"combin":[4,12],"comma":[7,17],"command":[2,4,6,12,14,16,17,19,20],"comment":4,"commit":3,"common":[6,24],"communic":24,"compact":15,"compar":4,"comparison":24,"compat":12,"compdat":[7,12,13,18],"complet":[2,14,18,22],"completion_removal_level":12,"completion_wel":[13,18],"complex":24,"complump":[13,18],"compseg":[13,18],"compsegs_wel":[13,18],"comput":[2,7,24],"con":14,"concaten":14,"concept":5,"condit":24,"confact":7,"confer":2,"config":6,"configur":[5,7,12,14,15,16,17,18,19,20,21,22],"configviadeck":[14,15,16,17,18,19,21,22],"configviatoml":[14,15,17,20],"conform":7,"connect":[4,7,12,14,24],"conquer":4,"consecut":15,"conserv":4,"consid":[2,7],"constant":15,"contain":[1,3,4,14,15,17,18,19,20,21,22],"content":[1,3,4,5,8,14,18],"continu":[12,14],"continuous_aggregation_method":12,"continuum":14,"contribut":[0,5,6],"control":[14,15],"convert":15,"coord":[14,15,19],"coord_indic":15,"coordin":[7,12,16,21,22],"copi":[3,4,6,9,14,19,20],"core":[1,8,10],"corner":[7,14,15,19,21,22,24],"corner_i":22,"corner_j":22,"corner_porv":22,"correct":[2,7,9,12,16,22,24],"correct_fluid_in_plac":12,"correl":24,"correspond":[2,4,7,18,22,24],"count":19,"cov":3,"cover":12,"cporv":2,"cr":[14,15],"creat":[3,4,6,7,9,14,16,17,19,22,24],"create_coarsening_map":[10,13,14],"create_coord_axis_map":[10,13,19],"create_deck":[10,13,16],"create_deck_config":[10,13,17],"create_refinement_map":[10,13,19],"create_vicinity_map":[10,13,22],"create_zcorn_axis_map":[10,13,19],"creation":7,"criterion":12,"criterium":7,"csp":23,"cssr":[0,4,6,7],"current":[2,4,6,7,9],"current_directori":6,"d":[4,7,12,24],"data":[2,3,4,7,12,14,15,16,18,19,22,24],"dataset":4,"date":2,"day":4,"dck":[14,15,16,18,19,21,22],"dcmake_build_typ":6,"deck":[1,2,3,5,7,12,14,15,16,17,18,19,21,22,24],"deck_dri":[7,12],"deck_encod":12,"deck_lin":18,"dedic":6,"default":[2,4,7,9,14,24],"default_valu":14,"defin":[2,4,9,14,24],"definit":[2,4,24],"degre":[4,7,12,21],"delax":4,"delet":2,"demonstr":4,"denot":22,"depend":[3,6,12,16,22,24],"depth":[12,14,24],"deriv":17,"describ":[3,4,7,9],"descript":[2,7],"destin":[12,15],"destination_surfac":19,"detail":12,"determin":16,"dev":[3,6],"develop":[4,6,7,9,24],"diamond":[7,12,22,24],"diamondxi":[4,7,12,22,24],"dict":[12,14],"differ":[1,2,4,7,24],"differenti":[4,7],"digit":[7,12,15],"dimen":[13,18],"dimens":[14,15,16,17,18,19,21,22],"dimension":15,"dir":[2,7],"direct":[2,4,7,9,12,14,18,24],"directori":[3,12,17],"discontinu":7,"discret":[12,14,24],"discrete_aggregation_method":12,"discuss":2,"disperc":7,"dist":2,"distanc":[14,22],"distpara":2,"distribut":[2,7,22,24],"divers":[7,24],"divid":[4,14,19],"dmar":2,"doc":3,"docs_configs_view":2,"docs_theory_coarsen":24,"docs_theory_refin":24,"docs_theory_submodel":24,"docs_theory_transform":24,"docs_via_config_drogon":4,"docs_via_deck_drogon":4,"docs_via_deck_dual_coarsen":4,"docs_via_deck_hello_world":4,"docs_via_deck_norn":4,"docs_via_deck_smeaheia":4,"docs_via_deck_spe10":4,"document":[2,3,4,6,7],"done":[6,7],"download":[4,6],"dpi":4,"drawback":24,"dri":[4,7,12,16],"drogon":[1,2,5,7,9,12,14],"drogon_2times_coars":4,"drogon_coars":4,"drogon_hist":4,"drogon_hist_prep_pycopm_dryrun":4,"drogon_hist_pycopm":4,"drogon_hist_pycopm_pycopm":4,"drogon_pycopm_comparison":4,"dual":[5,12,14,15],"dual_default":[13,14],"dual_porosity_criterion":12,"dual_trani":[13,14],"dual_tranx":[13,14],"dual_valu":14,"due":[4,24],"dure":[12,14,22,24],"duse_mpi":6,"dwith_ndebug":6,"dynam":23,"e":[2,3,4,6,7,9,12,24],"earli":9,"eas":24,"east":22,"edg":6,"editnnc":[13,18],"effect":24,"eg":2,"egrid":[3,16],"either":[6,7],"elaps":20,"elapsed_second":20,"els":6,"em":23,"email":3,"emltg":2,"emlto":2,"emphas":24,"empti":[4,7,14,22],"enabl":[4,7,14],"encod":[7,12,18],"end":[4,24],"endbox":18,"endnum":7,"ensembl":[2,4,7],"ensemble_experi":2,"ensemble_s":2,"ensemble_smooth":2,"enterpris":6,"entri":[2,7,12,24],"environ":[3,6],"eo":2,"eog":2,"eqlnum":7,"equal":[4,7,15,19,24],"equil":[2,4,12,13,18],"error":[2,4,9],"ert":[1,2,4,6,7,9,12,15,20],"ert_argu":2,"es_mda":2,"et":2,"etc":7,"everi":[14,15],"ew":2,"exampl":[2,3,5,9,14,24],"excel":24,"except":2,"exclud":22,"exectut":7,"execut":[1,2,3,4,6,7,9,12,20,24],"execution_mod":[2,12],"expans":0,"expect":15,"explicit":[4,7,12,17,18],"export":[3,6],"exprecc":[0,5],"extend":[3,14,15],"extern":12,"extract":[4,7,16,18,22,24],"extract_vicinity_grid":[10,13,22],"f":[3,4,7,12],"face":[7,24],"factor":12,"factori":[14,18,22],"fals":[15,18],"familiar":[2,3],"faster":24,"fault":[4,7,13,18,24],"favourit":[7,9],"featur":[0,4,24],"fewer":24,"fgip":7,"fi":6,"field":[4,18,24],"figur":[2,4,9,24],"file":[1,3,5,6,7,12,14,15,16,17,18,22,24],"file_path":18,"file_suffix":15,"filenam":12,"files_writ":[10,13],"filter":18,"final":[1,2,4,15],"find":[9,14,15,18],"find_multiplier_keyword":[10,13,18],"fine":[2,14],"finner":24,"fipnum":[7,24],"first":[2,4,7,14,16],"five":4,"fix":3,"flag":[2,3,4,7,18],"flatten":[14,15,19,22],"flexibl":[7,24],"float":[7,12,14,17,20,22],"flow":[1,2,3,5,7,12,15,16,20,23,24],"flow_cmdarg":12,"flow_command":[2,12],"flow_maco":3,"flow_toml":12,"fluid":[16,24],"fluidflow":23,"fluxnum":7,"focus":[4,7,9,24],"foip":[4,7],"folder":[1,3,4,5,6,7],"follow":[2,4,6,7,9,12,21,24],"fopr":4,"fork":[0,3],"format":[2,3,4,15,24],"format_opm_compact_valu":[10,13,15],"former":24,"formula":5,"found":18,"four":[4,22,24],"frac":24,"fraction":19,"fractur":[12,14,15],"framework":[1,4,23],"freeglut3":6,"frequent":24,"friend":[4,7],"front":24,"full":[2,4,9],"function":[1,2,4,14,15,16,20,22,24],"fund":0,"g":[2,3,4,6,7,9,12,24],"gas":[4,7,12,16],"generat":[1,2,3,4,7,9,12,14,15,16,17,18,19,20,21,22,23,24],"generate_deck":[10,13],"generate_postprocessing_plot":[10,13,20],"geolog":[2,4,7,9,24],"geometr":[22,24],"geometri":[19,21,22,24],"geophys":24,"get":[3,6],"gif":[4,23],"git":[4,6],"github":[2,3,4,6,7],"give":[4,9,24],"given":[2,4,7,24],"global":[14,15,22,24],"global_index":14,"go":4,"good":4,"graphic":5,"grdecl":15,"greater":14,"grid":[1,4,5,6,7,9,12,13,14,15,16,17,18,19,21,22],"grid_transform":[12,21],"ground":5,"group":[12,14,24],"group_cod":14,"gui":2,"h":2,"handi":24,"handl":[4,18,24],"handler":18,"harmon":[7,24],"has_edit":[13,18],"has_main_multflt":18,"hat":6,"hdf5":6,"head":18,"header":15,"hello":[3,5],"hello_world":[3,4],"hello_world_pycopm":[3,4],"hello_world_pycopm_pycopm":4,"help":[3,6],"hexahedron":24,"higher":4,"highest":14,"highlight":4,"histori":[2,4,7,9,23,24],"history_matching_end_d":2,"hm":[2,4],"home":7,"homebrew":6,"honor":[2,24],"horda":0,"horizont":[14,19,24],"host":7,"howev":4,"html":3,"https":[4,6,7],"hv":18,"identifi":[4,12,14,15,18,22],"ignor":[3,14],"ijk":[12,14],"ilustr":4,"imag":23,"imbnum":7,"impact":24,"implement":[4,7,12,24],"import":[3,24],"inact":[4,7,22,24],"inc":[4,7,15],"includ":[4,6,7,9,12,14,15,16,18,19,21,22],"include_lin":14,"include_prefix":12,"inclus":[17,22],"incompat":12,"increas":[4,24],"index":[2,5,14,16,18,22],"indic":[4,7,12,14,15,16,17,18,19,22,24],"induc":23,"industri":24,"inform":[2,24],"init":[2,4,14,16],"initi":[2,4,7,12,18,24],"initialization_method":2,"inj":4,"injector":4,"inlin":15,"input":[2,4,5,7,12,16,17,18,19,24],"input_deck_path":12,"input_fil":[12,17],"input_valu":[10,13],"insert":14,"insid":[4,6,22,24],"insight":24,"inspect":2,"instal":[3,4,5,7],"instead":[2,4,12,24],"instruct":6,"int":[14,15,17,18,19,20,22],"integ":4,"integr":2,"interest":[6,23,24],"interim":6,"interleav":14,"intermedi":14,"internal":7,"interpol":19,"interval":[2,4,7,12,14,19,24],"introduct":[5,9,24],"invalid":12,"involv":[4,24],"iso":[7,12],"isol":4,"issu":[0,4,5],"item":7,"iter":[2,4],"iterative_ensemble_smooth":2,"j":[2,4,7,12,14,15,22,24],"j5":6,"jk":7,"job":[1,15,20],"jump":[4,12,14],"jump_threshold":12,"just":7,"k":[2,4,7,12,14,22,24],"keep":[2,4,7],"key":[12,24],"keyword":[4,15,16,18],"kh":7,"kilomet":24,"km":4,"kwr":18,"l":[4,6,7,12],"l127":4,"l242":4,"label":4,"larg":24,"last":[2,7,14,16],"later":2,"latter":4,"launcher":12,"layer":[7,14,22,24],"layer_external_porv":[13,22],"layer_index":22,"layer_max_i":[13,22],"layer_max_j":[13,22],"layer_min_i":[13,22],"layer_min_j":[13,22],"layer_selected_porv":[13,22],"leakag":23,"least":[4,7],"left":[4,9,12,24],"legend":4,"less":4,"let":[2,15],"let_paramet":[2,20],"level":[7,12,18],"lg":2,"lib":6,"lie":24,"like":[4,24],"line":[2,3,4,6,12,14,15,16,17,18,19,21,22,24],"linear":19,"link":[4,6],"lint":6,"linux":[4,5],"list":[12,14,15,16,17,18,19,21,22],"lmltg":2,"lmlto":2,"lo":2,"load":[16,17],"load_toml_config":[10,13,17],"loc":4,"local":[3,17,24],"locat":[1,4,7,9,24],"log":2,"look":3,"loop":4,"lost":2,"lot":[4,24],"lower":[4,24],"lw":2,"m":[3,4,6,7,12],"machin":[2,6,7,12,15],"maco":[3,4,5],"macport":6,"made":[4,20,24],"main":[1,2,3,4,10,11,12,18],"maintain":3,"major":12,"make":[3,4,6,7,24],"mako":[1,15],"mamba":6,"manag":[6,24],"mantain":3,"manual":7,"map":[7,12,14,15,16,18,19,22,24],"map_nnc_transmiss":[10,13,14],"map_vicinity_properti":[10,13,22],"mapax":[4,13,18],"mark":14,"mask":[14,22],"master":6,"match":[2,4,7,9,12,16,23,24],"mathbb":24,"matrix":[12,14,15],"matrix_mask":[13,14],"max":[2,4,7,12,14,24],"max_i":[13,22],"max_j":[13,22],"max_k":[13,22],"max_parallel_r":2,"max_realization_runtime_second":2,"maximum":[2,4,14,22],"maxpermz":4,"may":15,"mean":[2,4,7,12,14,24],"measur":24,"member":4,"memori":2,"mention":[4,24],"merg":[3,14],"messag":12,"metadata":17,"meter":[7,24],"method":[12,22,24],"micp":5,"microbi":23,"microsystem":23,"middl":4,"might":[3,4,6,9,23,24],"million":4,"min":[2,4,7,12,14,24],"min_i":[13,22],"min_j":[13,22],"min_k":[13,22],"min_successful_r":2,"mini":7,"miniforg":6,"minimum":[2,14,22],"minj":7,"miscnum":7,"miss":3,"missmatch":24,"mkdir":6,"mode":[2,4,7,12,14,20,24],"model":[2,4,7,9,12,14,18,23,24],"model3":24,"model6":4,"model_nam":2,"modif":[2,17],"modifi":[2,4,6,7,9,12,16,18,19,21],"modified_deck":[14,15,16,18,19,21,22],"modul":[1,5,8,23],"mpi":[4,6],"mpirun":2,"mult":[7,18],"multflt":[13,18],"multi":[4,18],"multipli":[4,7,13,14,18,24],"multnum":7,"multregt":[4,13,18],"multx":[4,18],"multz":[4,18],"must":[2,4,24],"mypi":3,"n":[3,4,7,12,14,15,19],"name":[2,4,7,12,14,15,22],"name_of_input_fil":7,"name_of_script":4,"namespac":[12,14,16,17,19],"nan":14,"natur":24,"ndarray":[14,15,17,18,19,22],"near":23,"nearest":[14,22],"need":[2,3,4,6,7,9],"neighbor":24,"neighbor_indic":14,"neighbour":[4,7,14,24],"neighbourhood":[7,12,22,24],"neither":12,"nest":18,"net":[4,7,12,14],"new":[0,3,4,7,24],"newton":2,"next":2,"nice":[3,4,6],"nnc":[14,24],"nnc_text":[13,14],"non":[4,7,12,14,24],"none":[12,14,15,16,18,19,20,21,22],"norn":[1,2,5,7,9,12,14,24],"norne_atw2013":4,"north":22,"northeast":22,"northwest":22,"notat":[15,24],"note":[3,4],"noth":7,"now":4,"np":2,"nrwo":18,"ntg":[2,4],"num_dig":15,"number":[2,4,7,12,14,15,19,20,22,24],"number_group":14,"number_t":[15,20],"number_valu":15,"numer":12,"numpi":22,"nx":14,"nxyz":14,"nz":14,"o":[3,4,7,9,12],"object":[14,17,18,19,22],"observ":[4,15],"observation_minimum_error":2,"observation_relative_error":2,"offset":[13,14,22],"often":24,"oil":[4,7,12,16],"ok":4,"old":4,"omega":24,"omit":12,"onc":24,"one":[2,4,6,7,9,12,14,15,17,19,22,24],"onli":[2,4,7,17,21,24],"onlin":6,"onto":[14,19,22],"open":[4,16,23,24],"openbla":6,"oper":[4,12,13,18,24],"opernum":[7,15],"opm":[1,2,3,5,7,12,15,16,18,20,23,24],"opt":6,"optim":[7,9],"option":[2,4,7,12,15,16,24],"optvic":22,"order":[2,4,7,14,15,17,18,19,22,24],"org":6,"origin":[1,4,12,14,15,16,18,19,21,22],"original_lay":22,"original_ni":19,"original_nx":19,"otherwis":[9,14,15],"outisd":24,"output":[2,3,4,5,7,14,15,16,19,20,21,22],"output_deck_nam":12,"output_directori":[12,17],"output_ni":19,"output_nx":19,"output_path":15,"output_porv":[14,16,22],"outsid":[4,7,22,24],"overlap":24,"overview":[2,5],"p":[4,7,12,24],"packag":[1,3,4,5,8],"page":[3,5],"parallel":2,"paramet":[2,4,7,12,14,15,16,17,18,19,20,21,22,24],"paraview":4,"pars":[12,16,17,18],"parse_axis_modif":[10,13,17],"parser":[17,18],"parser_deck":[10,13],"part":[4,24],"particular":12,"pass":[3,4,15],"path":[2,3,4,7,9,12,15,17,18],"peqvr":7,"per":[4,7,14,16,19,22],"perform":[4,7,21,24],"perm":4,"perm_max":2,"perm_min":2,"permeabl":[2,4,24],"permi":[2,4,7,14],"permx":[2,4,7,14],"permz":[2,4,7,14],"pet":[7,9],"phi_":24,"physic":12,"pilar":[2,4,7],"pillar":[2,14,24],"pinch":[13,18],"pip":[3,4,6],"pkg":6,"pkg_config_path":6,"pkgconfig":6,"place":[4,12,16,19,20,24],"plane":[7,24],"platform":[0,6],"pleas":[0,3],"plopm":[2,4,5,24],"plot":[9,20],"plus":7,"pm":7,"png":4,"pngs":[4,23],"pofff":5,"point":[7,12,14,15,17,19,21,22,24],"polygon":[7,12,22,24],"popul":[14,16,17],"pore":[4,7,12,14,16,19,22,24],"pore_volum":[13,22],"pore_volume_correct":[2,12,22],"poro":[2,4,7],"poros":[2,4,14,15,24],"porv":[2,4,7,19],"posit":[7,12,19],"possibl":[3,24],"postprocess":[9,20],"practic":4,"prealloc":19,"preced":14,"precipit":23,"precis":[7,12,15,17],"prefer":3,"prefix":[12,15],"prep":[7,12],"prep_deck":[7,12],"preprocess":[16,20],"preprocessing_path":15,"prerequisit":6,"preserv":[12,15,18,19,24],"pressur":[4,24],"prevent":12,"previous":[3,4,6],"previous_complet":[13,18],"print":4,"problem":5,"process":12,"process_deck":[10,13,18],"process_edit":[13,18],"product":[4,24],"project":[0,4,6,15,17,20,23,24],"prop":[4,13,18],"properti":[2,4,7,12,14,15,16,18,19,21,22,24],"property_nam":15,"property_valu":[14,15],"provid":[2,7,16,18,24],"proxi":7,"proyect":4,"prs":3,"prt":2,"pull":[0,3],"push":3,"pushd":3,"pv":[2,4,7,24],"pvmean":[4,7,12,24],"pvtnum":7,"pvweightedperm":4,"pwd":6,"py314":3,"pycopm":[2,3,4,6,7,9,24],"pycopm_":7,"pycopm_porv":7,"pylint":3,"pymm":5,"pyocpm":1,"pyopmnearwel":5,"pyopmspe11":5,"pytest":3,"python":[3,5,23],"python3":[3,6],"q":[4,7,12],"quantiti":24,"question":24,"quick":23,"r":[3,6,7,12],"rais":[0,3,12],"random":2,"random_se":2,"rang":[7,17,18,24],"rate":[4,24],"ratio":24,"read":[4,7,12,14],"realiz":[2,20],"receiv":[15,22],"recent":6,"recis":7,"recommend":[4,9],"record":[14,18],"red":6,"redistribut":[4,14],"redistribute_removed_pore_volum":[10,13,14],"reduc":[4,12,14],"refer":[4,14,15,17,24],"reference_simul":1,"reference_to_coars":[13,14],"refin":[1,2,4,5,7,9,10,12,13,16,18],"refine_grid":[10,13,19],"refine_properti":[10,13,19],"refine_zcorn_surfac":[10,13,19],"refined_cell_count":[13,19],"refinement_valu":19,"refinementmap":[10,13,16,19],"reftocoa":15,"regard":[2,24],"region":[2,4,7,12,13,18,22,23,24],"regular":24,"relat":[4,5,15,19,24],"releas":6,"relev":4,"remain":[2,14],"remap":22,"remedi":23,"remov":[2,4,7,12,14,15,24],"removal_indic":14,"removal_mask":14,"renam":4,"render":[15,20],"repeat":15,"replac":[3,4,18],"repo":[3,6],"report":5,"repositori":[3,4,6,7],"repres":16,"represent":17,"reproduc":2,"request":[0,3,12],"requested_ijk":12,"requir":[3,4,6,18],"reservoir":[7,14,19,21,22,23,24],"resinsight":[2,4,9,24],"resolv":18,"resourc":0,"resource_directori":17,"respect":[2,7,24],"rest":[4,7],"restart":[14,16],"restrict":[7,12],"result":[2,4,7,9,12,22,24],"retain":[12,14,18],"retained_wel":[13,18],"return":[7,12,14,15,17,18,19,22],"review":3,"rewrit":[18,21],"rewritten":18,"right":[4,9,24],"rightarrow":24,"rise":3,"roadmap":5,"rock":[2,24],"rock_property_set":2,"rocknum":7,"rotat":[4,7,9,21,24],"rotatexi":[4,7,12,21,24],"rotatexz":[7,12,21],"rotateyz":[7,12,21],"round":15,"round_like_":[10,13,15],"row":[14,22],"row_offset":22,"ruff":3,"rule":5,"run":[1,2,3,4,6,7,9,12,16,20,24],"run_simul":[10,13,20],"runs_execut":[10,13],"runtim":2,"rx":[4,7,12],"ry":[4,12],"rz":12,"s":[2,3,4,7,12,19,24],"sandv":2,"sat":2,"satnum":[2,7,14,24],"satnum_generation_method":2,"satnum_opm":14,"satur":[2,4,14,15,20,24],"saturation_function_method":2,"save":[2,4,7],"scale":[2,7,9,12,21,24],"scan":18,"sch":4,"schedul":[4,15,18],"schedule_keyword":[13,18],"score":4,"screenshot":[4,9,24],"script":[1,3,4,6,20,24],"scriptsiz":24,"search":5,"second":[2,4,14,24],"section":[4,7,18],"see":[2,3,4,6,7,24],"seed":2,"seek":5,"seem":4,"segment":18,"segmented_wel":[13,18],"seismic":24,"select":[2,9,12,14,16,18,22,24],"selector":[13,22],"send":3,"separ":[7,12,13,14,17,18],"set":[2,4,6,7,9,12,14,18,20,22,24],"setuptool":6,"seven":3,"sever":24,"sgas":4,"sh":[2,4,6,24],"shape":[2,13,15,22,24],"shift":24,"short":16,"show":[4,9,24],"shown":2,"side":24,"signific":[7,12,15],"significant_digit":[12,15,17],"similar":24,"simpl":24,"simpli":[6,24],"simul":[2,3,4,6,7,9,12,17,20,23,24],"simulation_model":4,"sinc":[4,24],"singl":[2,3,14,20],"site":23,"six":24,"size":[14,24],"skip":6,"skip_block":[13,18],"sm":4,"smaller":24,"smallest":14,"smeaheia":[5,7],"softwar":5,"solut":[4,7,12,18,24],"soon":3,"sort":20,"sourc":[3,4,5,13,14,15,18,19,21,22,23],"source_indic":[13,19,22],"source_surfac":19,"south":22,"southeast":22,"southwest":22,"spatial":24,"spe10":5,"spe10_model2":4,"spe10_model2_pycopm":4,"spe11":23,"special":[4,24],"specif":[2,4,6,12,14,17,21,22],"specifi":7,"speed":24,"squash":3,"src":3,"stage":12,"standard":[2,4,24],"start":20,"state":18,"statement":[14,18],"static":[3,4,7,12],"statoil_feasibility_sim_model_with_depletion_kross_inj_sector_20":4,"statoil_feasibility_sim_model_with_depletion_kross_inj_sector_20_prep_pycopm_dryrun":4,"statoil_feasibility_sim_model_with_depletion_kross_inj_sector_20_pycopm":4,"step":[3,6,12],"still":4,"stop":2,"storag":[0,2,23],"store":[14,18,19,22],"str":[12,14,15,16,17,18,19,21,22],"straight":24,"strategi":22,"string":22,"studi":[2,4,7,9,12,20,23,24],"sub":[4,7],"subdivis":19,"subfig":4,"subgrid":22,"submodel":[1,2,4,5,7,9,12,16,18,22],"submodel_index":22,"submodul":[1,8,10],"subpackag":[1,5,8],"substract":4,"subsurfac":[0,4,24],"subsystem":6,"succed":6,"succeed":[2,3],"success":[2,16],"sudmodel":24,"sudo":6,"suffix":[2,15],"sum":[7,14,24],"sum_":24,"summari":[4,9,14],"suppli":12,"support":[2,5,6,7,12,14,18,21],"suptitl":4,"surfac":[14,19,24],"sustain":0,"swatinit":[4,7],"syntax":[12,15],"sys":12,"system":[3,7,21],"systemexit":12,"t":[4,7,12,24],"t_":24,"tabl":[14,15,20,24],"tag":6,"tailor":4,"taken":24,"tap":[4,6],"target":3,"tcpu":4,"techniqu":[7,24],"templat":[7,15,17],"template_path":15,"template_script":1,"temporari":18,"term":3,"termin":[2,4,6,7,24],"test":[1,2,3,4,6,7,24],"test_output":3,"test_run":2,"text":[4,7,14,15],"textbook":24,"tg":2,"thconr":7,"theori":[4,5],"thick":24,"thousand":4,"three":[4,7,17,18,24],"threshold":12,"time":[2,4,20,24],"tmltg":2,"tmlto":2,"tog":2,"toml":[2,4,7,12,14,15,17,20],"tool":[0,3,4,6,7,9,23,24],"top":[2,4,12,24],"total":[4,14,22],"track":18,"trade":24,"trailing_column":22,"trailing_row":22,"trani":14,"transfer":14,"transform":[1,2,4,5,7,9,10,12,13,16],"transform_grid":[10,13,21],"transform_properti":[10,13,21],"translat":[7,9,12,21,24],"transmis":[7,24],"transmiss":[7,12,14,15,24],"transmissibili":7,"transmissibility_coarsening_method":12,"transsmiss":24,"tranx":14,"tri":[3,4,6,7],"trial":4,"trim":24,"true":[4,15],"trust":[4,6],"tstep":4,"tune":7,"tunit":4,"tupl":[14,15,17,18,19,22],"turn":6,"tutori":[3,6],"tw":2,"two":[4,7,12,16,24],"txt":[3,6],"type":[2,4,6,12,14,15,17,18,19,22,23],"typic":24,"ual":7,"ubuntu":[3,6],"unassign":22,"uncertainti":24,"unchang":21,"uniform":[2,7,12,17],"unknown":12,"unlimit":2,"unprocess":12,"unrefin":24,"unwant":12,"updat":[14,16,18,19,21,22],"upgrad":6,"upper":24,"upscal":[7,24],"use":[0,1,2,3,4,6,7,9,12,14,16,17,18,22,23,24],"user":[2,3,4,6,7],"utf":15,"utf8":[7,12],"util":[1,8,10,12],"v":[4,7,12,15,24],"v2025":6,"v2026":6,"valid":[12,16,17],"valu":[2,4,7,12,14,15,16,17,18,19,21,22,24],"variabl":[3,7,15],"vector":4,"venv":6,"version":[3,6,10],"vertic":[14,24],"vertical_transfer_en":[13,14],"via":[2,5,6,7],"vicin":[4,7,10,12,13,16,18],"vicinity_specif":12,"vicinitymap":[10,13,16,18,22],"view":[4,18,24],"vile":4,"virtual":6,"visaluaz":4,"visit":[2,3],"visual":[4,9,24],"volum":[4,7,12,14,16,19,22,24],"vpycopm":6,"vtk":4,"vtks":23,"w":[4,7,12],"water":[4,7],"webviz":4,"weight":[2,4,7,12,24],"welcom":[0,3],"well":[2,3,4,7,12,14,18,22,23,24],"well_cel":[13,22],"well_cell_indic":18,"well_indic":[13,22],"wellcind":[14,18],"welldim":[13,18],"welln":7,"wellspec":4,"welseg":[13,18],"welspec":[13,18],"west":22,"wgpr":2,"wheel":6,"whether":[14,18],"whole":7,"whose":[14,16,19,22],"wide":24,"will":[2,3,4,7],"window":[4,5],"within":19,"without":[2,4,9,14],"wopr":2,"work":[3,6,7,12],"workflow":[12,14,16,17,20,24],"world":[3,5],"write":[2,4,6,7,12,14,15,17,19,22],"write_coarsened_model_fil":[10,13,15],"write_compact_property_fil":[10,13,15],"write_dual_properti":[10,13,15],"write_explicit_solut":12,"write_grid":[10,13,15],"write_includ":[10,13,15],"write_porv":[10,13,15],"write_properti":[10,13,15],"write_property_inc":[10,13,15],"write_reference_to_coarse_map":[10,13,15],"written":[14,15,22,24],"wsl":6,"wwpr":2,"x":[2,4,7,12,13,14,17,18,19,21,22,24],"x_coarsen":[2,12,14],"x_refin":[12,19],"xformat":4,"xi_":24,"xlnum":4,"xplicit":7,"xunit":4,"xy":[7,12,22,24],"xypolygon":[4,7,24],"xyz":[7,24],"xz":14,"y":[2,4,6,7,12,13,14,17,18,19,21,22,24],"y_coarsen":[2,12,14],"y_refin":[12,19],"yformat":4,"ylabel":4,"ylnum":4,"yml":[3,6],"yournam":3,"yunit":4,"z":[2,4,7,12,13,14,17,18,19,21,22,24],"z_coarsen":[2,12,14],"z_refin":[12,19],"zc":[14,15],"zcorn":[14,15,19],"zcorn_indic":15,"zcorn_x_fract":19,"zcorn_x_indic":19,"zcorn_y_fract":19,"zcorn_y_indic":19,"zero":[14,15,22,24]},"titles":["About pycopm","pycopm Python API","Configuration file","Contributing","Examples","Welcome to pycopm\u2019s documentation!","Installation","Introduction","pycopm","Output folder","pycopm package","pycopm.core package","pycopm.core.pycopm module","pycopm.utils package","pycopm.utils.coarsening module","pycopm.utils.files_writer module","pycopm.utils.generate_decks module","pycopm.utils.input_values module","pycopm.utils.parser_deck module","pycopm.utils.refinement module","pycopm.utils.runs_executer module","pycopm.utils.transformation module","pycopm.utils.vicinity module","Related","Theory"],"titleterms":{"About":0,"abstract":4,"ad":23,"api":1,"binari":6,"brew":6,"build":6,"coarsen":[4,14,24],"concept":7,"configur":[2,4,9],"content":[10,11,13],"contribut":3,"core":[11,12],"deck":[4,9],"document":5,"drogon":4,"dual":4,"exampl":4,"exprecc":23,"file":[2,4,9],"files_writ":15,"flow":[4,6,9],"folder":9,"formula":6,"generate_deck":16,"graphic":4,"grid":24,"ground":3,"hello":4,"indic":5,"input":9,"input_valu":17,"instal":6,"introduct":7,"issu":3,"linux":6,"maco":6,"micp":23,"modul":[10,11,12,13,14,15,16,17,18,19,20,21,22],"norn":4,"opm":[4,6,9],"output":9,"overview":7,"packag":[6,10,11,13],"parser_deck":18,"plopm":23,"pofff":23,"problem":3,"pycopm":[0,1,5,8,10,11,12,13,14,15,16,17,18,19,20,21,22],"pymm":23,"pyopmnearwel":23,"pyopmspe11":23,"python":[1,6],"refin":[19,24],"relat":23,"report":3,"roadmap":7,"rule":3,"runs_execut":20,"s":5,"seek":3,"smeaheia":4,"softwar":3,"sourc":6,"spe10":4,"submodel":24,"submodul":[11,13],"subpackag":10,"support":3,"tabl":5,"theori":24,"transform":[21,24],"util":[13,14,15,16,17,18,19,20,21,22],"via":[4,9],"vicin":22,"welcom":5,"window":6,"world":4}}) \ No newline at end of file +Search.setIndex({"alltitles":{"-a/--active_cell_methods ":[[31,"a-active-cell-methods-methods"]],"-c/--coarsening ":[[33,"c-coarsening-x-y-z"]],"-d/--grid_transformation ":[[35,"d-grid-transformation-specification"]],"-dual/--dual_porosity_criterion ":[[35,"dual-dual-porosity-criterion-criterion"]],"-e/--deck_encoding ":[[34,"e-deck-encoding-encoding"]],"-explicit/--write_explicit_solution <0|1>":[[34,"explicit-write-explicit-solution-0-1"]],"-f/--flow_command ":[[36,"f-flow-command-command"]],"-g/--refinement ":[[33,"g-refinement-x-y-z"]],"-i/--input_deck_path ":[[36,"i-input-deck-path-input"]],"-ijk/--requested_ijk ":[[34,"ijk-requested-ijk-i-j-k"]],"-j/--jump_thresholds ":[[31,"j-jump-thresholds-values"]],"-l/--include_prefix ":[[34,"l-include-prefix-prefix"]],"-m/--execution_mode ":[[36,"m-execution-mode-mode"]],"-n/--discrete_aggregation_method ":[[31,"n-discrete-aggregation-method-methods"]],"-o/--output_directory ":[[36,"o-output-directory-folder"]],"-p/--pore_volume_correction <0|1|2|3|4>":[[31,"p-pore-volume-correction-0-1-2-3-4"]],"-precision/--significant_digits <0..15>":[[34,"precision-significant-digits-0-15"]],"-q/--correct_fluid_in_place <0|1>":[[31,"q-correct-fluid-in-place-0-1"]],"-r/--completion_removal_level <0|1|2>":[[34,"r-completion-removal-level-0-1-2"]],"-rx/--x_refinement ":[[33,"rx-x-refinement-values"]],"-ry/--y_refinement ":[[33,"ry-y-refinement-values"]],"-rz/--z_refinement ":[[33,"rz-z-refinement-values"]],"-s/--continuous_aggregation_method ":[[31,"s-continuous-aggregation-method-methods"]],"-t/--transmissibility_coarsening_method <0|1|2>":[[31,"t-transmissibility-coarsening-method-0-1-2"]],"-v/--vicinity_specification ":[[35,"v-vicinity-specification-specification"]],"-w/--output_deck_name ":[[34,"w-output-deck-name-name"]],"-x/--x_coarsening ":[[33,"x-x-coarsening-values"]],"-y/--y_coarsening ":[[33,"y-y-coarsening-values"]],"-z/--z_coarsening ":[[33,"z-z-coarsening-values"]],"About the project":[[30,"about-the-project"]],"Binary packages":[[29,"binary-packages"]],"Brew formula for macOS":[[29,"brew-formula-for-macos"]],"Citation":[[30,"citation"]],"Command-line reference":[[16,null]],"Common commands":[[16,"common-commands"]],"Complete legacy configuration reference":[[17,null]],"Complete workflow":[[45,"complete-workflow"]],"Concept":[[30,"concept"]],"Contribute to the software":[[19,"contribute-to-the-software"]],"Contributing":[[19,null]],"Core workflows":[[30,"core-workflows"]],"Deck rewriting and output":[[34,null]],"Deck workflow scope":[[20,"deck-workflow-scope"]],"Deck-based workflow (recommended)":[[37,"deck-based-workflow-recommended"]],"Drogon":[[22,null]],"Dual coarsening":[[23,null]],"Examples":[[20,null]],"Graphical abstract":[[24,null]],"Grid coarsening":[[40,null]],"Grid coarsening and refinement":[[33,null]],"Grid refinement":[[41,null]],"Ground Rules":[[19,"ground-rules"]],"Hello World: generic deck workflow":[[45,null]],"Input and workflow":[[36,null]],"Installation":[[29,null]],"Introduction":[[30,null]],"Learning objectives":[[45,"learning-objectives"]],"Legacy TOML configuration":[[18,null]],"Legacy TOML workflow":[[37,"legacy-toml-workflow"]],"Legacy configuration workflows":[[21,null]],"Module contents":[[2,"module-pycopm"],[3,"module-pycopm.core"],[5,"module-pycopm.utils"]],"Next steps":[[29,"next-steps"],[45,"next-steps"]],"Norne":[[25,null]],"OPM Flow":[[29,"opm-flow"]],"Option compatibility and validation":[[32,null]],"Output folder":[[37,null]],"Project evolution":[[30,"project-evolution"]],"Property aggregation and conservation":[[31,null]],"Python API":[[0,null]],"Python package":[[29,"python-package"]],"Quick installation":[[28,"quick-installation"]],"Quick start":[[28,"quick-start"]],"Recommended workflow":[[30,"recommended-workflow"]],"Related projects":[[38,null]],"Report issues or problems":[[19,"report-issues-or-problems"]],"SPE10":[[27,null]],"Seek support":[[19,"seek-support"]],"Smeaheia":[[26,null]],"Source build in Linux/Windows":[[29,"source-build-in-linux-windows"]],"Source build in macOS":[[29,"source-build-in-macos"]],"Submodels":[[42,null]],"Submodels, transformations, and dual coarsening":[[35,null]],"Submodules":[[3,"submodules"],[5,"submodules"]],"Subpackages":[[2,"subpackages"]],"Theory and methods":[[39,null]],"Transformations":[[43,null]],"Tutorial":[[44,null]],"Verify the installation":[[29,"verify-the-installation"]],"Visualization and reproducibility":[[20,"visualization-and-reproducibility"]],"What can pycopm do?":[[28,"what-can-pycopm-do"]],"Where to continue":[[30,"where-to-continue"]],"pycopm":[[1,null],[28,null]],"pycopm package":[[2,null]],"pycopm.core package":[[3,null]],"pycopm.core.pycopm module":[[4,null]],"pycopm.utils package":[[5,null]],"pycopm.utils.coarsening module":[[6,null]],"pycopm.utils.files_writer module":[[7,null]],"pycopm.utils.generate_decks module":[[8,null]],"pycopm.utils.input_values module":[[9,null]],"pycopm.utils.parser_deck module":[[10,null]],"pycopm.utils.refinement module":[[11,null]],"pycopm.utils.runs_executer module":[[12,null]],"pycopm.utils.terminal module":[[13,null]],"pycopm.utils.transformation module":[[14,null]],"pycopm.utils.vicinity module":[[15,null]]},"docnames":["api","api/modules","api/pycopm","api/pycopm.core","api/pycopm.core.pycopm","api/pycopm.utils","api/pycopm.utils.coarsening","api/pycopm.utils.files_writer","api/pycopm.utils.generate_decks","api/pycopm.utils.input_values","api/pycopm.utils.parser_deck","api/pycopm.utils.refinement","api/pycopm.utils.runs_executer","api/pycopm.utils.terminal","api/pycopm.utils.transformation","api/pycopm.utils.vicinity","command-line","configuration/complete-reference","configuration_file","contributing","examples","examples/configuration-workflows","examples/drogon","examples/dual-coarsening","examples/graphical-abstract","examples/norne","examples/smeaheia","examples/spe10","index","installation","introduction","options/aggregation","options/compatibility","options/grid","options/output","options/submodels-transformations","options/workflow","output_folder","related","theory","theory/coarsening","theory/refinement","theory/submodels","theory/transformations","tutorial","tutorial/hello-world"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1},"filenames":["api.rst","api/modules.rst","api/pycopm.rst","api/pycopm.core.rst","api/pycopm.core.pycopm.rst","api/pycopm.utils.rst","api/pycopm.utils.coarsening.rst","api/pycopm.utils.files_writer.rst","api/pycopm.utils.generate_decks.rst","api/pycopm.utils.input_values.rst","api/pycopm.utils.parser_deck.rst","api/pycopm.utils.refinement.rst","api/pycopm.utils.runs_executer.rst","api/pycopm.utils.terminal.rst","api/pycopm.utils.transformation.rst","api/pycopm.utils.vicinity.rst","command-line.rst","configuration/complete-reference.rst","configuration_file.rst","contributing.rst","examples.rst","examples/configuration-workflows.rst","examples/drogon.rst","examples/dual-coarsening.rst","examples/graphical-abstract.rst","examples/norne.rst","examples/smeaheia.rst","examples/spe10.rst","index.rst","installation.rst","introduction.rst","options/aggregation.rst","options/compatibility.rst","options/grid.rst","options/output.rst","options/submodels-transformations.rst","options/workflow.rst","output_folder.rst","related.rst","theory.rst","theory/coarsening.rst","theory/refinement.rst","theory/submodels.rst","theory/transformations.rst","tutorial.rst","tutorial/hello-world.rst"],"indexentries":{},"objects":{"":[[2,0,0,"-","pycopm"]],"pycopm":[[3,0,0,"-","core"],[5,0,0,"-","utils"],[31,4,1,"cmdoption-pycopm-a","--active_cell_methods"],[33,4,1,"cmdoption-pycopm-c","--coarsening"],[34,4,1,"cmdoption-pycopm-r","--completion_removal_level"],[31,4,1,"cmdoption-pycopm-s","--continuous_aggregation_method"],[31,4,1,"cmdoption-pycopm-q","--correct_fluid_in_place"],[34,4,1,"cmdoption-pycopm-e","--deck_encoding"],[31,4,1,"cmdoption-pycopm-n","--discrete_aggregation_method"],[35,4,1,"cmdoption-pycopm-dual","--dual_porosity_criterion"],[36,4,1,"cmdoption-pycopm-m","--execution_mode"],[36,4,1,"cmdoption-pycopm-f","--flow_command"],[35,4,1,"cmdoption-pycopm-d","--grid_transformation"],[34,4,1,"cmdoption-pycopm-l","--include_prefix"],[36,4,1,"cmdoption-pycopm-i","--input_deck_path"],[31,4,1,"cmdoption-pycopm-j","--jump_thresholds"],[34,4,1,"cmdoption-pycopm-w","--output_deck_name"],[36,4,1,"cmdoption-pycopm-o","--output_directory"],[31,4,1,"cmdoption-pycopm-p","--pore_volume_correction"],[33,4,1,"cmdoption-pycopm-g","--refinement"],[34,4,1,"cmdoption-pycopm-ijk","--requested_ijk"],[34,4,1,"cmdoption-pycopm-precision","--significant_digits"],[31,4,1,"cmdoption-pycopm-t","--transmissibility_coarsening_method"],[35,4,1,"cmdoption-pycopm-v","--vicinity_specification"],[34,4,1,"cmdoption-pycopm-explicit","--write_explicit_solution"],[33,4,1,"cmdoption-pycopm-x","--x_coarsening"],[33,4,1,"cmdoption-pycopm-rx","--x_refinement"],[33,4,1,"cmdoption-pycopm-y","--y_coarsening"],[33,4,1,"cmdoption-pycopm-ry","--y_refinement"],[33,4,1,"cmdoption-pycopm-z","--z_coarsening"],[33,4,1,"cmdoption-pycopm-rz","--z_refinement"],[31,4,1,"cmdoption-pycopm-a","-a"],[33,4,1,"cmdoption-pycopm-c","-c"],[35,4,1,"cmdoption-pycopm-d","-d"],[35,4,1,"cmdoption-pycopm-dual","-dual"],[34,4,1,"cmdoption-pycopm-e","-e"],[34,4,1,"cmdoption-pycopm-explicit","-explicit"],[36,4,1,"cmdoption-pycopm-f","-f"],[33,4,1,"cmdoption-pycopm-g","-g"],[36,4,1,"cmdoption-pycopm-i","-i"],[34,4,1,"cmdoption-pycopm-ijk","-ijk"],[31,4,1,"cmdoption-pycopm-j","-j"],[34,4,1,"cmdoption-pycopm-l","-l"],[36,4,1,"cmdoption-pycopm-m","-m"],[31,4,1,"cmdoption-pycopm-n","-n"],[36,4,1,"cmdoption-pycopm-o","-o"],[31,4,1,"cmdoption-pycopm-p","-p"],[34,4,1,"cmdoption-pycopm-precision","-precision"],[31,4,1,"cmdoption-pycopm-q","-q"],[34,4,1,"cmdoption-pycopm-r","-r"],[33,4,1,"cmdoption-pycopm-rx","-rx"],[33,4,1,"cmdoption-pycopm-ry","-ry"],[33,4,1,"cmdoption-pycopm-rz","-rz"],[31,4,1,"cmdoption-pycopm-s","-s"],[31,4,1,"cmdoption-pycopm-t","-t"],[35,4,1,"cmdoption-pycopm-v","-v"],[34,4,1,"cmdoption-pycopm-w","-w"],[33,4,1,"cmdoption-pycopm-x","-x"],[33,4,1,"cmdoption-pycopm-y","-y"],[33,4,1,"cmdoption-pycopm-z","-z"]],"pycopm.core":[[4,0,0,"-","pycopm"]],"pycopm.core.pycopm":[[4,1,1,"","_check_cmdargs"],[4,1,1,"","_check_flow"],[4,1,1,"","_parse_arguments"],[4,1,1,"","main"]],"pycopm.utils":[[6,0,0,"-","coarsening"],[7,0,0,"-","files_writer"],[8,0,0,"-","generate_decks"],[9,0,0,"-","input_values"],[10,0,0,"-","parser_deck"],[11,0,0,"-","refinement"],[12,0,0,"-","runs_executer"],[13,0,0,"-","terminal"],[14,0,0,"-","transformation"],[15,0,0,"-","vicinity"]],"pycopm.utils.coarsening":[[6,2,1,"","CoarseningMaps"],[6,1,1,"","_collect_removed_zcorn_indices"],[6,1,1,"","_compact_permeability_properties"],[6,1,1,"","_find_active_neighbors"],[6,1,1,"","_find_include_statement"],[6,1,1,"","_global_index_to_ijk"],[6,1,1,"","_group_maximum_zero_based"],[6,1,1,"","_group_minimum_zero_based"],[6,1,1,"","_group_sum_zero_based"],[6,1,1,"","_grouped_count"],[6,1,1,"","_grouped_first"],[6,1,1,"","_grouped_last"],[6,1,1,"","_grouped_max"],[6,1,1,"","_grouped_mean"],[6,1,1,"","_grouped_min"],[6,1,1,"","_grouped_mode"],[6,1,1,"","_grouped_sum"],[6,1,1,"","_interleave_dual_property"],[6,1,1,"","_read_satnum"],[6,1,1,"","build_dual_porosity_grid"],[6,1,1,"","coarsen_and_write_properties"],[6,1,1,"","coarsen_corner_point_grid"],[6,1,1,"","coarsen_properties"],[6,1,1,"","create_coarsening_map"],[6,1,1,"","create_coarsening_maps"],[6,1,1,"","map_nnc_transmissibilities"],[6,1,1,"","redistribute_removed_pore_volume"]],"pycopm.utils.coarsening.CoarseningMaps":[[6,3,1,"","cell_groups"],[6,3,1,"","coarse_tranx"],[6,3,1,"","coarse_trany"],[6,3,1,"","coarsened_axes"],[6,3,1,"","dual_defaults"],[6,3,1,"","dual_tranx"],[6,3,1,"","dual_trany"],[6,3,1,"","matrix_mask"],[6,3,1,"","nnc_text"],[6,3,1,"","reference_to_coarse"],[6,3,1,"","vertical_transfer_enabled"],[6,3,1,"","x"],[6,3,1,"","y"],[6,3,1,"","z"]],"pycopm.utils.files_writer":[[7,1,1,"","_identify_removed_pillars"],[7,1,1,"","_render_template"],[7,1,1,"","_write_ert_files"],[7,1,1,"","_write_grid_files"],[7,1,1,"","_write_let_tables"],[7,1,1,"","format_opm_compact_values"],[7,1,1,"","round_like_e"],[7,1,1,"","write_coarsened_model_files"],[7,1,1,"","write_compact_property_file"],[7,1,1,"","write_dual_properties"],[7,1,1,"","write_grid"],[7,1,1,"","write_include"],[7,1,1,"","write_porv"],[7,1,1,"","write_property"],[7,1,1,"","write_property_inc"],[7,1,1,"","write_reference_to_coarse_map"]],"pycopm.utils.generate_decks":[[8,1,1,"","_correct_fluid_in_place"],[8,1,1,"","_create_index_mappings"],[8,1,1,"","_initialize_deck_data"],[8,1,1,"","create_deck"]],"pycopm.utils.input_values":[[9,1,1,"","_add_validation_error"],[9,1,1,"","_is_finite_number"],[9,1,1,"","_is_integer"],[9,1,1,"","_validate_coarsening"],[9,1,1,"","_validate_integer"],[9,1,1,"","_validate_let_parameters"],[9,1,1,"","_validate_number_array"],[9,1,1,"","_validate_rock_properties"],[9,1,1,"","_validate_string"],[9,1,1,"","_validate_toml"],[9,1,1,"","_warn"],[9,1,1,"","create_deck_config"],[9,1,1,"","load_toml_config"],[9,1,1,"","parse_axis_modifications"]],"pycopm.utils.parser_deck":[[10,2,1,"","_ParserState"],[10,1,1,"","__handle_schedule_keyword"],[10,1,1,"","_append_explicit_solution_includes"],[10,1,1,"","_collect_segmented_well_names"],[10,1,1,"","_collect_vicinity_well_names"],[10,1,1,"","_handle_aquancon"],[10,1,1,"","_handle_aqucon"],[10,1,1,"","_handle_aqunum"],[10,1,1,"","_handle_bccon"],[10,1,1,"","_handle_bwpr"],[10,1,1,"","_handle_compsegs"],[10,1,1,"","_handle_dimens"],[10,1,1,"","_handle_editnnc"],[10,1,1,"","_handle_equil"],[10,1,1,"","_handle_fault"],[10,1,1,"","_handle_grid_props"],[10,1,1,"","_handle_mapaxes"],[10,1,1,"","_handle_multflt"],[10,1,1,"","_handle_multiply"],[10,1,1,"","_handle_multregt"],[10,1,1,"","_handle_oper"],[10,1,1,"","_handle_pinch"],[10,1,1,"","_handle_props"],[10,1,1,"","_handle_regions"],[10,1,1,"","_handle_segmented_wells"],[10,1,1,"","_handle_source"],[10,1,1,"","_handle_welldims"],[10,1,1,"","_handle_wells"],[10,1,1,"","_handle_welsegs"],[10,1,1,"","_include_contains_endbox"],[10,1,1,"","_mark_multiplier_keyword"],[10,1,1,"","_scan_deck_file"],[10,1,1,"","find_multiplier_keywords"],[10,1,1,"","process_deck"]],"pycopm.utils.parser_deck._ParserState":[[10,3,1,"","aquancon"],[10,3,1,"","aqucon"],[10,3,1,"","aqunum"],[10,3,1,"","bccon"],[10,3,1,"","bwpr"],[10,3,1,"","compdat"],[10,3,1,"","completion_wells"],[10,3,1,"","complump"],[10,3,1,"","compsegs"],[10,3,1,"","compsegs_wells"],[10,3,1,"","dimens"],[10,3,1,"","editnnc"],[10,3,1,"","equil"],[10,3,1,"","faults"],[10,3,1,"","grid"],[10,3,1,"","has_edit"],[10,3,1,"","mapaxes"],[10,3,1,"","multflt"],[10,3,1,"","multiply"],[10,3,1,"","multregt"],[10,3,1,"","operation"],[10,3,1,"","pinch"],[10,3,1,"","previous_completion"],[10,3,1,"","process_edit"],[10,3,1,"","props"],[10,3,1,"","regions"],[10,3,1,"","retained_wells"],[10,3,1,"","schedule_keyword"],[10,3,1,"","segmented_wells"],[10,3,1,"","separator"],[10,3,1,"","skip_block"],[10,3,1,"","source"],[10,3,1,"","welldims"],[10,3,1,"","welsegs"],[10,3,1,"","welspecs"]],"pycopm.utils.refinement":[[11,2,1,"","RefinementMaps"],[11,1,1,"","create_coord_axis_map"],[11,1,1,"","create_refinement_maps"],[11,1,1,"","create_zcorn_axis_map"],[11,1,1,"","refine_grid"],[11,1,1,"","refine_properties"],[11,1,1,"","refine_zcorn_surface"]],"pycopm.utils.refinement.RefinementMaps":[[11,3,1,"","refined_cell_counts"],[11,3,1,"","x"],[11,3,1,"","y"],[11,3,1,"","z"]],"pycopm.utils.runs_executer":[[12,1,1,"","generate_postprocessing_plots"],[12,1,1,"","run_simulations"]],"pycopm.utils.terminal":[[13,1,1,"","_colorize"],[13,1,1,"","_supports_color"],[13,1,1,"","cli_correct_value"],[13,1,1,"","cli_error_value"],[13,1,1,"","cli_info_value"],[13,1,1,"","cli_warning_value"],[13,1,1,"","pycopm_error"],[13,1,1,"","pycopm_info"],[13,1,1,"","pycopm_name"],[13,1,1,"","pycopm_success"],[13,1,1,"","pycopm_tip"],[13,1,1,"","pycopm_warning"]],"pycopm.utils.transformation":[[14,1,1,"","transform_grid"],[14,1,1,"","transform_properties"]],"pycopm.utils.vicinity":[[15,2,1,"","VicinityMaps"],[15,2,1,"","_BoundaryMapping"],[15,1,1,"","_add_or_collect_porv"],[15,1,1,"","_apply_layer_pore_volume_correction"],[15,1,1,"","_corner_pore_volumes"],[15,1,1,"","_distribute_vertical_pore_volume"],[15,1,1,"","_find_nearest_active_corner_cell"],[15,1,1,"","_get_well_completions_for_vicinity"],[15,1,1,"","_map_east_boundary"],[15,1,1,"","_map_north_boundary"],[15,1,1,"","_map_south_boundary"],[15,1,1,"","_map_west_boundary"],[15,1,1,"","_original_index"],[15,1,1,"","_submodel_index"],[15,1,1,"","apply_boundary_pore_volume_correction"],[15,1,1,"","create_vicinity_maps"],[15,1,1,"","extract_vicinity_grid"],[15,1,1,"","map_vicinity_properties"]],"pycopm.utils.vicinity.VicinityMaps":[[15,3,1,"","active_counts"],[15,3,1,"","cell_mask"],[15,3,1,"","layer_external_porv"],[15,3,1,"","layer_max_i"],[15,3,1,"","layer_max_j"],[15,3,1,"","layer_min_i"],[15,3,1,"","layer_min_j"],[15,3,1,"","layer_selected_porv"],[15,3,1,"","max_i"],[15,3,1,"","max_j"],[15,3,1,"","max_k"],[15,3,1,"","min_i"],[15,3,1,"","min_j"],[15,3,1,"","min_k"],[15,3,1,"","selector"],[15,3,1,"","shape"],[15,3,1,"","source_indices"],[15,3,1,"","well_cells"],[15,3,1,"","well_indices"]],"pycopm.utils.vicinity._BoundaryMapping":[[15,3,1,"","active_count"],[15,3,1,"","offsets"],[15,3,1,"","pore_volume"]]},"objnames":{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","attribute","Python attribute"],"4":["std","cmdoption","program option"]},"objtypes":{"0":"py:module","1":"py:function","2":"py:class","3":"py:attribute","4":"std:cmdoption"},"terms":{"0f":[22,26],"0pressur":23,"1e":[23,45],"1f":22,"2d":[40,42],"2e":22,"2f":[22,26],"2xyz":22,"3h":42,"3xz":22,"A":[11,16,17,38,40],"After":22,"All":[16,22],"An":[19,30,32,38],"As":[33,37,40,45],"At":10,"Below":38,"By":[17,24,25,26,27],"Each":9,"For":[17,19,20,21,22,23,29,30,31,37,40,45],"From":[21,26,40,45],"Here":[17,22,24,39],"I":17,"If":[4,9,17,20,22,29,30,42,45],"In":[19,22,29,31,40,42,43],"It":[21,22,30],"On":16,"Other":4,"The":[0,4,6,8,9,10,13,15,16,17,19,20,21,22,23,29,30,32,33,35,36,37,40,41,42,43,44,45],"Then":[17,20,22,29,37,40,45],"There":20,"These":[15,24,40],"This":[4,17,18,21,23,24,30,40,42,43,45],"To":[17,20,22,27,29,40,45],"We":[19,22,26,40],"While":[20,21,29],"You":[20,29],"_":40,"__handle_schedule_keyword":[2,5,10],"_add_or_collect_porv":[2,5,15],"_add_validation_error":[2,5,9],"_append_explicit_solution_includ":[2,5,10],"_apply_layer_pore_volume_correct":[2,5,15],"_boundarymap":[2,5,15],"_build":19,"_check_cmdarg":[2,3,4],"_check_flow":[2,3,4],"_collect_removed_zcorn_indic":[2,5,6],"_collect_segmented_well_nam":[2,5,10],"_collect_vicinity_well_nam":[2,5,10],"_color":[2,5,13],"_compact_permeability_properti":[2,5,6],"_corner_pore_volum":[2,5,15],"_correct_fluid_in_plac":[2,5,8],"_create_index_map":[2,5,8],"_distribute_vertical_pore_volum":[2,5,15],"_find_active_neighbor":[2,5,6],"_find_include_stat":[2,5,6],"_find_nearest_active_corner_cel":[2,5,15],"_get_well_completions_for_vicin":[2,5,15],"_global_index_to_ijk":[2,5,6],"_group_maximum_zero_bas":[2,5,6],"_group_minimum_zero_bas":[2,5,6],"_group_sum_zero_bas":[2,5,6],"_grouped_count":[2,5,6],"_grouped_first":[2,5,6],"_grouped_last":[2,5,6],"_grouped_max":[2,5,6],"_grouped_mean":[2,5,6],"_grouped_min":[2,5,6],"_grouped_mod":[2,5,6],"_grouped_sum":[2,5,6],"_handle_aquancon":[2,5,10],"_handle_aqucon":[2,5,10],"_handle_aqunum":[2,5,10],"_handle_bccon":[2,5,10],"_handle_bwpr":[2,5,10],"_handle_compseg":[2,5,10],"_handle_dimen":[2,5,10],"_handle_editnnc":[2,5,10],"_handle_equil":[2,5,10],"_handle_fault":[2,5,10],"_handle_grid_prop":[2,5,10],"_handle_mapax":[2,5,10],"_handle_multflt":[2,5,10],"_handle_multipli":[2,5,10],"_handle_multregt":[2,5,10],"_handle_op":[2,5,10],"_handle_pinch":[2,5,10],"_handle_prop":[2,5,10],"_handle_region":[2,5,10],"_handle_segmented_wel":[2,5,10],"_handle_sourc":[2,5,10],"_handle_wel":[2,5,10],"_handle_welldim":[2,5,10],"_handle_welseg":[2,5,10],"_identify_removed_pillar":[2,5,7],"_include_contains_endbox":[2,5,10],"_initialize_deck_data":[2,5,8],"_interleave_dual_properti":[2,5,6],"_io":13,"_is_finite_numb":[2,5,9],"_is_integ":[2,5,9],"_map_east_boundari":[2,5,15],"_map_north_boundari":[2,5,15],"_map_south_boundari":[2,5,15],"_map_west_boundari":[2,5,15],"_mark_multiplier_keyword":[2,5,10],"_original_index":[2,5,15],"_parse_argu":[2,3,4],"_parserst":[2,5,10],"_pycopm":34,"_read_satnum":[2,5,6],"_render_templ":[2,5,7],"_scan_deck_fil":[2,5,10],"_submodel_index":[2,5,15],"_supports_color":[2,5,13],"_validate_coarsen":[2,5,9],"_validate_integ":[2,5,9],"_validate_let_paramet":[2,5,9],"_validate_number_array":[2,5,9],"_validate_rock_properti":[2,5,9],"_validate_str":[2,5,9],"_validate_toml":[2,5,9],"_warn":[2,5,9],"_write_ert_fil":[2,5,7],"_write_grid_fil":[2,5,7],"_write_let_t":[2,5,7],"abl":20,"abov":[15,22,40,45],"abstract":20,"accept":[9,13,19,28,30],"accord":42,"account":17,"accumul":6,"accuraci":40,"achiev":[17,22,24,27,41,45],"across":[15,22,40],"action":[19,29],"activ":[4,6,10,15,16,17,20,28,29,31,39,40,42,44,45],"active_cell_method":4,"active_count":[5,15],"actnum":[6,31],"actual":22,"ad":38,"adapt":[28,30,37],"add":[4,6,7,9,17,19,20,22,28,29,30,31,34,35,36,39,41,42,45],"addiiton":43,"addit":[4,11,17,20,22,40,41,42,43],"address":9,"adjust":[4,8,31],"adopt":40,"advanc":28,"affin":43,"aggreg":[4,6,9,16,28,30,32,45],"al":17,"align":[28,43],"allow":[17,22,23,40,42],"allow_inlin":7,"along":[4,11,15,31,40],"also":[6,9,19,20,21,22,29,30,34,40,45],"altern":[13,19,33],"alway":13,"among":[6,11,42],"anaconda":29,"analys":19,"analysi":[19,30],"angl":[4,14],"ani":[7,9,17,19,32,41],"anim":20,"anoth":30,"ansi":13,"answer":19,"api":30,"appear":[16,37],"append":[10,13],"appli":[13,14,16,18,22,30,35,40,43,45],"applic":[20,21,28,30,37,40,43],"apply_boundary_pore_volume_correct":[2,5,15],"apporach":22,"approach":[17,19,22,37,40],"appropri":[4,40],"aproach":22,"apt":29,"aquancon":[5,10],"aqucon":[5,10],"aquif":10,"aqunum":[5,10],"area":[40,42],"argpars":9,"argument":[4,6,8,9,11,17],"argv":4,"arithmet":[31,40],"armon":[31,40],"around":[27,31,35,42,45],"array":[6,7,9,15,17,33,40],"arxiv":30,"asp":23,"assign":[6,15,35,40],"associ":14,"assum":22,"assur":40,"auto":19,"avail":[4,8,21,28,29,30],"averag":[31,40],"avoid":[22,31],"awar":20,"axe":[4,6,11],"axi":[4,6,7,8,9,10,11,15,35],"axis_valu":9,"azurewebsit":21,"back":17,"bar":23,"base":[0,4,6,9,10,11,12,15,20,30,32,36,38,39,40],"basetemp":19,"bash":20,"bc0407_hist01122006":25,"bccon":[5,10],"becaus":[9,14],"becom":6,"befor":[7,9,12,13,17,19,29,32,34],"benchmark":38,"better":[21,23,40],"betweens":43,"bin":[17,19,29,36],"binari":[19,20,36],"black":[19,23,45],"block":10,"bool":[6,7,9,10,13],"boolean":[9,10,15],"bottom":[17,21,22,25,27,45],"bound":[8,9,15],"boundari":[6,9,15,17,28,31,39,41,42,45],"box":[4,15,16,28,30,35,42],"branch":29,"break":40,"brew":20,"broader":30,"brows":30,"build":[8,17,19,36],"build_dual_porosity_grid":[2,5,6],"build_opm_mpi":29,"built":[21,29,36],"bundl":[28,30,44],"bwpr":[5,10],"c":[4,16,19,22,26,27,28,30,32,40,45],"c1":22,"c2":22,"ca":27,"calcit":38,"calcul":15,"call":[0,17,37],"can":[4,6,8,15,17,19,20,21,22,27,29,30,31,33,35,37,40,41,42,43,45],"capillari":40,"care":40,"cartesian":[6,40],"case":[0,4,6,8,22,40],"cbf":[22,26],"cbl":23,"cbn":22,"cbp":[22,23,26],"cd":[21,26,29,45],"cell":[4,6,7,8,10,11,15,17,22,23,26,27,28,31,33,34,35,39,40,41,42,44,45],"cell_group":[5,6],"cell_mask":[5,15],"center":[22,30,45],"centr":[4,15,20,28,30],"certain":25,"cet_rainbow_bgyrm_35_85_c69":[22,26],"cfd":38,"cfg":[6,7,12],"cfg_file":9,"chang":[6,17,19],"charact":4,"character":43,"check":[4,9,13,19,29,30,38],"checker":19,"checkout":29,"child":16,"choos":28,"chosen":15,"ci_pycopm_maco":19,"ci_pycopm_ubuntu":[19,29],"cijk":9,"cite":30,"class":[6,10,11,15,30],"cleanup_file_suffix":17,"cli":[13,28],"cli_correct_valu":[2,5,13],"cli_error_valu":[2,5,13],"cli_info_valu":[2,5,13],"cli_warning_valu":[2,5,13],"clone":[29,45],"closest":[22,31],"cluster":[6,39,40],"cluster_id":6,"cluster_maximum":6,"cluster_minimum":6,"cmake":29,"cmdarg":[4,6,8,9,11],"co2":[30,38],"coa_map":6,"coa_z":6,"coars":[6,7,8,10,17,22,26,31,33,40],"coarse_trani":[5,6],"coarse_tranx":[5,6],"coarsen":[0,2,4,5,7,8,9,16,17,18,20,21,22,24,25,26,27,28,30,31,32,37,39,44,45],"coarsen_and_write_properti":[2,5,6],"coarsen_corner_point_grid":[2,5,6],"coarsen_properti":[2,5,6],"coarsened_ax":[5,6],"coarseningmap":[2,5,6,8],"coarser":[17,22,26,31,33,37,40],"coarsern":22,"code":[2,13,19,29,30],"coeffici":9,"collabor":19,"collect":[8,9,15],"color":13,"column":[6,15],"column_offset":15,"com":[20,28,29,38],"combin":[4,22,32],"comma":[9,33],"command":[4,6,8,9,11,12,13,17,18,28,29,30,45],"comment":22,"commit":19,"common":[29,40],"communic":40,"compact":7,"compar":[20,21,22,23],"comparison":[30,40,43],"compat":[4,16,28,30,45],"compdat":[4,5,10,34],"complementari":30,"complet":[6,9,10,15,18,20,28,30,34],"completion_removal_level":4,"completion_wel":[5,10],"complex":40,"complump":[5,10],"compseg":[5,10],"compsegs_wel":[5,10],"comput":[17,34,40],"con":6,"concaten":6,"condit":[41,42],"confact":34,"confer":17,"config":29,"configur":[4,6,7,8,9,10,11,12,14,15,16,20,28,29,30,36,37],"configviadeck":[6,7,8,9,10,11,14,15],"configviatoml":[6,7,9,12],"conform":30,"connect":[4,6,22,28,31,34,35,40],"conquer":22,"consecut":7,"conserv":[16,20,22,39,45],"consid":[17,35],"consist":13,"constant":7,"constraint":9,"construct":9,"consult":30,"contain":[0,6,7,9,10,11,12,13,14,15,19,21],"content":[1,6,10,16,19,22],"continu":[4,6,28,31,45],"continuous_aggregation_method":4,"continuum":6,"contribut":[29,30],"control":[6,7,28,31,34,45],"convert":7,"coord":[6,7,11],"coord_indic":7,"coordin":[4,8,14,15,30,35],"copi":[6,11,12,19,22,29,37],"core":[0,1,2],"corner":[6,7,11,14,15,31,40,42],"corner_i":15,"corner_j":15,"corner_porv":15,"correct":[4,8,13,15,17,28,31,37,42],"correct_fluid_in_plac":4,"correl":40,"correspond":[10,15,17,24,31,34,40,41,42],"count":11,"cov":19,"cover":[4,44],"cporv":17,"cr":[6,7],"creat":[6,8,9,11,15,19,20,25,28,29,30,37,40,43],"create_coarsening_map":[2,5,6],"create_coord_axis_map":[2,5,11],"create_deck":[2,5,8],"create_deck_config":[2,5,9],"create_refinement_map":[2,5,11],"create_vicinity_map":[2,5,15],"create_zcorn_axis_map":[2,5,11],"creation":[30,31],"criterion":4,"criterium":35,"cross":16,"csp":38,"cssr":[20,28,29,30,38],"current":[9,17,18,20,22,28,29,30,37],"current_directori":29,"d":[4,16,23,24,30,32,43,45],"daavid00":38,"data":[4,6,7,8,10,11,15,16,17,19,21,22,23,24,25,26,27,28,30,34,36,39,41,43,44,45],"dataset":26,"date":17,"day":22,"dck":[6,7,8,10,11,14,15],"dcmake_build_typ":29,"deck":[0,4,6,7,8,9,10,11,14,15,16,17,18,19,22,24,28,29,30,31,32,36,41,42,44],"deck_dri":[4,36],"deck_encod":4,"deck_lin":10,"dedic":29,"default":[6,17,22,28,30,31,32,33,34,35,36,37,40,45],"default_valu":6,"defin":[6,17,18,22,33,37,40,41,42],"definit":[9,17,22,29,40],"degre":[4,14,35,45],"delet":17,"demonstr":20,"denot":15,"depend":[4,8,15,19,28,29,30,40],"deprec":13,"depth":[4,6,43],"describ":[19,21,24,25,37,45],"descript":[17,28,30,33],"destin":[4,7],"destination_surfac":11,"detail":[4,33,44,45],"determin":[8,13],"dev":[19,29],"develop":[16,18,20,28,29,30,37,40,44],"diamond":[4,15,28,30,35,42],"diamondxi":[4,15,27,28,35,42],"dict":[4,6,9],"differ":[0,17,20,22,28,35,40,42,43],"differenti":[22,23,35],"digit":[4,7,34],"dimen":[5,10],"dimens":[6,7,8,10,11,14,15,28],"dimension":7,"dir":[17,33],"direct":[4,6,10,16,17,20,22,26,28,30,31,32,33,35,37,40,41,42],"directori":[4,9,13,19],"disabl":13,"discontinu":31,"discret":[4,6,28,31,40],"discrete_aggregation_method":4,"discuss":17,"disperc":31,"display":[9,13,28],"dist":17,"distanc":[6,15],"distpara":17,"distribut":[9,15,17,31,42],"divers":43,"divid":[6,11,22],"dmar":17,"doc":19,"docs_configs_view":17,"docs_theory_coarsen":40,"docs_theory_refin":41,"docs_theory_submodel":42,"docs_theory_transform":43,"docs_via_config_drogon":21,"docs_via_deck_drogon":22,"docs_via_deck_dual_coarsen":23,"docs_via_deck_hello_world":45,"docs_via_deck_norn":25,"docs_via_deck_smeaheia":26,"docs_via_deck_spe10":27,"document":[17,19,20,29],"doi":30,"done":29,"download":[22,25,26,27,29],"downstream":9,"dpi":22,"drawback":40,"dri":[4,8,28,36,45],"drogon":[0,4,6,17,18,20,21,24,28,29,30,31,36,37],"drogon_2times_coars":22,"drogon_coars":21,"drogon_hist":[22,24],"drogon_hist_prep_pycopm_dryrun":22,"drogon_hist_pycopm":22,"drogon_hist_pycopm_pycopm":[22,24],"drogon_pycopm_comparison":22,"dual":[4,6,7,16,20,28,30,32],"dual_default":[5,6],"dual_porosity_criterion":4,"dual_trani":[5,6],"dual_tranx":[5,6],"dual_valu":6,"due":[40,45],"dure":[4,6,9,15,35,40],"duse_mpi":29,"dwith_ndebug":29,"dynam":38,"e":[4,17,19,21,22,24,29,31,33,35,36,37,40,41,42,43,45],"earli":37,"eas":43,"east":15,"edg":29,"editnnc":[5,10],"effect":40,"eg":17,"egrid":[8,9,19],"either":[29,32,33],"elaps":12,"elapsed_second":12,"els":29,"em":38,"email":19,"emltg":17,"emlto":17,"emphas":40,"empti":[6,9,15,22,34],"enabl":[6,13,20,31,45],"encod":[4,10,13],"end":[22,23,25,32,40],"endbox":10,"endnum":31,"ensembl":[17,21,22],"ensemble_experi":17,"ensemble_s":17,"ensemble_smooth":17,"enterpris":29,"entri":[4,9,17,34,41],"environ":[19,28,29],"eo":17,"eog":17,"eqlnum":31,"equal":[7,11,22,35,40,42],"equat":45,"equil":[4,5,10,17,45],"error":[9,13,17,21,22,25,37],"ert":[0,4,7,12,17,20,21,28,29,30,37],"ert_argu":17,"es_mda":17,"estim":9,"et":17,"etc":34,"everi":[6,7,9,16],"ew":17,"exact":[28,29,30],"exampl":[6,17,19,21,23,25,28,30,33,37,40,44,45],"excel":40,"except":17,"exclud":15,"exclus":32,"exectut":36,"execut":[0,4,12,17,19,20,21,22,27,29,36,37,40,42,45],"execution_mod":[4,17],"exist":[20,28,30],"expans":30,"expect":7,"expected_length":9,"explan":17,"explicit":[4,10,16,44,45],"explor":30,"export":[19,29],"exprecc":[30,38],"extend":[6,7,19],"extern":4,"extract":[8,10,15,20,28,30,32,35,39,42,43,44,45],"extract_vicinity_grid":[2,5,15],"f":[4,16,18,19,45],"face":[31,40],"factor":[4,32],"factori":[6,10,15],"fals":[7,10],"familiar":[17,19],"faster":42,"fatal":13,"fault":[5,10,22,28,31,40,41,42],"favourit":37,"featur":[25,30,40],"fewer":42,"fgip":31,"fi":29,"field":[10,22,40,43],"figur":[17,20,21,22,23,24,25,26,27,37,40,41,42,43,45],"file":[0,4,6,7,8,9,10,11,13,14,15,16,17,18,19,20,21,22,25,28,29,30,34,36,37,42,45],"file_path":10,"file_suffix":7,"filenam":[4,13],"files_writ":[2,5],"filter":10,"final":[0,7,17,45],"find":[6,7,10,37],"find_multiplier_keyword":[2,5,10],"fine":[6,17],"finit":9,"finner":41,"fipnum":[31,35,40,42],"first":[6,8,17,22,28,32,33,45],"five":[24,26],"fix":19,"flag":[9,10,16,17,19,20,22,23,27,30,33,45],"flatten":[6,7,11,15],"flexibl":40,"float":[4,6,9,12,15,34],"flow":[0,4,7,8,12,16,17,18,19,20,21,22,23,24,28,30,34,36,37,38,42,45],"flow_cmdarg":4,"flow_command":[4,17],"flow_maco":19,"flow_toml":4,"fluid":[8,20,28,31,40],"fluidflow":38,"fluxnum":31,"fn":[22,26,45],"focus":[20,28,30,37,42],"foip":[22,31],"folder":[0,19,21,26,29,30,45],"follow":[4,14,17,20,21,22,23,25,28,29,30,32,37,40,45],"fopr":22,"fork":[19,30],"format":[7,13,17,19,22,28,32,40,43],"format_opm_compact_valu":[2,5,7],"former":[20,40],"found":10,"four":[15,20,26,32,42,44],"frac":40,"fraction":11,"fractur":[4,6,7],"framework":[0,22,30,38],"freeglut3":29,"frequent":40,"friend":[22,26,30],"front":42,"fs":[22,23,26],"full":[17,37,45],"function":[0,6,7,8,12,15,17,20,22,30,40],"fund":30,"fz":22,"g":[4,17,19,22,24,29,31,32,34,35,36,37,40,41,42,43,45],"gas":[4,8,22,25,31],"ge":[23,45],"generat":[0,4,6,7,8,9,10,11,12,13,14,15,17,19,20,21,22,24,25,26,27,28,30,33,34,36,37,38,40,41,42,43,45],"generate_deck":[2,5],"generate_postprocessing_plot":[2,5,12],"generated_fil":[6,11,14,15],"generic":[16,18,20,28,29,30,44],"geolog":[17,22,24,25,28,30,37,39,40,43],"geometr":[15,30,40],"geometri":[11,14,15,20,28,40],"geophys":40,"get":[19,28,29],"gi":22,"gif":[22,38],"git":[20,28,29],"github":[17,19,20,28,29,30,38],"give":[22,37,40],"given":[17,22,33,34,35,39,40,42,45],"gl":22,"global":[6,7,15,28,30,41],"global_index":6,"go":22,"goal":30,"good":22,"graphic":[13,20],"grdecl":7,"greater":6,"grid":[0,4,5,6,7,8,9,10,11,14,15,16,20,22,23,28,29,30,34,35,37,39,43,45],"grid_transform":[4,14],"group":[4,6,32,43],"group_cod":6,"gui":17,"guid":[28,30],"guidanc":17,"h":17,"hand":16,"handi":43,"handl":[10,22,28,31,40,44],"handler":10,"harmon":[31,40],"has_edit":[5,10],"has_main_multflt":10,"hat":29,"hdf5":29,"head":[10,16],"header":7,"hello":[19,20,28,29,30,44],"hello_world":[16,19,28,30,44,45],"hello_world_1_left":45,"hello_world_1_right":45,"hello_world_pycopm":[19,45],"hello_world_pycopm_pycopm":45,"help":[19,28,29],"helper":9,"hexahedron":40,"higher":22,"highest":6,"highlight":45,"histori":[9,17,18,21,22,28,30,37,38,40],"history_matching_end_d":17,"hm":[17,21],"home":36,"homebrew":29,"honor":[17,40],"horda":30,"horizont":[6,11,40,41],"howev":20,"html":19,"https":[20,21,28,29,30,38],"human":[9,13],"hv":10,"identifi":[4,6,7,10,15,22],"ignor":[6,19],"ijk":[4,6],"illustr":20,"ilustr":24,"imag":38,"imbnum":31,"impact":43,"implement":[4,22,32,40],"import":[19,40],"inact":[15,22,26,31,40],"inc":[7,22,34],"includ":[4,6,7,8,10,11,14,15,17,22,25,28,29,30,34,37,45],"include_lin":6,"include_prefix":4,"inclus":[9,15],"incompat":[4,32],"increas":[23,40,45],"index":[6,8,10,15,17,28,34,39],"indic":[4,6,7,8,9,10,11,15,22,32,33,34,40,41,42,43],"induc":38,"industri":40,"inform":[13,17,43],"inherit":39,"init":[6,8,17,22],"initi":[4,9,10,17,21,22,28,30,31,34,40,44],"initialization_method":17,"inj":27,"injector":27,"inlin":7,"input":[4,8,9,10,11,16,17,18,20,21,22,28,30,31,32,34,37,40,41,42,45],"input_deck_path":4,"input_fil":[4,9],"input_valu":[2,5],"insert":6,"insid":[15,21,29,41,42,45],"insight":39,"inspect":[9,13,17],"instal":[19,20,30,36],"instead":[4,17,27,40,45],"instruct":29,"int":[6,7,9,10,11,12,15],"integ":[9,45],"integr":[17,18,28,29,30],"interest":[29,38,42],"interfac":28,"interim":29,"interleav":6,"intermedi":6,"internal":[9,34],"interpol":11,"interval":[4,6,11,17,28,30,35,42,45],"introduct":[28,33,37,40],"invalid":[4,9,13],"involv":[22,40],"iso":[4,34],"isol":45,"issu":[20,30],"item":34,"iter":[17,21,22],"iterative_ensemble_smooth":17,"j":[4,6,7,15,17,22,32,35,40,41,42],"j5":29,"job":[0,7,12],"jump":[4,6,22,28,31,32],"jump_threshold":4,"just":36,"k":[4,6,15,17,22,32,35,40,41,42],"keep":[17,22,25,33,45],"key":[4,9,40],"keyword":[7,8,10,22,30,45],"kh":34,"kilomet":43,"km":[22,26],"kwr":10,"l":[4,22,23,27,29],"l127":22,"l242":22,"label":22,"landa":30,"larg":[40,43],"larger":[28,45],"last":[6,8,17,33,34],"later":17,"latter":20,"launcher":4,"layer":[6,15,31,43],"layer_external_porv":[5,15],"layer_index":15,"layer_max_i":[5,15],"layer_max_j":[5,15],"layer_min_i":[5,15],"layer_min_j":[5,15],"layer_selected_porv":[5,15],"leakag":38,"learn":44,"least":[26,31,32],"left":[4,21,22,26,27,37,40,41,43,45],"legaci":[16,20,28,29,30],"legend":45,"length":9,"less":21,"let":[7,9,17],"let_paramet":[12,17],"level":[4,10,33],"lg":17,"lib":29,"lie":40,"like":[22,43],"line":[4,6,7,8,9,10,11,13,14,15,17,18,19,22,28,29,30,39,40,41,43,45],"linear":11,"link":[20,29],"lint":29,"linux":20,"list":[4,6,7,8,9,10,11,13,14,15],"ll":22,"llb":22,"lmltg":17,"lmlto":17,"lo":17,"load":[8,9],"load_toml_config":[2,5,9],"local":[9,19,41],"locat":[0,13,21,22,35,36,37,41,42,43,45],"log":17,"look":[19,28],"lose":44,"lost":17,"lot":[22,40],"lower":[9,22,41,42,43],"lw":17,"m":[4,16,19,22,24,25,26,27,28,29,30,45],"machin":[4,7,17,29,34],"maco":[19,20],"macport":29,"made":[12,26,43],"main":[0,2,3,4,10,17,19,20,21,22,25,30,45],"maintain":[19,28,30],"major":4,"make":[19,22,29,30,31,42,45],"mako":[0,7],"mamba":29,"manag":[9,29,43],"mantain":19,"map":[4,6,7,8,10,11,15,28,30,34,39,41],"map_nnc_transmiss":[2,5,6],"map_vicinity_properti":[2,5,15],"mapax":[5,10,22],"marb\u00e1n":30,"mark":6,"mask":[6,15],"master":29,"match":[4,8,9,17,18,21,22,28,30,37,38,40,42],"mathbb":40,"matrix":[4,6,7],"matrix_mask":[5,6],"max":[4,6,17,22,23,31,40,45],"max_i":[5,15],"max_j":[5,15],"max_k":[5,15],"max_parallel_r":17,"max_realization_runtime_second":17,"maximum":[6,9,15,17,22,45],"maxpermz":22,"may":7,"mean":[4,6,17,22,31,40],"measur":43,"member":[21,22],"memori":17,"mention":[40,45],"merg":[6,19],"messag":[4,9,13],"metadata":9,"meter":[35,42],"method":[4,9,15,30,32,40,45],"micp":38,"microbi":38,"microsystem":38,"middl":45,"might":[19,22,29,37,38,40],"million":27,"min":[4,6,16,17,25,26,31,40],"min_i":[5,15],"min_j":[5,15],"min_k":[5,15],"min_successful_r":17,"mini":31,"miniforg":29,"minimum":[6,9,15,17],"minj":31,"miscnum":31,"miss":19,"missmatch":43,"mkdir":29,"mode":[4,6,12,13,16,17,31,40,45],"model":[4,6,9,10,16,17,18,20,21,22,24,25,26,27,28,30,31,32,33,34,35,36,37,38,40,41,42,43,44,45],"model3":41,"model6":[16,23],"model_nam":17,"modif":[9,17],"modifi":[4,8,10,11,14,17,22,28,29,30,34,36,37],"modified_deck":[6,7,8,10,11,14,15],"modul":[1,30,38],"mpi":[21,29],"mpirun":17,"msg":13,"mult":[10,31],"multflt":[5,10],"multi":[10,22],"multipl":32,"multipli":[5,6,10,22,35,40],"multnum":31,"multregt":[5,10,22],"multx":[10,22],"multz":[10,22],"must":[9,17,22,32,40],"mutual":32,"mypi":19,"n":[4,6,7,11,19,32,45],"name":[4,6,7,9,11,13,14,15,17,22,28,35,36],"name_of_script":20,"namespac":[4,6,8,9,11],"nan":6,"natur":40,"ndarray":[6,7,9,10,11,15],"near":38,"nearest":[6,15],"need":[17,19,20,22,24,25,29,30,31,37,45],"negat":9,"neighbor":40,"neighbor_indic":6,"neighbour":[6,28,31,40,45],"neighbourhood":[4,15,28,30,35,42],"neither":4,"nest":10,"net":[4,6,20,21,23,28,35],"new":[18,19,20,21,22,28,30,31,40,41,45],"newton":17,"next":17,"nice":[19,22,29],"nnc":[6,40],"nnc_text":[5,6],"nncs":39,"non":[4,6,9,13,20,23,28,30,32,35,40],"none":[4,6,7,8,9,10,11,12,13,14,15],"noreturn":13,"normal":9,"norn":[0,4,6,17,18,20,21,28,29,30,31,37,42],"norne_atw2013":25,"north":15,"northeast":15,"northwest":15,"notat":[7,40],"note":[19,22,24],"noth":31,"now":[20,22],"np":17,"nrwo":10,"ntg":[17,22],"num_dig":7,"number":[4,6,7,9,11,12,15,17,22,30,34,35,40,41,42,45],"number_group":6,"number_t":[7,12],"number_valu":7,"numer":[4,9,34],"nx":6,"nxyz":6,"nz":6,"o":[4,16,18,19,21,26,28,30,37,45],"object":[6,9,10,11,13,15],"observ":[7,21],"observation_minimum_error":17,"observation_relative_error":17,"occur":9,"offset":[5,6,15],"often":40,"oil":[4,8,21,22,31],"ok":22,"old":22,"omega":40,"omit":4,"onc":43,"one":[4,6,7,9,11,15,17,21,22,24,26,29,30,31,32,33,37,40,45],"onli":[9,14,16,17,18,21,29,30,31,34,35,36,40,45],"onlin":29,"onto":[6,11,15],"open":[8,9,20,30,38,40],"openbla":29,"oper":[4,5,10,22,28,30,40,44],"opernum":[7,31],"opm":[0,4,7,8,10,12,16,17,18,19,20,21,22,24,28,30,34,36,37,38,42,45],"opt":29,"optim":[28,30,37],"option":[4,7,8,9,13,16,17,20,28,29,30,31,35,40,42,45],"optvic":15,"order":[6,7,9,10,11,15,17,22,25,31,40],"org":[29,30],"organiz":16,"origin":[0,4,6,7,8,10,11,14,15,17,26,27,33,44],"original_lay":15,"original_ni":11,"original_nx":11,"otherwis":[6,7,37],"outer":9,"outisd":42,"output":[6,7,8,11,12,13,14,15,16,17,19,20,22,28,30,36,45],"output_deck_nam":4,"output_dir":13,"output_directori":[4,9],"output_ni":11,"output_nx":11,"output_path":7,"output_porv":[6,8,15],"outsid":[15,31,42,45],"overlap":40,"p":[4,16,22,25,27,32,42,45],"packag":[0,1,19,20],"page":[16,17,19,21],"paper":30,"parallel":17,"paramet":[4,6,7,8,9,10,11,12,13,14,15,17,22,31,40],"paraview":[20,45],"pars":[4,8,9,10],"parse_axis_modif":[2,5,9],"parser":10,"parser_deck":[2,5],"part":[26,40],"particular":4,"pass":[7,9,19,45],"path":[4,7,9,10,17,19,20,21,22,28,36,37,45],"peqvr":34,"per":[6,8,11,15,22,31,32],"perform":[14,21,23,40],"perm":22,"perm_max":17,"perm_min":17,"permeabl":[9,17,22,40,41],"permi":[6,17,22,31],"permx":[6,17,22,31,34],"permz":[6,17,22,31],"pet":[28,30,37],"phi_":40,"physic":4,"pilar":[17,25],"pillar":[6,17,28,30,33,40,41],"pinch":[5,10],"pip":[19,20,28,29],"pkg":29,"pkg_config_path":29,"pkgconfig":29,"place":[4,8,11,12,16,20,22,28,31,40],"plane":[35,40,43],"platform":[28,29,30],"pleas":19,"plopm":[17,20,21,22,23,25,26,27,38,41,42,43,45],"plot":[12,37],"plus":[13,34],"pm":35,"png":26,"pngs":[20,38],"pofff":38,"point":[4,6,7,9,11,14,15,21,32,34,40,43,45],"polygon":[4,15,28,30,35,39,42,45],"popul":[6,8,9],"pore":[4,6,8,11,15,22,24,25,27,28,30,31,39,40,42,44,45],"pore_volum":[5,15],"pore_volume_correct":[4,15,17],"poro":[16,17,22,23,26,31,35],"poros":[6,7,17,26,27,40,41],"porv":[11,17,22,31,34,45],"posit":[4,9,11,32,34,35],"possibl":[19,42],"postprocess":[12,37],"practic":22,"prealloc":11,"preced":6,"precipit":38,"precis":[4,7,9,16,18,28],"predefin":18,"prefer":19,"prefix":[4,7],"prep":[4,36],"prep_deck":[4,36],"prepar":[20,28,30],"preprocess":[8,12],"preprocessing_path":7,"prerequisit":29,"present":9,"preserv":[4,7,10,11,17,20,28,30,40,43],"pressur":[23,40,43],"prevent":4,"previous":[19,22,25,29],"previous_complet":[5,10],"print":[13,21],"problem":30,"process":[4,30,32,36],"process_deck":[2,5,10],"process_edit":[5,10],"product":[22,43],"program":13,"progress":13,"project":[7,9,12,29,42,45],"prop":[5,10,22],"properti":[4,6,7,8,9,10,11,14,15,16,17,22,24,28,30,34,36,39,40,41,42],"property_nam":7,"property_valu":[6,7],"provid":[8,10,13,17,30,42],"proxi":[28,30],"proyect":45,"prs":19,"prt":17,"public":30,"publish":30,"pull":[19,30],"push":19,"pushd":19,"pv":[17,22,31,40],"pvmean":[4,22,25,27,31,40],"pvtnum":31,"pvweightedperm":22,"pwd":29,"py314":19,"pycopm":[0,16,17,19,20,21,22,23,24,25,26,27,29,30,32,36,37,39,40,42,43,45],"pycopm_":34,"pycopm_error":[2,5,13],"pycopm_info":[2,5,13],"pycopm_nam":[2,5,13],"pycopm_porv":34,"pycopm_success":[2,5,13],"pycopm_tip":[2,5,13],"pycopm_warn":[2,5,13],"pylint":19,"pymm":38,"pyopmnearwel":38,"pyopmspe11":38,"pytest":19,"python":[19,28,30,38],"python3":[19,29],"q":[4,22,25,32],"quantiti":40,"question":40,"quick":38,"quot":13,"r":[4,19,29],"rais":[4,9,13,19],"random":17,"random_se":17,"rang":[9,10,30,31,32,33,41],"rate":[22,43],"ratio":40,"raw":9,"rdl":[22,23,26],"read":[4,6,22,30,34,45],"readabl":[9,13],"readi":[28,30],"realiz":[12,17],"receiv":[7,9,15],"recent":29,"recommend":[20,21,22,29,45],"record":[6,10],"red":29,"redistribut":[6,22],"redistribute_removed_pore_volum":[2,5,6],"reduc":[4,6,22,45],"refer":[6,7,9,18,22,28,29,30,40,45],"reference_simul":0,"reference_to_coars":[5,6],"refin":[0,2,4,5,8,10,16,17,20,24,28,30,32,37,39,44,45],"refine_grid":[2,5,11],"refine_properti":[2,5,11],"refine_zcorn_surfac":[2,5,11],"refined_cell_count":[5,11],"refinement_valu":11,"refinementmap":[2,5,8,11],"reftocoa":7,"regard":[17,39,42],"region":[4,5,10,15,17,22,28,30,31,35,38,39,40,41,42,45],"regular":40,"reject":32,"relat":[7,11,22,28,30,31,40],"releas":29,"relev":22,"remain":[6,9,16,17,28],"remap":15,"remedi":38,"remov":[4,6,7,9,17,21,22,25,28,30,31,33,34,40,45],"removal_indic":6,"removal_mask":6,"renam":22,"render":[7,12],"rendit":13,"repeat":[7,32],"replac":[10,19,22,25],"repo":[19,29],"report":[9,13,30],"reposit":28,"repositori":[19,20,29,30],"repres":8,"represent":9,"reproduc":[17,30],"request":[4,19,30,32],"requested_ijk":4,"requir":[9,10,19,22,29,30,32,45],"reservoir":[6,11,14,15,38,40,43],"resinsight":[17,20,21,22,24,25,27,37,41,42,43,45],"resolv":10,"resourc":30,"resource_directori":9,"respect":[17,35,40,41,42],"rest":[22,31],"restart":[6,8],"restrict":[4,35],"result":[4,15,17,20,21,22,23,30,35,37,40,42,45],"retain":[4,6,9,10,18,20,21,28,30,33],"retained_wel":[5,10],"return":[4,6,7,8,9,10,11,12,13,14,15,34],"review":[19,30],"rewrit":[10,14,16,28],"rewritten":10,"right":[16,21,22,23,26,27,37,40,41,42,43,45],"rightarrow":40,"rise":19,"rock":[9,17,40],"rock_property_set":17,"rocknum":31,"rot":22,"rotat":[14,28,30,35,37,39,43,45],"rotatexi":[4,14,16,24,35,43,45],"rotatexz":[4,14,35],"rotateyz":[4,14,35],"round":7,"round_like_":[2,5,7],"row":[6,9,15],"row_offset":15,"ruff":19,"rule":[28,30],"run":[0,4,8,12,17,19,20,21,22,23,24,25,26,27,28,29,30,36,37,41,42,43,45],"run_simul":[2,5,12],"runs_execut":[2,5],"runtim":17,"rx":[4,32,45],"ry":[4,32,45],"rz":[4,32],"s":[4,11,17,19,22,23,25,26,27,32,43,45],"sandv":17,"sat":17,"satisfi":9,"satnum":[6,17,31,40],"satnum_generation_method":17,"satnum_opm":6,"satur":[6,7,12,17,21,22,25,40],"saturation_function_method":17,"save":[17,34],"scale":[4,14,17,28,30,31,32,35,37,39,40,43],"scan":10,"sch":25,"schedul":[7,10,22],"schedule_keyword":[5,10],"score":21,"screenshot":[20,37,42],"script":[0,12,19,20,29,40],"scriptsiz":40,"second":[6,17,22,40],"section":[10,22,34,45],"see":[17,19,20,21,22,23,25,26,27,28,29,30,33,40,41,42,43,45],"seed":17,"seek":30,"seem":22,"segment":10,"segmented_wel":[5,10],"seismic":43,"select":[4,6,8,9,10,13,15,16,17,18,28,30,36,37,42,45],"selector":[5,15],"send":19,"separ":[4,5,6,9,10,33,35],"sequenc":13,"set":[4,6,9,10,12,15,17,21,22,29,31,34,35,37,40,41],"setuptool":29,"seven":19,"sever":43,"sg":[22,23,26],"sgas":22,"sh":[17,20,21,22,23,25,26,27,29,40,41,42,43,45],"shape":[5,7,9,15,17,42,43],"share":9,"shift":42,"short":8,"show":[20,22,24,37,40,42],"shown":17,"side":42,"signific":[4,7,34],"significant_digit":[4,7,9],"similar":40,"simpl":40,"simpli":[29,40],"simul":[4,9,12,17,19,20,22,23,26,28,29,30,36,37,38,40,42],"simulation_model":26,"sinc":[22,40,42,43],"singl":[6,12,17,19],"site":38,"six":40,"size":[6,42],"skip":29,"skip_block":[5,10],"sm":22,"small":[30,45],"smaller":42,"smallest":6,"smeaheia":[20,30,31],"solut":[4,10,34,40,45],"soon":19,"sort":12,"sourc":[4,5,6,7,8,9,10,11,12,13,14,15,19,21,28,30,38],"source_indic":[5,11,15],"source_surfac":11,"south":15,"southeast":15,"southwest":15,"spatial":40,"spe10":[20,30],"spe10_model2":27,"spe10_model2_pycopm":27,"spe11":38,"special":[22,25,40],"specif":[4,6,9,14,15,17,18,20,21,22,28,29,30],"specifi":31,"speed":40,"sphinx":16,"squash":19,"src":19,"st":[23,26],"stabl":16,"stage":[4,28,30,36],"standard":[17,20,22,23,40],"start":[12,18,20,21,45],"state":10,"statement":[6,10],"static":[4,19,24,35,36],"statoil_feasibility_sim_model_with_depletion_kross_inj_sector_20":26,"statoil_feasibility_sim_model_with_depletion_kross_inj_sector_20_prep_pycopm_dryrun":26,"statoil_feasibility_sim_model_with_depletion_kross_inj_sector_20_pycopm":26,"stderr":13,"step":[4,19],"still":[21,22],"stop":17,"storag":[17,30,38],"store":[6,10,11,15],"str":[4,6,7,8,9,10,11,13,14,15],"straight":43,"strategi":15,"stream":13,"string":[9,15],"studi":[4,12,17,18,20,21,28,29,30,37,38,40],"sub":[27,35],"subdivis":11,"subgrid":15,"submodel":[0,4,8,10,15,16,17,20,24,27,28,30,31,32,37,39,44,45],"submodel_index":15,"submodul":[1,2],"subpackag":1,"subsect":16,"substract":45,"subsurfac":[21,30,40],"subsystem":29,"succed":29,"succeed":[17,19],"success":[8,13,17],"sudmodel":42,"sudo":29,"suffix":[7,17],"suggest":13,"suitabl":9,"sum":[6,31,40,42],"sum_":40,"summari":[6,22,37],"suppli":4,"support":[4,6,10,13,14,17,28,29,30],"surfac":[6,11,43],"sustain":30,"swatinit":[22,31],"syntax":[4,7,28,30],"sys":4,"system":[14,19,31],"systemexit":[4,9,13],"t":[4,22,23,26,32,40],"t_":40,"tabl":[6,7,9,12,16,40],"tag":29,"tailor":[20,28,30],"taken":40,"tap":[20,29],"target":[18,19],"task":16,"tcpu":22,"techniqu":40,"templat":[7,9,30],"template_path":7,"template_script":0,"temporari":10,"term":19,"termin":[2,5,9,17,20,29,34,40],"test":[0,17,19,20,21,29,30,40],"test_output":19,"test_run":17,"text":[6,7,13,34,45],"textbook":40,"textiowrapp":13,"tf":16,"tg":17,"thconr":31,"theori":[22,30,45],"thick":43,"thousand":27,"three":[9,10,22,33,35,40,42],"threshold":[4,32],"time":[12,17,22,26,43],"tip":[13,17],"tmltg":17,"tmlto":17,"tog":17,"toml":[4,6,7,9,12,16,17,20,21,28,29,30,36],"tool":[19,20,22,28,29,30,37,38,43],"top":[4,17,21,22,24,25,26,27,42,43,45],"total":[6,15,22],"track":10,"tracker":30,"trade":40,"trailing_column":15,"trailing_row":15,"trani":6,"transfer":6,"transform":[0,2,4,5,8,16,17,20,24,28,30,37,39,44,45],"transform_grid":[2,5,14],"transform_properti":[2,5,14],"translat":[4,14,28,30,35,37,39,43],"transmis":[34,40],"transmiss":[4,6,7,28,30,31,39,40],"transmissibili":31,"transmissibility_coarsening_method":4,"transsmiss":40,"tranx":6,"tri":[19,20,29,31],"trial":22,"trim":42,"true":[7,20,45],"trust":[20,29],"tstep":22,"tu":22,"tune":31,"tupl":[6,7,9,10,11,15],"turn":29,"tutori":[18,19,28,29,30],"tw":17,"two":[4,8,21,22,23,30,33,40,42,43],"txt":[19,29],"type":[4,6,7,8,9,10,11,12,13,14,15,17,20,29],"typeguard":9,"typic":43,"ubuntu":[19,29],"unassign":15,"uncertainti":[28,30,43],"unchang":[13,14],"understand":[28,30],"uniform":[4,9,17,31,32,33],"unknown":[4,9],"unlimit":17,"unprocess":4,"unrefin":41,"unwant":4,"updat":[6,8,10,11,14,15],"upgrad":29,"upper":[9,41,42],"uppercas":9,"upscal":[28,30,31,39,40],"use":[0,4,6,8,9,10,13,15,17,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,37,38,40,41,42,43,45],"user":[9,17,18,19,20,29,30,45],"utf":[7,13],"utf8":[4,34],"util":[0,1,2,4],"v":[4,7,16,22,23,24,26,27,32,40,42,45],"v2026":29,"valid":[4,8,9,13,16],"valu":[4,6,7,8,9,10,11,13,14,15,17,22,26,27,28,30,34,35,40,41,42,45],"variabl":[7,9,19,35],"vector":22,"venv":29,"verifi":30,"version":[2,19,28,29,30],"vertic":[6,16,40,41],"vertical_transfer_en":[5,6],"via":[17,22,29,36,37,45],"vicin":[2,4,5,8,10,27,35],"vicinity_specif":4,"vicinitymap":[2,5,8,10,15],"view":[10,22,25,26,42],"vile":22,"virtual":[28,29],"visaluaz":24,"visit":[17,19],"visual":[21,22,24,28,30,37,41,42,43,44,45],"volum":[4,6,8,11,15,22,24,25,27,28,30,31,39,40,42,44,45],"vpycopm":29,"vtk":[20,45],"vtks":38,"w":[4,13,22,23,24,27],"warn":[9,13],"water":[22,31],"webviz":21,"weight":[4,17,22,31,40],"welcom":[19,30],"well":[4,6,10,15,17,19,20,22,24,27,28,30,34,35,38,39,41,42,43],"well_cel":[5,15],"well_cell_indic":10,"well_indic":[5,15],"wellcind":[6,10],"welldim":[5,10],"welln":[16,35],"wellspec":22,"welseg":[5,10],"welspec":[5,10],"west":15,"wgpr":17,"wheel":29,"whether":[6,9,10,13],"whole":31,"whose":[6,8,11,15],"wide":43,"will":[17,19,26,34],"window":20,"within":[9,11,30],"without":[6,17,25,37,44],"wopr":17,"work":[4,19,28,29,30,44],"workflow":[4,6,8,9,12,16,18,28,29,32,40,44],"world":[19,20,28,29,30,44],"wrap":13,"write":[4,6,7,9,11,15,17,28,29,30,31,34,45],"write_coarsened_model_fil":[2,5,7],"write_compact_property_fil":[2,5,7],"write_dual_properti":[2,5,7],"write_explicit_solut":4,"write_grid":[2,5,7],"write_includ":[2,5,7],"write_porv":[2,5,7],"write_properti":[2,5,7],"write_property_inc":[2,5,7],"write_reference_to_coarse_map":[2,5,7],"written":[6,7,11,14,15,42],"wsl":29,"wwpr":17,"x":[4,5,6,9,10,11,14,15,17,22,25,26,28,32,35,40,45],"x_coarsen":[4,6,17],"x_refin":[4,11],"xf":[22,26],"xi_":40,"xnt":22,"xu":[22,26],"xy":[4,15,28,30,35,40,42],"xypolygon":[24,32,35,42,45],"xyz":[31,40],"xz":6,"y":[4,5,6,9,10,11,14,15,17,20,22,25,26,28,29,32,35,40,45],"y_coarsen":[4,6,17],"y_refin":[4,11],"yf":[22,26],"yl":22,"yml":[19,29],"ynt":22,"yournam":19,"yu":[22,26],"z":[4,5,6,9,10,11,14,15,16,17,22,23,25,28,31,32,35,40],"z_coarsen":[4,6,17],"z_refin":[4,11],"zc":[6,7],"zcorn":[6,7,11],"zcorn_indic":7,"zcorn_x_fract":11,"zcorn_x_indic":11,"zcorn_y_fract":11,"zcorn_y_indic":11,"zero":[6,7,15,32,40]},"titles":["Python API","pycopm","pycopm package","pycopm.core package","pycopm.core.pycopm module","pycopm.utils package","pycopm.utils.coarsening module","pycopm.utils.files_writer module","pycopm.utils.generate_decks module","pycopm.utils.input_values module","pycopm.utils.parser_deck module","pycopm.utils.refinement module","pycopm.utils.runs_executer module","pycopm.utils.terminal module","pycopm.utils.transformation module","pycopm.utils.vicinity module","Command-line reference","Complete legacy configuration reference","Legacy TOML configuration","Contributing","Examples","Legacy configuration workflows","Drogon","Dual coarsening","Graphical abstract","Norne","Smeaheia","SPE10","pycopm","Installation","Introduction","Property aggregation and conservation","Option compatibility and validation","Grid coarsening and refinement","Deck rewriting and output","Submodels, transformations, and dual coarsening","Input and workflow","Output folder","Related projects","Theory and methods","Grid coarsening","Grid refinement","Submodels","Transformations","Tutorial","Hello World: generic deck workflow"],"titleterms":{"About":30,"I":34,"What":28,"Where":30,"abstract":24,"active_cell_method":31,"aggreg":31,"api":0,"base":37,"binari":29,"brew":29,"build":29,"c":33,"can":28,"citat":30,"coarsen":[6,23,33,35,40],"command":[16,36],"common":16,"compat":32,"complet":[17,45],"completion_removal_level":34,"concept":30,"configur":[17,18,21],"conserv":31,"content":[2,3,5],"continu":30,"continuous_aggregation_method":31,"contribut":19,"core":[3,4,30],"correct_fluid_in_plac":31,"criterion":35,"d":35,"deck":[20,34,37,45],"deck_encod":34,"discrete_aggregation_method":31,"drogon":22,"dual":[23,35],"dual_porosity_criterion":35,"e":34,"encod":34,"evolut":30,"exampl":20,"execution_mod":36,"explicit":34,"f":36,"files_writ":7,"flow":29,"flow_command":36,"folder":[36,37],"formula":29,"g":33,"generate_deck":8,"generic":45,"graphic":24,"grid":[33,40,41],"grid_transform":35,"ground":19,"hello":45,"ijk":34,"include_prefix":34,"input":36,"input_deck_path":36,"input_valu":9,"instal":[28,29],"introduct":30,"issu":19,"j":[31,34],"jump_threshold":31,"k":34,"l":34,"learn":45,"legaci":[17,18,21,37],"line":16,"linux":29,"m":36,"maco":29,"method":[31,39],"mode":36,"modul":[2,3,4,5,6,7,8,9,10,11,12,13,14,15],"n":31,"name":34,"next":[29,45],"norn":25,"o":36,"object":45,"opm":29,"option":32,"output":[34,37],"output_deck_nam":34,"output_directori":36,"p":31,"packag":[2,3,5,29],"parser_deck":10,"pore_volume_correct":31,"precis":34,"prefix":34,"problem":19,"project":[30,38],"properti":31,"pycopm":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,28],"python":[0,29],"q":31,"quick":28,"r":34,"recommend":[30,37],"refer":[16,17],"refin":[11,33,41],"relat":38,"report":19,"reproduc":20,"requested_ijk":34,"rewrit":34,"rule":19,"runs_execut":12,"rx":33,"ry":33,"rz":33,"s":31,"scope":20,"seek":19,"significant_digit":34,"smeaheia":26,"softwar":19,"sourc":29,"spe10":27,"specif":35,"start":28,"step":[29,45],"submodel":[35,42],"submodul":[3,5],"subpackag":2,"support":19,"t":31,"termin":13,"theori":39,"toml":[18,37],"transform":[14,35,43],"transmissibility_coarsening_method":31,"tutori":44,"util":[5,6,7,8,9,10,11,12,13,14,15],"v":35,"valid":32,"valu":[31,33],"verifi":29,"vicin":15,"vicinity_specif":35,"visual":20,"w":34,"window":29,"workflow":[20,21,30,36,37,45],"world":45,"write_explicit_solut":34,"x":33,"x_coarsen":33,"x_refin":33,"y":33,"y_coarsen":33,"y_refin":33,"z":33,"z_coarsen":33,"z_refin":33}}) \ No newline at end of file diff --git a/docs/text/_static/custom.css b/docs/text/_static/custom.css new file mode 100644 index 0000000..b704b1d --- /dev/null +++ b/docs/text/_static/custom.css @@ -0,0 +1,101 @@ +:root { + --pst-color-primary: #1769aa; + --pst-color-secondary: #3f8fc5; + --pst-color-link: #1769aa; + --pst-color-link-hover: #0f4f82; +} + +html[data-theme="dark"] { + --pst-color-primary: #71b7e6; + --pst-color-secondary: #8cc8ec; + --pst-color-link: #71b7e6; + --pst-color-link-hover: #a7d8f4; +} + +.bd-main .bd-content .bd-article-container { + max-width: 72rem; +} + +.bd-page-width { + max-width: 96rem; +} + +.bd-header .navbar-header-items__start { + min-width: max-content; +} + +.navbar-brand .title { + font-weight: 700; + letter-spacing: 0.01em; +} + +.bd-header .navbar-brand img { + max-height: 102px; +} + +.sd-card { + height: 100%; + overflow: hidden; + border-radius: 0.75rem; + transition: + transform 160ms ease, + box-shadow 160ms ease, + border-color 160ms ease; +} + +.sd-card-title { + font-weight: 650; +} + +.sd-card:hover { + transform: translateY(-3px); + border-color: var(--pst-color-primary); + box-shadow: 0 0.5rem 1.25rem rgb(0 0 0 / 14%); +} + +.sd-card .sd-card-title { + color: var(--pst-color-primary); +} + +.sd-card .sd-card-body { + padding: 1rem; +} + +.rst-class.lead, +.lead { + font-size: 1.2rem; + color: var(--pst-color-text-muted); +} + +div.highlight pre { + font-size: 0.9rem; +} + +table.docutils { + width: 100%; +} + +.sd-card-img-top { + display: block; + width: 100%; + height: 250px; + object-fit: contain; + object-position: center; + padding: 0.75rem; + background: var(--pst-color-surface); + border-bottom: 1px solid var(--pst-color-border); +} + +html[data-theme="dark"] .sd-card-img-top { + opacity: 0.92; +} + +.related-projects .sd-card-img-top { + height: 260px; + padding: 1rem; +} + +.related-projects .sd-card-footer { + color: var(--pst-color-primary); + font-weight: 600; +} diff --git a/docs/text/about.rst b/docs/text/about.rst deleted file mode 100644 index ca75d25..0000000 --- a/docs/text/about.rst +++ /dev/null @@ -1,12 +0,0 @@ -============ -About pycopm -============ - -.. image:: ./figs/pycopm.png - -The **pycopm** tool is being funded by the `Center for Sustainable Subsurface Resources (CSSR) `_ [project no. 331841] -and by `Expansion of Resources for CO2 Storage on the Horda Platform (ExpReCCS) `_ [project no. 336294]. - -Contributions are more than welcome using the fork and pull request approach. - -For a new feature, please request this raising an issue. diff --git a/docs/text/api.rst b/docs/text/api.rst index 9840f1f..d585672 100644 --- a/docs/text/api.rst +++ b/docs/text/api.rst @@ -1,7 +1,7 @@ -================= -pycopm Python API -================= +.. _api-reference: +Python API +========== The main script for the **pycopm** executable is located in the core folder. The different jobs called by ERT are located in the jobs folder. The reference_simulation folder contains the generated files after running Flow in the norne and drogon case in the opm-test folder. @@ -11,6 +11,9 @@ grid refinement, grid coarsening, submodels, and transformations. .. figure:: figs/contents.png - Files in the pyocpm package. + Files in the pycopm package. -.. include:: modules.rst +.. toctree:: + :maxdepth: 2 + + api/modules diff --git a/docs/text/modules.rst b/docs/text/api/modules.rst similarity index 100% rename from docs/text/modules.rst rename to docs/text/api/modules.rst diff --git a/docs/text/pycopm.core.pycopm.rst b/docs/text/api/pycopm.core.pycopm.rst similarity index 100% rename from docs/text/pycopm.core.pycopm.rst rename to docs/text/api/pycopm.core.pycopm.rst diff --git a/docs/text/pycopm.core.rst b/docs/text/api/pycopm.core.rst similarity index 100% rename from docs/text/pycopm.core.rst rename to docs/text/api/pycopm.core.rst diff --git a/docs/text/pycopm.rst b/docs/text/api/pycopm.rst similarity index 100% rename from docs/text/pycopm.rst rename to docs/text/api/pycopm.rst diff --git a/docs/text/pycopm.utils.coarsening.rst b/docs/text/api/pycopm.utils.coarsening.rst similarity index 100% rename from docs/text/pycopm.utils.coarsening.rst rename to docs/text/api/pycopm.utils.coarsening.rst diff --git a/docs/text/pycopm.utils.files_writer.rst b/docs/text/api/pycopm.utils.files_writer.rst similarity index 100% rename from docs/text/pycopm.utils.files_writer.rst rename to docs/text/api/pycopm.utils.files_writer.rst diff --git a/docs/text/pycopm.utils.generate_decks.rst b/docs/text/api/pycopm.utils.generate_decks.rst similarity index 100% rename from docs/text/pycopm.utils.generate_decks.rst rename to docs/text/api/pycopm.utils.generate_decks.rst diff --git a/docs/text/pycopm.utils.input_values.rst b/docs/text/api/pycopm.utils.input_values.rst similarity index 100% rename from docs/text/pycopm.utils.input_values.rst rename to docs/text/api/pycopm.utils.input_values.rst diff --git a/docs/text/pycopm.utils.parser_deck.rst b/docs/text/api/pycopm.utils.parser_deck.rst similarity index 100% rename from docs/text/pycopm.utils.parser_deck.rst rename to docs/text/api/pycopm.utils.parser_deck.rst diff --git a/docs/text/pycopm.utils.refinement.rst b/docs/text/api/pycopm.utils.refinement.rst similarity index 100% rename from docs/text/pycopm.utils.refinement.rst rename to docs/text/api/pycopm.utils.refinement.rst diff --git a/docs/text/pycopm.utils.rst b/docs/text/api/pycopm.utils.rst similarity index 95% rename from docs/text/pycopm.utils.rst rename to docs/text/api/pycopm.utils.rst index 32cd074..47fcac7 100644 --- a/docs/text/pycopm.utils.rst +++ b/docs/text/api/pycopm.utils.rst @@ -14,6 +14,7 @@ Submodules pycopm.utils.parser_deck pycopm.utils.refinement pycopm.utils.runs_executer + pycopm.utils.terminal pycopm.utils.transformation pycopm.utils.vicinity diff --git a/docs/text/pycopm.utils.runs_executer.rst b/docs/text/api/pycopm.utils.runs_executer.rst similarity index 100% rename from docs/text/pycopm.utils.runs_executer.rst rename to docs/text/api/pycopm.utils.runs_executer.rst diff --git a/docs/text/api/pycopm.utils.terminal.rst b/docs/text/api/pycopm.utils.terminal.rst new file mode 100644 index 0000000..54f8943 --- /dev/null +++ b/docs/text/api/pycopm.utils.terminal.rst @@ -0,0 +1,8 @@ +pycopm.utils.terminal module +============================ + +.. automodule:: pycopm.utils.terminal + :members: + :private-members: + :show-inheritance: + :undoc-members: diff --git a/docs/text/pycopm.utils.transformation.rst b/docs/text/api/pycopm.utils.transformation.rst similarity index 100% rename from docs/text/pycopm.utils.transformation.rst rename to docs/text/api/pycopm.utils.transformation.rst diff --git a/docs/text/pycopm.utils.vicinity.rst b/docs/text/api/pycopm.utils.vicinity.rst similarity index 100% rename from docs/text/pycopm.utils.vicinity.rst rename to docs/text/api/pycopm.utils.vicinity.rst diff --git a/docs/text/command-line.rst b/docs/text/command-line.rst new file mode 100644 index 0000000..05dd4dc --- /dev/null +++ b/docs/text/command-line.rst @@ -0,0 +1,43 @@ +.. _command-line-reference: + +Command-line reference +====================== + +A **pycopm** command selects a generic OPM Flow ``.DATA`` deck or a legacy TOML +configuration: + +.. code-block:: console + + pycopm -i INPUT [OPTIONS] + +The reference is organized by task. On each child page, every flag is an +explicit subsection heading, so the flags appear in the right-hand page table +of contents. The Sphinx ``option`` directives remain in place for stable +cross-references. + +.. note:: + + Generic deck input is the actively developed workflow. All options apply to + ``.DATA`` decks. Only ``-i``, ``-o``, ``-f``, and ``-precision`` apply to + legacy TOML files. + +.. toctree:: + :maxdepth: 1 + + options/workflow + options/grid + options/aggregation + options/submodels-transformations + options/output + options/compatibility + +Common commands +--------------- + +.. code-block:: console + + pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output + pycopm -i MODEL.DATA -z 1:10,11:15 -a min,mode -p 1 -m all + pycopm -i MODEL.DATA -v 'WELLN box [-1,1] [-2,2] [0,3]' -p 1 -m all + pycopm -i MODEL.DATA -d 'rotatexy 45' -m all + pycopm -i MODEL6.DATA -z 1:4 -dual 'poro <= 0.1, vertical TF = 0' -m all diff --git a/docs/text/conf.py b/docs/text/conf.py index 54dcfd3..393c43b 100644 --- a/docs/text/conf.py +++ b/docs/text/conf.py @@ -1,33 +1,74 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html +"""Sphinx configuration for the pycopm documentation.""" +from __future__ import annotations +import os +import sys +from importlib.metadata import PackageNotFoundError, version as package_version +from pathlib import Path -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +DOCS_DIR = Path(__file__).resolve().parent +REPOSITORY_ROOT = DOCS_DIR.parents[1] +SRC_DIR = REPOSITORY_ROOT / "src" +sys.path.insert(0, str(SRC_DIR)) -project = 'pycopm' -copyright = '2024-2026, NORCE Research AS' +project = "pycopm" +copyright = "2024-2026, NORCE Research AS" +author = "pycopm contributors" +try: + release = package_version("pycopm") +except PackageNotFoundError: + release = os.environ.get("PYCOPM_DOCS_VERSION", "development") +version = release -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.napoleon'] - -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -autodoc_default_options = { - 'members': True, - 'undoc-members': False, +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.viewcode", + "numpydoc", + "sphinx_copybutton", + "sphinx_design", +] +autosummary_generate = True +autodoc_member_order = "bysource" +autodoc_typehints = "description" +add_module_names = False +numpydoc_show_class_members = False +root_doc = "index" +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +smartquotes = False +html_theme = "pydata_sphinx_theme" +html_title = "pycopm documentation" +html_logo = "figs/logo.png" +html_static_path = ["_static"] +html_css_files = ["custom.css"] +html_context = { + "github_user": "cssr-tools", + "github_repo": "pycopm", + "github_version": "main", + "doc_path": "docs/text", } - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = 'sphinx_rtd_theme' -html_logo = "./figs/logo.png" html_theme_options = { - "logo_only": True, - "style_nav_header_background": "#aaaaaa", + "navbar_start": ["navbar-logo"], + "navbar_center": ["navbar-nav"], + "navbar_persistent": ["search-button"], + "navbar_end": ["theme-switcher", "navbar-icon-links"], + "header_links_before_dropdown": 7, + "show_toc_level": 2, + "navigation_depth": 4, + "collapse_navigation": True, + "show_nav_level": 1, + "back_to_top_button": True, + "use_edit_page_button": True, + "secondary_sidebar_items": ["page-toc", "edit-this-page", "sourcelink"], + "logo": {"alt_text": "pycopm documentation - Home"}, + "icon_links": [ + {"name": "Report an issue", "url": "https://github.com/cssr-tools/pycopm/issues/new/choose", "icon": "fa-solid fa-bug", "type": "fontawesome"}, + {"name": "GitHub repository", "url": "https://github.com/cssr-tools/pycopm", "icon": "fa-brands fa-github", "type": "fontawesome"}, + ], } +pygments_style = "sphinx" +pygments_dark_style = "monokai" +copybutton_prompt_text = r">>> |\.\.\. |\$ |# " +copybutton_prompt_is_regexp = True +html_show_sourcelink = True +html_show_sphinx = False +html_last_updated_fmt = "%Y-%m-%d" diff --git a/docs/text/configuration/complete-reference.rst b/docs/text/configuration/complete-reference.rst new file mode 100644 index 0000000..60872d9 --- /dev/null +++ b/docs/text/configuration/complete-reference.rst @@ -0,0 +1,141 @@ +.. _legacy-configuration-complete: + +Complete legacy configuration reference +======================================= + +This page preserves the complete original TOML documentation, including the full arrays, parameter explanations, ERT guidance, figures, and tips. + +.. Note:: + The configuration files allow to set the integrated studies (coarsening and history matching) + only for the drogon and norne model. To use **pycopm** in any given OPM Flow geological model + to generate modified files (coarsening, refinement, submodels, and transformations), this can + be achieved without a configuration file, but setting the parameters via command lines (see + the :doc:`../command-line` or run `pycopm -h` for the definition of the argument options, as well as + the examples in :doc:`../examples`.) + + +Here we use as an example one of the configuration files used in the tests +(see `input.toml `_). +The first input parameter is: + +.. code-block:: python + :linenos: + + # Set mpirun, the full path to the flow executable, and simulator flags (except --output-dir) + flow_command = "flow --newton-min-iterations=1" + +If **flow** is not in your path, then write the full path to the executable, as well as adding mpirun +if this is supported in your machine (e.g., flow_command = "mpirun -np 8 /Users/dmar/Github/opm/build/opm-simulators/bin/flow -\-newton-min-iterations=1"). + +The next entries define the following parameters: + +.. code-block:: python + :linenos: + :lineno-start: 4 + + # Set the model parameters + model_name = "norne" # Geological model (norne or drogon) + execution_mode = "single-run" # Mode to run (single-run, files, or ert) + x_coarsening = [0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0] # Array of x-coarsening + y_coarsening = [0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0] # Array of y-coarsening + z_coarsening = [0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0] # Array of z-coarsening + ensemble_size = 2 # Number of ensembles + max_parallel_realizations = 2 # Maximum number of ensembles running in parallel + max_realization_runtime_seconds = 600 # Maximum runtime in seconds of a realization. A value of 0 means unlimited runtime. + min_successful_realizations = 2 # Minimum number of realizations that must have succeeded for the simulation to be regarded as a success. + random_seed = 7 # Set a specific seed for reproducibility. A value of 0 means no seed. + saturation_function_method = 0 # Select which coarser deck to use: 0 -> default one or 1 -> LET sat functions + satnum_generation_method = 1 # For norne: for the LET coarser deck, select: 0 -> SATNUM=1, 1 -> SATNUM is computed from Sandve et al 2022, 2 -> #SATNUM=#Cells. + pore_volume_correction = 0 # 0 -> no corrections for lost porv, 1 -> correct it on all cells + initialization_method = 0 # Initialization 0 -> Equil 1->INIT from fine-scale + observation_relative_errors = [0.1,0.1,0.1] # Error WWPR, WOPR, and WGPR + observation_minimum_errors = [100,100,100000] # Minimum error of WWPR, WOPR, and WGPR + history_matching_end_date = 2005-03-01 # Last date to HM + cleanup_file_suffixes = ["PRT"] # Delete files with this suffix to save storage + +The single-run mode results in Flow running only one simulation and the information shown in +the terminal is the one from the Flow executable instead of the ERT one. The ert option calls the ERT +executable and the command values are given after in the same input file which are discussed later. The +files option only writes the needed input files for ERT. + +The x-, y, and z-coarsening inputs define which pillars are removed (value 2) in the x, y, and z direction respectively. + +.. figure:: ../figs/index.png +.. figure:: ../figs/index_plopm.png + + I, J, and K cell index in the standard Norne data set (top figures from `ResInsight `_ and bottom figures from `plopm `_, see/run `docs_configs_views.sh `_). + +Values of 0 do not remove the pilar. + +.. tip:: + The 0 values are used to keep pillars in order to honor the main Norne shape. Then from the + example provided here, change the 2's to 0's to add back the pillars. + +The remaining lines define the number of ensembles, maximum number of ensembles running in parallel, the maximum runtime in seconds to stop a realization +(0 means unlimited time), the minimum number of realizations for the simulation to be regarded as a success, +the random seed for reproducibility (0 means no seed), to use the saturation +functions from opm-tests norne or to use the LET saturation functions, defining if each of the coarser cells is +considered as only one region, regions as in Sandve et al 2022 or as a different SATNUM region, and the cporv entry +sets if the ntg and poro properties on the cells on the boundary are modified to account for lost PV respect to the +default simulation or if the porosity of all cells is modified to match this value. The last entry set the type of files to +delete after each realization is completed to save memory. + +If the ert option is activated, then the following input: + +.. code-block:: python + :linenos: + :lineno-start: 25 + + # Set the command line for the ert executable (gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda) and flags + ert_arguments = "es_mda --weights 1" + +sets the type of ert option and command flags. Currently, the options supported are gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda. +Confer to the `ERT `_ documentation for a full description of these options. + +The LET saturation function parameters for each of the coarsened cells are given in the following entry: + +.. code-block:: python + :linenos: + :lineno-start: 28 + + # Properties LET saturation functions: name, value, use dist in hm?, dist, distpara, distpara + let_parameters = [["lw", 3, 1, "UNIFORM", 1.1, 5], + ["ew", 1, 1, "UNIFORM", -1, 2], + ["tw", 3, 1, "UNIFORM", 1.1, 5], + ["lo", 3, 1, "UNIFORM", 1.1, 5], + ["eo", 1, 1, "UNIFORM", -1, 2], + ["to", 3, 1, "UNIFORM", 1.1, 5], + ["lg", 3, 1, "UNIFORM", 1.1, 5], + ["eg", 1, 1, "UNIFORM", -1, 2], + ["tg", 3, 1, "UNIFORM", 1.1, 5], + ["log", 3, 1, "UNIFORM", 1.1, 5], + ["eog", 1, 1, "UNIFORM", -1, 2], + ["tog", 1, 1, "UNIFORM", 1.1, 5], + ["lmlto", 1.5, 1, "UNIFORM", 1, 2], + ["emlto", 1, 1, "UNIFORM", 0.9, 2.1], + ["tmlto", 1.5, 1, "UNIFORM", 1, 2], + ["lmltg", 1.5, 1, "UNIFORM", 1, 2], + ["emltg", 1, 1, "UNIFORM", 0.9, 2.1], + ["tmltg", 1.5, 1, "UNIFORM", 1, 2]] + +The 'use dist in hm?' defines if the property will be history match (1) or the provided value in the second entry will be used (0). + +Finally, we set if the permeabilities will be considered for the hm: + +.. code-block:: python + :linenos: + :lineno-start: 48 + + # Coarser rock properties: name, use dist in hm?, coarsing approach (max or mean) + rock_property_settings = [["PERMX", 1, "max"], + ["PERMY", 0, "max"], + ["PERMZ", 0, "max"]] + +If option 1 is selected, then the distributions are UNIFORM with interval values of [perm_min, perm_max], corresponding to the +minimum and maximum values in each of the coarser cells. + +.. tip:: + By setting the "mode = 'files'" in the toml configuration file, only the needed files to run a history matching using + ERT are generated. Then one can inspect those files and do additional modifications before running the history matching by + calling directly the ert executable. If you are not familiar with the format of the files to use ERT, visit + `the ERT documentation `_. diff --git a/docs/text/configuration_file.rst b/docs/text/configuration_file.rst index 509f3d0..f7ff1f5 100644 --- a/docs/text/configuration_file.rst +++ b/docs/text/configuration_file.rst @@ -1,137 +1,17 @@ -================== -Configuration file -================== -.. Note:: - The configuration files allow to set the integrated studies (coarsening and history matching) - only for the drogon and norne model. To use **pycopm** in any given OPM Flow geological model - to generate modified files (coarsening, refinement, submodels, and transformations), this can - be achieved without a configuration file, but setting the parameters via command lines (see - the :ref:`overview` or run `pycopm -h` for the definition of the argument options, as well as - the examples in :ref:`generic`.) +.. _configuration-file: +Legacy TOML configuration +========================= -Here we use as an example one of the configuration files used in the tests -(see `input.toml `_). -The first input parameter is: +TOML configuration files define integrated coarsening and history-matching studies for the predefined Norne and Drogon models. -.. code-block:: python - :linenos: +.. warning:: - # Set mpirun, the full path to the flow executable, and simulator flags (except --output-dir) - flow_command = "flow --newton-min-iterations=1" - -If **flow** is not in your path, then write the full path to the executable, as well as adding mpirun -if this is supported in your machine (e.g., flow_command = "mpirun -np 8 /Users/dmar/Github/opm/build/opm-simulators/bin/flow -\-newton-min-iterations=1"). + This workflow is retained for legacy, model-specific studies. Current development targets generic OPM Flow deck input. New users should start with :doc:`tutorial` and :doc:`command-line`. -The next entries define the following parameters: +Only ``-i``, ``-o``, ``-f``, and ``-precision`` apply on the command line when a TOML file is selected. -.. code-block:: python - :linenos: - :lineno-start: 4 +.. toctree:: + :maxdepth: 1 - # Set the model parameters - model_name = "norne" # Geological model (norne or drogon) - execution_mode = "single-run" # Mode to run (single-run, files, or ert) - x_coarsening = [0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0] # Array of x-coarsening - y_coarsening = [0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0] # Array of y-coarsening - z_coarsening = [0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0] # Array of z-coarsening - ensemble_size = 2 # Number of ensembles - max_parallel_realizations = 2 # Maximum number of ensembles running in parallel - max_realization_runtime_seconds = 600 # Maximum runtime in seconds of a realization. A value of 0 means unlimited runtime. - min_successful_realizations = 2 # Minimum number of realizations that must have succeeded for the simulation to be regarded as a success. - random_seed = 7 # Set a specific seed for reproducibility. A value of 0 means no seed. - saturation_function_method = 0 # Select which coarser deck to use: 0 -> default one or 1 -> LET sat functions - satnum_generation_method = 1 # For norne: for the LET coarser deck, select: 0 -> SATNUM=1, 1 -> SATNUM is computed from Sandve et al 2022, 2 -> #SATNUM=#Cells. - pore_volume_correction = 0 # 0 -> no corrections for lost porv, 1 -> correct it on all cells - initialization_method = 0 # Initialization 0 -> Equil 1->INIT from fine-scale - observation_relative_errors = [0.1,0.1,0.1] # Error WWPR, WOPR, and WGPR - observation_minimum_errors = [100,100,100000] # Minimum error of WWPR, WOPR, and WGPR - history_matching_end_date = 2005-03-01 # Last date to HM - cleanup_file_suffixes = ["PRT"] # Delete files with this suffix to save storage - -The single-run mode results in Flow running only one simulation and the information shown in -the terminal is the one from the Flow executable instead of the ERT one. The ert option calls the ERT -executable and the command values are given after in the same input file which are discussed later. The -files option only writes the needed input files for ERT. - -The x-, y, and z-coarsening inputs define which pillars are removed (value 2) in the x, y, and z direction respectively. - -.. figure:: figs/index.png -.. figure:: figs/index_plopm.png - - I, J, and K cell index in the standard Norne data set (top figures from `ResInsight `_ and bottom figures from `plopm `_, see/run `docs_configs_views.sh `_). - -Values of 0 do not remove the pilar. - -.. tip:: - The 0 values are used to keep pillars in order to honor the main Norne shape. Then from the - example provided here, change the 2's to 0's to add back the pillars. - -The remaining lines define the number of ensembles, maximum number of ensembles running in parallel, the maximum runtime in seconds to stop a realization -(0 means unlimited time), the minimum number of realizations for the simulation to be regarded as a success, -the random seed for reproducibility (0 means no seed), to use the saturation -functions from opm-tests norne or to use the LET saturation functions, defining if each of the coarser cells is -considered as only one region, regions as in Sandve et al 2022 or as a different SATNUM region, and the cporv entry -sets if the ntg and poro properties on the cells on the boundary are modified to account for lost PV respect to the -default simulation or if the porosity of all cells is modified to match this value. The last entry set the type of files to -delete after each realization is completed to save memory. - -If the ert option is activated, then the following input: - -.. code-block:: python - :linenos: - :lineno-start: 25 - - # Set the command line for the ert executable (gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda) and flags - ert_arguments = "es_mda --weights 1" - -sets the type of ert option and command flags. Currently, the options supported are gui, test_run, ensemble_experiment, ensemble_smoother, iterative_ensemble_smoother, and es_mda. -Confer to the `ERT `_ documentation for a full description of these options. - -The LET saturation function parameters for each of the coarsened cells are given in the following entry: - -.. code-block:: python - :linenos: - :lineno-start: 28 - - # Properties LET saturation functions: name, value, use dist in hm?, dist, distpara, distpara - let_parameters = [["lw", 3, 1, "UNIFORM", 1.1, 5], - ["ew", 1, 1, "UNIFORM", -1, 2], - ["tw", 3, 1, "UNIFORM", 1.1, 5], - ["lo", 3, 1, "UNIFORM", 1.1, 5], - ["eo", 1, 1, "UNIFORM", -1, 2], - ["to", 3, 1, "UNIFORM", 1.1, 5], - ["lg", 3, 1, "UNIFORM", 1.1, 5], - ["eg", 1, 1, "UNIFORM", -1, 2], - ["tg", 3, 1, "UNIFORM", 1.1, 5], - ["log", 3, 1, "UNIFORM", 1.1, 5], - ["eog", 1, 1, "UNIFORM", -1, 2], - ["tog", 1, 1, "UNIFORM", 1.1, 5], - ["lmlto", 1.5, 1, "UNIFORM", 1, 2], - ["emlto", 1, 1, "UNIFORM", 0.9, 2.1], - ["tmlto", 1.5, 1, "UNIFORM", 1, 2], - ["lmltg", 1.5, 1, "UNIFORM", 1, 2], - ["emltg", 1, 1, "UNIFORM", 0.9, 2.1], - ["tmltg", 1.5, 1, "UNIFORM", 1, 2]] - -The 'use dist in hm?' defines if the property will be history match (1) or the provided value in the second entry will be used (0). - -Finally, we set if the permeabilities will be considered for the hm: - -.. code-block:: python - :linenos: - :lineno-start: 48 - - # Coarser rock properties: name, use dist in hm?, coarsing approach (max or mean) - rock_property_settings = [["PERMX", 1, "max"], - ["PERMY", 0, "max"], - ["PERMZ", 0, "max"]] - -If option 1 is selected, then the distributions are UNIFORM with interval values of [perm_min, perm_max], corresponding to the -minimum and maximum values in each of the coarser cells. - -.. tip:: - By setting the "mode = 'files'" in the toml configuration file, only the needed files to run a history matching using - ERT are generated. Then one can inspect those files and do additional modifications before running the history matching by - calling directly the ert executable. If you are not familiar with the format of the files to use ERT, visit - `the ERT documentation `_. + configuration/complete-reference diff --git a/docs/text/examples.rst b/docs/text/examples.rst index 84c19e2..b586128 100644 --- a/docs/text/examples.rst +++ b/docs/text/examples.rst @@ -1,6 +1,16 @@ -******** +.. _examples: + Examples -******** +======== + +Deck-based examples are the recommended and actively developed path. The legacy configuration example is retained for existing Norne and Drogon studies. + +.. warning:: + + For new applications, start with :doc:`tutorial/hello-world` and the deck-based examples. TOML workflows are legacy and model-specific. + +Visualization and reproducibility +--------------------------------- For additional examples demonstrating the applicability of **pycopm**, see the `tests `_. @@ -34,299 +44,76 @@ For additional examples demonstrating the applicability of **pycopm**, see the ` . ./tests/scripts/name_of_script.sh -======================= -Via configuration files -======================= - -The `examples `_ folder contains configuration files -to perform HM studies in drogon and norne using `ERT `_. For example, by executing inside the `example folder for drogon `_: - -.. code-block:: bash - - # From inside the main pycopm folder - cd examples/configurations/drogon - pycopm -i input.toml -o drogon_coarser - -The following are the drogon model from `opm-tests `_ and coarsened model generated using **pycopm** using ResInsight for the visualization: - -.. figure:: figs/drogon_coarser.png -.. figure:: figs/drogon_coarser_plopm.png - - Initial oil saturation from the input (left) and coarsened (right) models (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_config_drogon.sh `_). - -.. note:: - For Drogon, a scored is printed after the run to compared the error to the results available at - https://webviz-subsurface-example.azurewebsites.net/history-match. While `input.toml `_ - only runs one HM iteration with two ensemble members that is used in testing **pycopm**, `hm.toml `_ runs a history matching - with a better score (i.e., less error compare to the observation data). This configuration file is also an example of how to use mpi to run Flow built from source - (set the flow path to your flow location; if you do not have mpi, you can remove it and still run the example). - - -.. _generic: - -================== -Via OPM Flow decks -================== +Deck workflow scope +------------------- The current development of **pycopm** focuses on creating tailored models (grid refinement, grid coarsening, submodels, and transformations) by using input decks. While in the Hello world example these four different options are demonstrated, for the latter examples the focus is on the grid coarsening functionality, and the -SPE10 also shows the submodel functionality. - -Hello world ------------ -For the `HELLO_WORLD.DATA `_ deck, by executing: - -.. code-block:: bash - - # From inside the main pycopm folder - cd examples/decks - pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output - -.. note:: - - If the folder to flow is not added to your path, then pass the full path to the flow executable using the flag **-f /path/to/flow**. - -Using `plopm `_, then we can visualize the generated files in the output folder: - -.. figure:: figs/hello_world_1.png - - Dry run from the input cloned deck (left) and (right) coarsened model. Adding the flag **-p 1** would add the remove pore volume to the neighbouring cells (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). - -As mentioned above, if you do not have ResInsight, then to visualize the results in ParaView run - -.. code-block:: bash - - flow HELLO_WORLD.DATA --enable-vtk-output=true - flow HELLO_WORLD_PYCOPM.DATA --enable-vtk-output=true - -To make active the coarsened cell where there is only one active cell, this can be achieved by: - -.. code-block:: bash - - pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -a max - -.. figure:: figs/hello_world_2.png - - Dry run from the input cloned deck (left) and (right) coarsened model. The region numbers by default are given by the mode, e.g., use the flag **-n max** to keep the maximum integer (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). - -As described in the :doc:`theory <./theory>`, **pycopm** can be not only used for grid coarsening, but also to apply grid refinements, submodels, and transformations. -Then, with the following commands first we substract a submodel around the isolated grid cell proyecting the outside pore volume on the boundaries, after -we apply a grid refinement on the cells in the middle x and y location, and finally we rotate the model 45 degrees. - -.. code-block:: bash - - pycopm -i HELLO_WORLD.DATA -v 'xypolygon [4,8.5] [4,16.5] [11.5,16.5] [11.5,8.5] [4,8.5]' -p 1 -m all - pycopm -i HELLO_WORLD_PYCOPM.DATA -rx 0,0,0,2,0,0,0 -ry 0,0,0,2,0,0,0 -m all - pycopm -i HELLO_WORLD_PYCOPM_PYCOPM.DATA -d 'rotatexy 45' -m all - -.. figure:: figs/hello_world_3.png - - Extracted region with the projected pore volumes (bottom left), refinement around the center cells (top right), and rotation (bottom right). - The text in the legends highlight that the pore volume is conserved (35.58) and the number of active cells is reduced from 351 to 25 in the - submodel and after increased to 41 due to the grid refinement (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). - -.. note:: - - To write the cell values for the SOLUTION section instead of using the EQUIL keyword, this can be achieved by the flag **-explicit 1**; the - only requirement is that the EQUIL keyword needs to be in the main input DATA file and no via INCLUDE files. - -Smeaheia --------- - -By downloading the `Smeaheia simulation model `_ (dataset part Simulation models), -then: - -.. code-block:: bash - - # From the download folders - cd Simulation_Models/data - pycopm -c 5,4,1 -a min -m all -i Statoil_Feasibility_sim_model_with_depletion_KROSS_INJ_SECTOR_20.DATA -o . - -will generate a coarser model five times in the x direction and four in the y direction, where the coarse cell is -made inactive if at least one cell is inactive (**-a min**). - -We use our `plopm `_ friend to generate PNG figures: - -.. code-block:: bash - - plopm -i 'STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PREP_PYCOPM_DRYRUN STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PYCOPM' -s ,,1 -v poro -subfigs 1,2 -save smeaheia -t 'Smeaheia Coarsened Smeaheia' -delax 1 -xunits km -xformat .0f -yunits km -yformat .0f -d 5,4.5 -suptitle 0 -c cet_rainbow_bgyrm_35_85_c69 -cbsfax 0.2,0.95,0.6,0.02 -cformat .2f - -.. figure:: figs/smeaheia.png - - Top view of porosity values for the (left) original and (right) coarsened model (figures using plopm, see/run `docs_via_deck_smeaheia.sh `_). - -.. _drogon: - -Drogon ------- - -.. note:: - In the current implementation of the **pycopm** tool, the handling of properties that requires definitions of i,j,k indices - (e.g., FAULTS, WELLSPECS) are assumed to be defined in the main .DATA deck. Then, in order to use **pycopm** for simulation models - where these properties are define via include files, replace those includes in the .DATA deck with the actual content of the include files. - Here are some relevant keywords per deck section that need to be in the main input deck and not via include files: - - SECTION GRID: MAPAXES, FAULTS, MULTREGT (other keywords like MULTZ, NTG, or definitions/operations for perms and poro can be in included files since - permx, permy, permz, poro, porv, multx, multy, multz are read from the .INIT file) - - SECTION PROPS: EQUALS, COPY, ADD, and MULTIPLY since this involve i,j,k indices and are applied to properties such as saturation functions parameters that - are still given in the same input format in the generated deck. In addition, SWATINIT if used in the deck, is read from the .INIT file and output for the - modified deck in a new file, then one might need to give the right include path to this special case. - - SECTION SCHEDULE: All keywords in this section must be in the input deck and no via include viles. - -Following the note above, then by downloading the `DROGON model `_, adding the `MAPAXES `_ -to the deck, replacing the lines in `DROGON_HIST.DATA `_ for the FAULTS (L127-128) and SCHEDULE (L242-243) with the actual content of those include files, then by executing: - -.. code-block:: bash - - pycopm -i DROGON_HIST.DATA -c 1,1,3 -p 1 -q 1 -l C1 - pycopm -i DROGON_HIST_PYCOPM.DATA -c 1,3,1 -p 1 -q 1 -j 2.5 -l C2 -m all - -this would generate the following coarsened model: - -.. figure:: figs/drogon_generic.png -.. figure:: figs/drogon_generic_plopm.png - - Note that the total pore volume is conserved for the coarsened model (right). The properties of the standard model (left) can be visualized using the DROGON_HIST_PREP_PYCOPM_DRYRUN generated files (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_deck_drogon.sh `_). - -Here, we first coarse in the z direction, which reduces the number of cells from 31 to 11, and after we coarse in the y direction. -After trial and error, the jump (**-j**) is set to 2.5 to avoid generated connections across the faults. For geological models with a lot of -inactive cells and faults, this divide and conquer apporach is recommended, i.e., coarsening first in the z direction and after coarsening -in the x and y directions. Also, we add labels (**-l**) C1 and C2 to differentiate between the coarse include files. In addition, we use the -flags **-p 1 -q 1** to add the remove pore volume to the closest coarser cells and to redistribute the pore volume in the locations with -gas and oil, this results in the coarsened model having the same total pore volume, field gas in place, and practically same oil and water in -place as the input model. - -.. note:: - Add to the generated deck the removed include files in the grid section related to the region operations (e.g., - ../include/grid/drogon.multregt for this case). - -Now, we also show a two times coarsened model in all directions (referring to the previous comment about divide and conquer, for the Drogon model -it seems still ok to do a two times coarsening in one go): - -.. code-block:: bash - - pycopm -i DROGON_HIST.DATA -c 2,2,2 -p 1 -q 1 -j 4 -w DROGON_2TIMES_COARSER -m all - -Here, we use the **-w** flag to give a specific name to the generated coarsened deck, as well as using a higher value of **-j** to avoid generated connections across the faults. - -.. tip:: - To use a different approach from the default ones (see the :doc:`theory <./theory>`) to coarse one of the properties (e.g., permeabilities), this can - be achieve by the **-s** flag, e.g., **-s pvmean** to coarse the permeabilities using a pv-weighted mean. In addition, one could add a different label - **-l pvweightedperms** to identify the generated .INC files with the permeabilities, and rename these files in order to be used in the coarserned model with the rest - of the properties using the default aproaches or a combination of them (e.g., **-s max -l maxpermz** and keep the maximum values of permz). - -If we run these three models using OPM Flow: - -.. code-block:: bash - - flow DROGON_HIST.DATA - flow DROGON_HIST_PYCOPM_PYCOPM - flow DROGON_2TIMES_COARSER - -then we can compare the summary vectors. To this end, we use our good old friend `plopm `_: - -.. code-block:: bash - - plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v 'FOIP,FOPR,TCPU' -tunits y -f 14 -subfigs 2,2 -delax 1 -loc empty,empty,empty,center -d 10,5 -xformat '.1f' -xlnum 6 -ylabel 'sm$^3$ sm$^3$/day seconds' -t 'Field oil in place Field oil production rate Simulation time' -labels 'DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER' -save drogon_pycopm_comparison -yformat '.2e,.0f,.0f' - -.. figure:: figs/drogon_pycopm_comparison.png - - Note that the coarsened models have the same initial field oil in place as the input model. It seems the coarsened properties (e.g., permeabilities) - are good initial inputs to use in a history matching framework (e.g., to history match saturation function parameters), and the lower simulation - time for the coarsened models allow for more ensemble members and more iterations (figures using plopm, see/run `docs_via_deck_drogon.sh `_). - -We can also make a nice GIF by executing: - -.. code-block:: bash - - plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v sgas -subfigs 1,3 -d 15,11 -cnum 5 -m gif -xlnum 4 -ylnum 4 -dpi 300 -t "DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER" -f 16 -interval 2000 -loop 1 -cformat .2f -cbsfax 0.15,0.93,0.7,0.02 -s ,,1 -rotate '-30' -xunits km -yunits km -xformat .0f -yformat .0f -c cet_rainbow_bgyrm_35_85_c69 -delax 1 -tunits tstep - -.. figure:: figs/sgas.gif - - Top view of the Drogon and the two coarsened models (figures using plopm, see/run `docs_via_deck_drogon.sh `_). - -Norne ------ -By downloading the `Norne model `_ (and replacing the needed include files as described in the previous -example, specially the include file `./INCLUDE/BC0407_HIST01122006.SCH `_ at the end of `NORNE_ATW2013.DATA `_ to run the example without errors), -then here we create a coarsened model by removing certain pilars in order to keep the main features of the geological model: - -.. code-block:: bash - - pycopm -i NORNE_ATW2013.DATA -s pvmean -x 0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0 -y 0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0 -z 0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0 -a min -p 1 -q 1 -m all - -this would generate the following coarsened model: - -.. figure:: figs/norne_vec.png -.. figure:: figs/norne_plopm.png - - Top view of Norne for the (top) pore volume and (bottom) gas saturation (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_deck_norne.sh `_). - -.. _spe10: - -SPE10 ------ - -By downloading the `SPE10_MODEL2 model `_, then: - -.. code-block:: bash - - pycopm -i SPE10_MODEL2.DATA -s pvmean -c 4,8,2 -m all - -generates a coarsened model from ca. 1 million cells to ca. 20 thousands cells. - -.. figure:: figs/spe10_model2_coarser.png +SPE10 also shows the submodel functionality. - Porosity values for the (left) original and (right) coarsened SPE10 model. +.. grid:: 1 1 2 2 + :gutter: 3 -To generate a submodel from the coarsened model around the injector 'INJ', this can be achieved by executing: + .. grid-item-card:: Legacy configuration workflows + :link: example-legacy-configurations + :link-type: ref + :img-top: figs/drogon_coarser.png -.. code-block:: bash + Existing Norne and Drogon TOML and ERT studies. - pycopm -i SPE10_MODEL2_PYCOPM.DATA -p 1 -v 'INJ diamondxy 5' -m all -w vicinity -l sub -m all + .. grid-item-card:: Smeaheia + :link: example-smeaheia + :link-type: ref + :img-top: figs/smeaheia.png -.. figure:: figs/vicinity.png -.. figure:: figs/spe10_plopm.png + Coarsen the Smeaheia simulation model. - Pore volume values for the coarsened and vicinity around the well INJ in the SPE10 model (top figures using ResInsight and bottom figures using plopm (running pycopm with the flag -p 0 instead of -p 1), see/run `docs_via_deck_spe10.sh `_). + .. grid-item-card:: Drogon + :link: drogon + :link-type: ref + :img-top: figs/sgas.gif + Prepare, coarsen, conserve fluids in place, compare, and animate Drogon. -Dual coarsening ---------------- + .. grid-item-card:: Norne + :link: example-norne + :link-type: ref + :img-top: figs/norne_plopm.png -The flag **-dual** allows to perform a coarsening by differentiating between net and non-net cells, resulting in -two coarsened grids. For example, using the `MODEL6.DATA `_: + Preserve the Norne geometry with directional coarsening. -.. code-block:: bash + .. grid-item-card:: SPE10 + :link: spe10 + :link-type: ref + :img-top: figs/spe10_plopm.png - pycopm -i MODEL6.DATA -z 1:4 -w STANDARD -l S -t 2 -a max - pycopm -i MODEL6.DATA -z 1:4 -w DUAL -dual 'poro <= 0.1' -l D -t 2 -a max - flow MODEL6.DATA - flow STANDARD.DATA - flow DUAL.DATA - plopm -i 'MODEL6 STANDARD DUAL' -v 'pressure - 0pressure' -subfigs 1,3 -delax 1 -cbsfax 0.1,0.95,0.8,0.02 -d 12,4 -suptitle 0 -z 0 -clabel 'Pressure increase end of simulation [bar]' -grid 'black,1e-2' + Coarsen SPE10 Model 2 and extract a well-centred submodel. -This results in the following figure, where the pressure on the most right cell compares better using the dual coarsening than the standard: + .. grid-item-card:: Dual coarsening + :link: example-dual-coarsening + :link-type: ref + :img-top: figs/dual_pressure-0pressure_i,1,k_t2.png -.. figure:: figs/dual_pressure-0pressure_i,1,k_t2.png + Compare standard and net/non-net-aware coarsening. - Figures using plopm, see/run `docs_via_deck_dual_coarsening.sh `_. + .. grid-item-card:: Graphical abstract + :link: example-graphical-abstract + :link-type: ref + :img-top: figs/pycopm.png -================== -Graphical abstract -================== + Reproduce the coarsening, submodel, refinement, and transformation illustrations. -Here we describe how to generate the geological model ilustrations in the `graphical abstract `_. -These five ilustrations are generated from the `DROGON_HIST.DATA `_ model, and the -visualization is achieve using `ResInsight `_. +The complete former Hello World example is now the :doc:`tutorial/hello-world`. -* Top figure: By running the `DROGON_HIST.DATA `_ using opm flow and visaluazing the static property pore volume. -* Coarsenings: This corresponds to the generated DROGON_HIST_PYCOPM_PYCOPM.DATA deck in :ref:`drogon`. -* Submodels: pycopm -i DROGON_HIST.DATA -v 'xypolygon [463739,5931508] [464872,5932123] [464401,5932862] [463284,5932209] [463739,5931508]' -w SUBMODELS -m all -* Refinements: pycopm -i SUBMODELS.DATA -g 2,2,2 -w REFINEMENTS -m all -* Transformations: pycopm -i DROGON_HIST.DATA -d 'rotatexy 45' -w TRANSFORMATIONS -m all +.. toctree:: + :hidden: + :maxdepth: 1 -Note that for ResInsight to show the wells, one needs to run the decks (e.g., flow REFINEMENTS.DATA). + examples/configuration-workflows + examples/smeaheia + examples/drogon + examples/norne + examples/spe10 + examples/dual-coarsening + examples/graphical-abstract diff --git a/docs/text/examples/configuration-workflows.rst b/docs/text/examples/configuration-workflows.rst new file mode 100644 index 0000000..64b49cb --- /dev/null +++ b/docs/text/examples/configuration-workflows.rst @@ -0,0 +1,35 @@ +.. _example-legacy-configurations: + +Legacy configuration workflows +============================== + +.. warning:: + + This page describes the retained, model-specific Norne and Drogon TOML workflow. It is not the recommended starting point for new pycopm applications. + +The `examples `_ folder contains configuration files +to perform HM studies in drogon and norne using `ERT `_. For example, by executing inside the `example folder for drogon `_: + +.. code-block:: bash + + # From inside the main pycopm folder + cd examples/configurations/drogon + pycopm -i input.toml -o drogon_coarser + +The following are the drogon model from `opm-tests `_ and coarsened model generated using **pycopm** using ResInsight for the visualization: + +.. figure:: ../figs/drogon_coarser.png +.. figure:: ../figs/drogon_coarser_plopm.png + + Initial oil saturation from the input (left) and coarsened (right) models (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_config_drogon.sh `_). + +.. note:: + + For Drogon, a scored is printed after the run to compared the error to the results available at + https://webviz-subsurface-example.azurewebsites.net/history-match. While `input.toml `_ + only runs one HM iteration with two ensemble members that is used in testing **pycopm**, `hm.toml `_ runs a history matching + with a better score (i.e., less error compare to the observation data). This configuration file is also an example of how to use mpi to run Flow built from source + (set the flow path to your flow location; if you do not have mpi, you can remove it and still run the example). + + +.. _generic: diff --git a/docs/text/examples/drogon.rst b/docs/text/examples/drogon.rst new file mode 100644 index 0000000..86d872c --- /dev/null +++ b/docs/text/examples/drogon.rst @@ -0,0 +1,91 @@ +.. _drogon: + +Drogon +------ + +.. note:: + In the current implementation of the **pycopm** tool, the handling of properties that requires definitions of i,j,k indices + (e.g., FAULTS, WELLSPECS) are assumed to be defined in the main .DATA deck. Then, in order to use **pycopm** for simulation models + where these properties are define via include files, replace those includes in the .DATA deck with the actual content of the include files. + Here are some relevant keywords per deck section that need to be in the main input deck and not via include files: + + SECTION GRID: MAPAXES, FAULTS, MULTREGT (other keywords like MULTZ, NTG, or definitions/operations for perms and poro can be in included files since + permx, permy, permz, poro, porv, multx, multy, multz are read from the .INIT file) + + SECTION PROPS: EQUALS, COPY, ADD, and MULTIPLY since this involve i,j,k indices and are applied to properties such as saturation functions parameters that + are still given in the same input format in the generated deck. In addition, SWATINIT if used in the deck, is read from the .INIT file and output for the + modified deck in a new file, then one might need to give the right include path to this special case. + + SECTION SCHEDULE: All keywords in this section must be in the input deck and no via include viles. + +Following the note above, then by downloading the `DROGON model `_, adding the `MAPAXES `_ +to the deck, replacing the lines in `DROGON_HIST.DATA `_ for the FAULTS (L127-128) and SCHEDULE (L242-243) with the actual content of those include files, then by executing: + +.. code-block:: bash + + pycopm -i DROGON_HIST.DATA -c 1,1,3 -p 1 -q 1 -l C1 + pycopm -i DROGON_HIST_PYCOPM.DATA -c 1,3,1 -p 1 -q 1 -j 2.5 -l C2 -m all + +this would generate the following coarsened model: + +.. figure:: ../figs/drogon_generic.png +.. figure:: ../figs/drogon_generic_plopm.png + + Note that the total pore volume is conserved for the coarsened model (right). The properties of the standard model (left) can be visualized using the DROGON_HIST_PREP_PYCOPM_DRYRUN generated files (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_deck_drogon.sh `_). + +Here, we first coarse in the z direction, which reduces the number of cells from 31 to 11, and after we coarse in the y direction. +After trial and error, the jump (**-j**) is set to 2.5 to avoid generated connections across the faults. For geological models with a lot of +inactive cells and faults, this divide and conquer apporach is recommended, i.e., coarsening first in the z direction and after coarsening +in the x and y directions. Also, we add labels (**-l**) C1 and C2 to differentiate between the coarse include files. In addition, we use the +flags **-p 1 -q 1** to add the remove pore volume to the closest coarser cells and to redistribute the pore volume in the locations with +gas and oil, this results in the coarsened model having the same total pore volume, field gas in place, and practically same oil and water in +place as the input model. + +.. note:: + Add to the generated deck the removed include files in the grid section related to the region operations (e.g., + ../include/grid/drogon.multregt for this case). + +Now, we also show a two times coarsened model in all directions (referring to the previous comment about divide and conquer, for the Drogon model +it seems still ok to do a two times coarsening in one go): + +.. code-block:: bash + + pycopm -i DROGON_HIST.DATA -c 2,2,2 -p 1 -q 1 -j 4 -w DROGON_2TIMES_COARSER -m all + +Here, we use the **-w** flag to give a specific name to the generated coarsened deck, as well as using a higher value of **-j** to avoid generated connections across the faults. + +.. tip:: + To use a different approach from the default ones (see the :doc:`theory <../theory>`) to coarse one of the properties (e.g., permeabilities), this can + be achieve by the **-s** flag, e.g., **-s pvmean** to coarse the permeabilities using a pv-weighted mean. In addition, one could add a different label + **-l pvweightedperms** to identify the generated .INC files with the permeabilities, and rename these files in order to be used in the coarserned model with the rest + of the properties using the default aproaches or a combination of them (e.g., **-s max -l maxpermz** and keep the maximum values of permz). + +If we run these three models using OPM Flow: + +.. code-block:: bash + + flow DROGON_HIST.DATA + flow DROGON_HIST_PYCOPM_PYCOPM + flow DROGON_2TIMES_COARSER + +then we can compare the summary vectors. To this end, we use our good old friend `plopm `_: + +.. code-block:: bash + + plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v 'FOIP,FOPR,TCPU' -tu y -fz 14 -sg 2,2 -rdl 1 -ll empty,empty,empty,center -fs 10,5 -xf '.1f' -xnt 6 -yl 'sm$^3$ sm$^3$/day seconds' -t 'Field oil in place Field oil production rate Simulation time' -llb 'DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER' -fn drogon_pycopm_comparison -yf '.2e,.0f,.0f' + +.. figure:: ../figs/drogon_pycopm_comparison.png + + Note that the coarsened models have the same initial field oil in place as the input model. It seems the coarsened properties (e.g., permeabilities) + are good initial inputs to use in a history matching framework (e.g., to history match saturation function parameters), and the lower simulation + time for the coarsened models allow for more ensemble members and more iterations (figures using plopm, see/run `docs_via_deck_drogon.sh `_). + +We can also make a nice GIF by executing: + +.. code-block:: bash + + plopm -i 'DROGON_HIST DROGON_HIST_PYCOPM_PYCOPM DROGON_2TIMES_COARSER' -v sgas -sg 1,3 -fs 15,11 -cbn 5 -m gif -xnt 4 -ynt 4 -dpi 300 -t "DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER" -fz 16 -gi 2000 -gl 1 -cbf .2f -cbp 0.15,0.93,0.7,0.02 -s ,,1 -rot '-30' -xu km -yu km -xf .0f -yf .0f -c cet_rainbow_bgyrm_35_85_c69 -rdl 1 -tu tstep + +.. figure:: ../figs/sgas.gif + + Top view of the Drogon and the two coarsened models (figures using plopm, see/run `docs_via_deck_drogon.sh `_). diff --git a/docs/text/examples/dual-coarsening.rst b/docs/text/examples/dual-coarsening.rst new file mode 100644 index 0000000..e61cc2c --- /dev/null +++ b/docs/text/examples/dual-coarsening.rst @@ -0,0 +1,21 @@ +.. _example-dual-coarsening: + +Dual coarsening +=============== +The flag **-dual** allows to perform a coarsening by differentiating between net and non-net cells, resulting in +two coarsened grids. For example, using the `MODEL6.DATA `_: + +.. code-block:: bash + + pycopm -i MODEL6.DATA -z 1:4 -w STANDARD -l S -t 2 -a max + pycopm -i MODEL6.DATA -z 1:4 -w DUAL -dual 'poro <= 0.1' -l D -t 2 -a max + flow MODEL6.DATA + flow STANDARD.DATA + flow DUAL.DATA + plopm -i 'MODEL6 STANDARD DUAL' -v 'pressure - 0pressure' -sg 1,3 -rdl 1 -cbp 0.1,0.95,0.8,0.02 -fs 12,4 -st 0 -asp 0 -cbl 'Pressure increase end of simulation [bar]' -ge 'black,1e-2' + +This results in the following figure, where the pressure on the most right cell compares better using the dual coarsening than the standard: + +.. figure:: ../figs/dual_pressure-0pressure_i,1,k_t2.png + + Figures using plopm, see/run `docs_via_deck_dual_coarsening.sh `_. diff --git a/docs/text/examples/graphical-abstract.rst b/docs/text/examples/graphical-abstract.rst new file mode 100644 index 0000000..c26a00d --- /dev/null +++ b/docs/text/examples/graphical-abstract.rst @@ -0,0 +1,18 @@ +.. _example-graphical-abstract: + +Graphical abstract +================== + +.. figure:: ../figs/pycopm.png + +Here we describe how to generate the geological model ilustrations in the `graphical abstract `_. +These five ilustrations are generated from the `DROGON_HIST.DATA `_ model, and the +visualization is achieve using `ResInsight `_. + +* Top figure: By running the `DROGON_HIST.DATA `_ using opm flow and visaluazing the static property pore volume. +* Coarsenings: This corresponds to the generated DROGON_HIST_PYCOPM_PYCOPM.DATA deck in :ref:`drogon`. +* Submodels: pycopm -i DROGON_HIST.DATA -v 'xypolygon [463739,5931508] [464872,5932123] [464401,5932862] [463284,5932209] [463739,5931508]' -w SUBMODELS -m all +* Refinements: pycopm -i SUBMODELS.DATA -g 2,2,2 -w REFINEMENTS -m all +* Transformations: pycopm -i DROGON_HIST.DATA -d 'rotatexy 45' -w TRANSFORMATIONS -m all + +Note that for ResInsight to show the wells, one needs to run the decks (e.g., flow REFINEMENTS.DATA). diff --git a/docs/text/examples/norne.rst b/docs/text/examples/norne.rst new file mode 100644 index 0000000..46d97ab --- /dev/null +++ b/docs/text/examples/norne.rst @@ -0,0 +1,18 @@ +.. _example-norne: + +Norne +===== +By downloading the `Norne model `_ (and replacing the needed include files as described in the previous +example, specially the include file `./INCLUDE/BC0407_HIST01122006.SCH `_ at the end of `NORNE_ATW2013.DATA `_ to run the example without errors), +then here we create a coarsened model by removing certain pilars in order to keep the main features of the geological model: + +.. code-block:: bash + + pycopm -i NORNE_ATW2013.DATA -s pvmean -x 0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0 -y 0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0 -z 0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0 -a min -p 1 -q 1 -m all + +this would generate the following coarsened model: + +.. figure:: ../figs/norne_vec.png +.. figure:: ../figs/norne_plopm.png + + Top view of Norne for the (top) pore volume and (bottom) gas saturation (top figures using ResInsight and bottom figures using plopm, see/run `docs_via_deck_norne.sh `_). diff --git a/docs/text/examples/smeaheia.rst b/docs/text/examples/smeaheia.rst new file mode 100644 index 0000000..4d850c0 --- /dev/null +++ b/docs/text/examples/smeaheia.rst @@ -0,0 +1,25 @@ +.. _example-smeaheia: + +Smeaheia +======== +By downloading the `Smeaheia simulation model `_ (dataset part Simulation models), +then: + +.. code-block:: bash + + # From the download folders + cd Simulation_Models/data + pycopm -c 5,4,1 -a min -m all -i Statoil_Feasibility_sim_model_with_depletion_KROSS_INJ_SECTOR_20.DATA -o . + +will generate a coarser model five times in the x direction and four in the y direction, where the coarse cell is +made inactive if at least one cell is inactive (**-a min**). + +We use our `plopm `_ friend to generate PNG figures: + +.. code-block:: bash + + plopm -i 'STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PREP_PYCOPM_DRYRUN STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PYCOPM' -s ,,1 -v poro -sg 1,2 -fn smeaheia -t 'Smeaheia Coarsened Smeaheia' -rdl 1 -xu km -xf .0f -yu km -yf .0f -fs 5,4.5 -st 0 -c cet_rainbow_bgyrm_35_85_c69 -cbp 0.2,0.95,0.6,0.02 -cbf .2f + +.. figure:: ../figs/smeaheia.png + + Top view of porosity values for the (left) original and (right) coarsened model (figures using plopm, see/run `docs_via_deck_smeaheia.sh `_). diff --git a/docs/text/examples/spe10.rst b/docs/text/examples/spe10.rst new file mode 100644 index 0000000..0236ef4 --- /dev/null +++ b/docs/text/examples/spe10.rst @@ -0,0 +1,27 @@ +.. _spe10: + +SPE10 +----- + +By downloading the `SPE10_MODEL2 model `_, then: + +.. code-block:: bash + + pycopm -i SPE10_MODEL2.DATA -s pvmean -c 4,8,2 -m all + +generates a coarsened model from ca. 1 million cells to ca. 20 thousands cells. + +.. figure:: ../figs/spe10_model2_coarser.png + + Porosity values for the (left) original and (right) coarsened SPE10 model. + +To generate a submodel from the coarsened model around the injector 'INJ', this can be achieved by executing: + +.. code-block:: bash + + pycopm -i SPE10_MODEL2_PYCOPM.DATA -p 1 -v 'INJ diamondxy 5' -m all -w vicinity -l sub -m all + +.. figure:: ../figs/vicinity.png +.. figure:: ../figs/spe10_plopm.png + + Pore volume values for the coarsened and vicinity around the well INJ in the SPE10 model (top figures using ResInsight and bottom figures using plopm (running pycopm with the flag -p 0 instead of -p 1), see/run `docs_via_deck_spe10.sh `_). diff --git a/docs/text/index.rst b/docs/text/index.rst index 4b046b9..8dc800b 100644 --- a/docs/text/index.rst +++ b/docs/text/index.rst @@ -1,25 +1,142 @@ -.. pycopm documentation master file +.. _home: -Welcome to pycopm's documentation! -================================== +pycopm +====== + +.. rst-class:: lead + + Tailor OPM Flow geological models by coarsening, refining, extracting submodels, and transforming grids. + +**pycopm** creates modified, simulation-ready OPM Flow models from existing +``.DATA`` decks. Generic deck workflows are the active development path. +Legacy TOML workflows remain available for existing Norne and Drogon +coarsening and ERT studies. + +.. grid:: 1 2 2 4 + :gutter: 3 + :margin: 4 0 4 0 + + .. grid-item-card:: :octicon:`rocket;1.2em` Get started + :link: introduction + :link-type: doc + + Understand the deck-first workflow and choose a model operation. + + .. grid-item-card:: :octicon:`download;1.2em` Install + :link: installation + :link-type: doc + + Install pycopm, OPM Flow, and visualization tools. + + .. grid-item-card:: :octicon:`book;1.2em` Follow the tutorial + :link: tutorial + :link-type: doc + + Work through the complete Hello World deck workflow. + + .. grid-item-card:: :octicon:`terminal;1.2em` CLI reference + :link: command-line + :link-type: doc + + Look up options, defaults, formats, and compatibility rules. + +Quick installation +------------------ + +Install the current development version: + +.. code-block:: console + + pip install git+https://github.com/cssr-tools/pycopm.git + +See :doc:`installation` for supported Python versions, OPM Flow, visualization +tools, virtual environments, platform-specific dependencies, and installation +from source. + +Quick start +----------- + +Coarsen the bundled Hello World deck and run the complete workflow: + +.. code-block:: console + + pycopm -i examples/decks/HELLO_WORLD.DATA -c 5,5,1 -m all -o output + +Display the available command-line options: + +.. code-block:: console + + pycopm --help + +See :doc:`tutorial` for a guided workflow, :doc:`examples` for larger +applications, and :doc:`command-line` for exact syntax, accepted values, +defaults, and option compatibility. + +What can pycopm do? +------------------- + +.. grid:: 1 2 2 2 + :gutter: 3 + + .. grid-item-card:: Coarsen geological models + + Remove selected pillars and grid lines, aggregate continuous and + discrete properties, preserve pore volume, and optionally upscale + transmissibilities and non-neighbouring connections. + + .. grid-item-card:: Refine simulation grids + + Add cells globally or in selected x, y, and z intervals while mapping + properties, regions, wells, faults, and other indexed deck data to the + refined grid. + + .. grid-item-card:: Extract focused submodels + + Select cells by region values, xy polygons, or well-centred box, + diamond, and diamondxy neighbourhoods, with configurable boundary + pore-volume handling. + + .. grid-item-card:: Transform grid geometry + + Translate, scale, or rotate OPM Flow grids to reposition models, modify + dimensions, or align geological descriptions from different sources. + + .. grid-item-card:: Generate simulation-ready files + + Write modified OPM Flow decks and supporting include files, run + preparation and dry-run stages, and control names, precision, + initialization, and completion rewriting. + + .. grid-item-card:: Support advanced coarsening workflows + + Control active-cell aggregation, region and property upscaling, + fluid-in-place correction, fault-related jump handling, and dual + net/non-net coarsening. + + .. grid-item-card:: Integrate with other tools + + Use generated decks directly with OPM Flow or adapt them for ERT, PET, + visualization tools, history matching, optimization, proxy modelling, + and uncertainty studies. + + .. grid-item-card:: Maintain legacy integrated studies + + Continue existing Norne and Drogon coarsening and ERT workflows through + the retained TOML configuration interface while new development focuses + on generic deck input. .. toctree:: - :maxdepth: 4 + :hidden: + :maxdepth: 2 introduction - theory installation - configuration_file + tutorial examples + theory + command-line + related + configuration_file api output_folder contributing - related - about - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/text/installation.rst b/docs/text/installation.rst index 78aa6bb..c2de933 100644 --- a/docs/text/installation.rst +++ b/docs/text/installation.rst @@ -1,7 +1,7 @@ -============ +.. _installation: + Installation ============ - The following steps work installing the dependencies in Ubuntu via apt-get or in macOS using `brew `_ or `macports `_. While using package managers such as Anaconda, Miniforge, or Mamba might work, these are not tested. The supported Python versions are 3.12 to 3.14. @@ -132,3 +132,19 @@ You can check if the installation of OPM Flow succeded by typing in the terminal Source build in macOS +++++++++++++++++++++ See `this repository `_ dedicated to build OPM Flow from source in macOS 26 (GitHub actions), and tested with **pycopm** + + +Verify the installation +----------------------- + +.. code-block:: console + + pycopm --help + flow --help + +Next steps +---------- + +* Follow :doc:`tutorial/hello-world` for the recommended generic deck workflow. +* Use :doc:`command-line` for exact option definitions. +* Use :doc:`configuration_file` only for legacy Norne and Drogon integrated studies. diff --git a/docs/text/introduction.rst b/docs/text/introduction.rst index b01867f..1917826 100644 --- a/docs/text/introduction.rst +++ b/docs/text/introduction.rst @@ -1,64 +1,130 @@ -============ +.. image:: figs/pycopm.png + :alt: Geological-model operations supported by pycopm + :align: center + +.. _introduction: + Introduction ============ -.. image:: ./figs/pycopm.png - -This documentation describes the **pycopm** tool hosted in `https://github.com/cssr-tools/pycopm `_. +**pycopm** is an open-source tool for creating tailored OPM Flow geological +models from existing simulation decks. It can coarsen and refine grids, +extract submodels, and apply geometric transformations while generating the +files needed to run the modified model with OPM Flow. Concept ------- -User-friendly creation of OPM Flow geological models from provided input decks with options for grid refinement, grid coarsening, submodels, and transformations including scalings, rotations, and translations. -Roadmap -------- -In the initial development of the pycopm repository, the focus were two available models in `opm-tests `_: `norne `_ -and `drogon `_, where the coarsened models were used to perform history matching studies using -the Ensemble based reservoir tool `ERT `_, via a :doc:`configuration file <./configuration_file>`. +The main goal is the user-friendly creation of OPM Flow geological models from +provided input decks. The generic deck workflow supports grid refinement, grid +coarsening, submodel extraction, and transformations including scaling, +rotation, and translation. + +The generated decks can be used directly with OPM Flow or adapted for a +history-matching or optimization framework such as `ERT +`_ or `PET +`_. This supports applications +such as proxy modelling, non-conforming grids, upscaling studies, model +comparison, and uncertainty analysis. + +Recommended workflow +-------------------- + +Current pycopm development focuses on the **generic deck-based workflow**. +Provide an OPM Flow ``.DATA`` deck and select the required model operation: + +.. code-block:: console + + pycopm -i MODEL.DATA [OPTIONS] + +For example, coarsen the bundled Hello World model and run all processing +stages: + +.. code-block:: console + + pycopm -i examples/decks/HELLO_WORLD.DATA -c 5,5,1 -m all -o output -The current development of **pycopm** focuses on creating models (i.e., all needed input files to run OPM Flow such as the grid) by only providing the OPM Flow input files -(i.e., avoiding the manual work to create templates as it was done for drogon and norne). This allows for flexibility to adapt the generated decks in your -favourite history matching/optimization tool (e.g., `ERT `_, `PET `_), as well as to -test different approaches in diverse applications (e.g., proxy models, non-conforming grids, upscaling techniques). +Use :doc:`command-line` for the complete option descriptions, accepted values, +defaults, examples, and compatibility rules. Follow :doc:`tutorial` for a +guided workflow. -.. _overview: +.. warning:: -Overview + TOML configuration files are a legacy, model-specific workflow retained for + existing integrated Norne and Drogon coarsening and ERT studies. New pycopm + functionality is developed for generic OPM Flow deck input. Use + :doc:`configuration_file` only when maintaining one of those legacy studies. + +Core workflows +-------------- + +* **Grid coarsening:** Remove selected pillars and grid lines, aggregate model + properties, preserve pore volume, and optionally upscale + transmissibilities. +* **Grid refinement:** Add pillars and grid lines globally or in selected + intervals while mapping grid-dependent properties and deck keywords. +* **Submodel extraction:** Select a region by property values, an xy polygon, + or a well-centred box, diamond, or xy-diamond neighbourhood. +* **Geometric transformations:** Translate, scale, or rotate grid coordinates. +* **Simulation-ready output:** Write a modified OPM Flow deck and supporting + include files for direct simulation or use in another workflow. +* **Legacy integrated studies:** Generate coarsened Norne or Drogon projects + and optionally run OPM Flow or ERT from a TOML configuration. + +Project evolution +----------------- + +The initial development of **pycopm** focused on two public models from +`opm-tests `_: `Norne +`_ and `Drogon +`_. Coarsened versions of +these models were used in history-matching studies with ERT through a +:doc:`configuration file `. + +The current generic workflow removes the need to prepare model-specific +templates. It creates modified models directly from OPM Flow input decks, +which makes pycopm applicable to a broader range of geological models and +workflows. + +About the project +----------------- + +**pycopm** is developed within the `Center for Sustainable Subsurface +Resources `_ (CSSR, project number 331841) and has also been +funded by `Expansion of Resources for CO2 Storage on the Horda Platform +`_ +(ExpReCCS, project number 336294). + +The source code, issue tracker, and development history are available in the +`pycopm GitHub repository `_. +Contributions are welcome through the fork and pull-request workflow. Request +new features or report problems through the repository issue tracker. + +Citation -------- -The current implementation supports the following executable with the argument options: - -.. code-block:: bash - - pycopm -i name_of_input_file - -where - --i The base name of the :doc:`toml configuration file <./configuration_file>` or the name of the deck, e.g., 'DROGON.DATA', ('input.toml' by default). --o The base name of the :doc:`output folder <./output_folder>` ('.' by default, i.e., the folder where pycopm is executed). --f OPM Flow path to executable (e.g., /home/pycopm/build/opm-simulators/bin/flow), or just 'flow' if this was installed via binaries or if the folder where flow is built has been added to your path ('flow' by default). --m Execute a dry run of the input deck to generate the static properties ('prep'), generate only the modified files ('deck'), only exectute a dry run on the generated model ('dry'), 'prep_deck', 'deck_dry', or do all ('all') ('prep_deck' by default). --v The location to extract the sub model which can be assigned by region values, e.g., 'fipnum 2,4' extracts the cells with fipnums equal to 2 or 4, by a polygon given the xy locations in meters, e.g., 'xypolygon [0,0] [30,0] [30,30] [0,0]', or by the name of the well and three different options for the neighbourhood: box, diamond, and diamondxy, where for box the i, j, and k interval around the connections are given, e.g., 'welln box [-1,1] [-2,2] [0,3]' results in a vicinity with 1 pm cell in the x direction, 2 pm cells in the y direction and only 3 cells in the k positive direction, while the diamond considers only the given number of cells around the well connections (e.g., 'welln diamond 2') and diamondxy it is restricted to the xy plane ('' by default). --c Level of coarsening in the x, y, and z dir (',' by default; either use this flag or the -x, -y, and -z ones). --x Array of x-coarsening, e.g., if the grid has 6 cells in the x direction, then '0,2,0,2,0,2,0' would generate a coarsened model with 3 cells, while '0,2,2,2,2,2,0' would generate a coarser model with 1 cell, i.e., 0 keeps the pilars while 2 removes them. As an alternative, the range of the cells to coarse can be given separate them by commas, e.g., '1:3,5:6' generates a coarsened model with 3 cells where the cells with the first three and two last i indices are coarsened to one ('' by default), --y Array of y-coarsening, see the description for -x ('' by default). --z Array of z-coarsening, see the description for -x ('' by default). --g Level of grid refinement in the x, y, and z dir ('' by default). --rx Array of x-refinement, e.g., if the grid has 6 cells in the x direction, then '0,1,0,2,0,4' would generate a refined model with 13 cells, while 0,0,0,1,0,0 would generate a refined model with 7 cells ('' by default). --ry Array of y-refinement, see the description for -rx ('' by default). --rz Array of z-refinement, see the description for -rx ('' by default). --a In coarsening, use 'min', 'max', or 'mode' to scale the actnum, e.g., 'min' makes the new coarser cell inactive if at least one cell is inactive, while 'max' makes it active it at least one cell is active ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:10,11:15 -a min,mode'. --n In coarsening, use 'min', 'max', or 'mode' to scale endnum, eqlnum, fipnum, fluxnum, imbnum, miscnum, multnum, opernum, pvtnum, rocknum, and satnum ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:3,4 -a max,min'. --s In coarsening, use 'min', 'max', 'mean', or 'pvmean' to scale permx, permy, permz, poro, swatinit, disperc, thconr, and all mult(-)xyz ('' by default, i.e., using the arithmetic average for permx/permy, harmonic average for permz, volume weighted mean for mult(-)xyz, and the pore volume weighted mean ('pvmean') for the rest). . For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:40,41:43,44:50 -a pvmean,max,pvmean'. --p In coarsening, set to '1' to add the removed pore volume to the closest coarser cells, while in submodels '1' adds the porv from outside on the boundary of the submodel, '2' adds the corner regions (e.g., below the mini and minj from the input model) to the corners in the submodel, '3' distributes the porv uniformly along the boundary, and '4' distributes it on the whole submodel ('0' by default, i.e., no porv correction). --q Adjust the pv to the initial FGIP and FOIP from the input deck; use this option only for systems with initial oil, gas, and water, e.g., norne or drogon, but no in Smeaheia ('0' by default, '1' to enable). --t In coarsening, write and use upscaled transmissibilities by ('1') armonic averaging and summing the transmissibilities in the corresponding coarsening direction and ('2') scaling the face transmissibily on the coarse faces ('0' by default, i.e., transmissibilities are not used). --r Remove CONFACT and KH from COMPDAT ('1') and also remove PEQVR ('2') (ITEM 13, the last entry) to compute the well transmisibility connections internally in OPM Flow using the grid properties ('2' by default; set to '0' to not remove). --j In coarsening, tuning parameter to avoid creation of neighbouring connections in the coarsened model where there are discontinuities between cells along the z direction, e.g., around faults ('' by default, i.e., nothing corrected; if need it, try with values of the order of 1). --w Name of the generated deck ('' by default, i.e., the name of the input deck plus _PYCOPM.DATA). --l Added text before each generated .INC ('PYCOPM\_' by default, i.e., the modified porv is saved in PYCOPM_PORV.INC; set to '' to generate PORV.INC, PERMX.INC, etc). --e Use 'utf8' or 'ISO-8859-1' encoding to read the deck ('ISO-8859-1' by default). --ijk Given i,j,k indices in the input model, return the modified i,j,k corresponding positions ('' by default; if not empty, e.g., '1,2,3', then there will not be generation of modified files, only the i,j,k mapped indices in the terminal). --d Options to transform the x,y,z coordinates: 'translate [10,-5,4]' adds the values in meters to the coordinates, 'scale [1,2,3]' multiplies the coordinates by the given values respectively, and 'rotatexy 45' applies a rotation in degrees in the xy plane (rotatexz and rotateyz applies a rotation around the y and x axis respectively) ('' by default). --explicit Set to 1 to explicitly write the cell values in the SOLUTION section in the deck ('0' by default). --dual Set the criterium to differentiate net and non-net in coarsening using a static variable, e.g., 'poro <= 0.1' ('' by default). --precision Set the number of significant digits used when writing floating-point values, or 0 to use machine precision + +If **pycopm** contributes to published work, cite the project paper: + + Landa-Marbán, D. (2026). *pycopm: An open-source tool to tailor OPM Flow + geological models*. arXiv:2602.11777. + https://doi.org/10.48550/arXiv.2602.11777. + +Where to continue +----------------- + +* Complete :doc:`installation` and verify both pycopm and OPM Flow. +* Follow the :doc:`Hello World tutorial ` to coarsen, + extract, refine, transform, run, and visualize a small model. +* Use :doc:`command-line` for exact flag syntax, accepted values, defaults, and + compatibility rules. +* Read :doc:`theory` for the complete coarsening, refinement, submodel, and + transformation methods. +* Browse :doc:`examples` for Smeaheia, Drogon, Norne, SPE10, dual coarsening, + and reproducible visualization commands. +* Review :doc:`output_folder` to understand generated decks, include files, + simulation results, and legacy ERT projects. +* Consult :doc:`configuration_file` for retained Norne and Drogon TOML studies. +* Browse :doc:`api` for the Python modules, classes, and functions. +* See :doc:`contributing` to contribute code, run checks, report issues, or seek + support. +* Explore :doc:`related` for complementary open-source subsurface tools. diff --git a/docs/text/make.bat b/docs/text/make.bat deleted file mode 100644 index 32bb245..0000000 --- a/docs/text/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/text/options/aggregation.rst b/docs/text/options/aggregation.rst new file mode 100644 index 0000000..516d53c --- /dev/null +++ b/docs/text/options/aggregation.rst @@ -0,0 +1,72 @@ +.. _options-aggregation: + +Property aggregation and conservation +===================================== + +Control active cells, discrete regions, continuous properties, pore +volume, fluids in place, transmissibilities, and fault-related jump handling. + +.. program:: pycopm + +-a/--active_cell_methods +---------------------------------- + +.. option:: -a , --active_cell_methods + :no-contents-entry: + :no-typesetting: + +In coarsening, use 'min', 'max', or 'mode' to scale the actnum, e.g., 'min' makes the new coarser cell inactive if at least one cell is inactive, while 'max' makes it active it at least one cell is active ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:10,11:15 -a min,mode'. + +-n/--discrete_aggregation_method +------------------------------------------ + +.. option:: -n , --discrete_aggregation_method + :no-contents-entry: + :no-typesetting: + +In coarsening, use 'min', 'max', or 'mode' to scale endnum, eqlnum, fipnum, fluxnum, imbnum, miscnum, multnum, opernum, pvtnum, rocknum, and satnum ('mode' by default). For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:3,4 -a max,min'. + +-s/--continuous_aggregation_method +-------------------------------------------- + +.. option:: -s , --continuous_aggregation_method + :no-contents-entry: + :no-typesetting: + +In coarsening, use 'min', 'max', 'mean', or 'pvmean' to scale permx, permy, permz, poro, swatinit, disperc, thconr, and all mult(-)xyz ('' by default, i.e., using the arithmetic average for permx/permy, harmonic average for permz, volume weighted mean for mult(-)xyz, and the pore volume weighted mean ('pvmean') for the rest). . For range coarsenings using -z, then one can specify this per layer, e.g., '-z 1:40,41:43,44:50 -a pvmean,max,pvmean'. + +-p/--pore_volume_correction <0|1|2|3|4> +--------------------------------------- + +.. option:: -p <0|1|2|3|4>, --pore_volume_correction <0|1|2|3|4> + :no-contents-entry: + :no-typesetting: + +In coarsening, set to '1' to add the removed pore volume to the closest coarser cells, while in submodels '1' adds the porv from outside on the boundary of the submodel, '2' adds the corner regions (e.g., below the mini and minj from the input model) to the corners in the submodel, '3' distributes the porv uniformly along the boundary, and '4' distributes it on the whole submodel ('0' by default, i.e., no porv correction). + +-q/--correct_fluid_in_place <0|1> +--------------------------------- + +.. option:: -q <0|1>, --correct_fluid_in_place <0|1> + :no-contents-entry: + :no-typesetting: + +Adjust the pv to the initial FGIP and FOIP from the input deck; use this option only for systems with initial oil, gas, and water, e.g., norne or drogon, but no in Smeaheia ('0' by default, '1' to enable). + +-t/--transmissibility_coarsening_method <0|1|2> +----------------------------------------------- + +.. option:: -t <0|1|2>, --transmissibility_coarsening_method <0|1|2> + :no-contents-entry: + :no-typesetting: + +In coarsening, write and use upscaled transmissibilities by ('1') armonic averaging and summing the transmissibilities in the corresponding coarsening direction and ('2') scaling the face transmissibily on the coarse faces ('0' by default, i.e., transmissibilities are not used). + +-j/--jump_thresholds +----------------------------- + +.. option:: -j , --jump_thresholds + :no-contents-entry: + :no-typesetting: + +In coarsening, tuning parameter to avoid creation of neighbouring connections in the coarsened model where there are discontinuities between cells along the z direction, e.g., around faults ('' by default, i.e., nothing corrected; if need it, try with values of the order of 1). diff --git a/docs/text/options/compatibility.rst b/docs/text/options/compatibility.rst new file mode 100644 index 0000000..5004c70 --- /dev/null +++ b/docs/text/options/compatibility.rst @@ -0,0 +1,22 @@ +.. _options-compatibility: + +Option compatibility and validation +=================================== + +pycopm validates input formats and rejects incompatible workflows before +processing the model. + +.. program:: pycopm + +The implementation validates the following combinations before processing a deck: + +* Use either uniform ``-c`` or directional ``-x``, ``-y``, and ``-z`` coarsening. +* Use either uniform ``-g`` or directional ``-rx``, ``-ry``, and ``-rz`` refinement. +* Coarsening and refinement are mutually exclusive. +* ``-v`` cannot be combined with ``-d``, refinement, or ``-q 1``. +* ``-d`` cannot be combined with refinement. +* Non-default ``-a`` and ``-n``, and any ``-s``, ``-t``, ``-j``, or ``-dual``, require coarsening. +* ``-p 1`` requires coarsening or submodel extraction. ``-p 2``, ``-p 3``, and ``-p 4`` require submodel extraction. +* Multiple aggregation methods require range-based ``-z`` coarsening and one method per z group. +* An ``xypolygon`` must have at least four points and repeat its first point at the end. +* Scale factors cannot be zero, jump thresholds must be positive, and requested ``i,j,k`` indices must be positive. diff --git a/docs/text/options/grid.rst b/docs/text/options/grid.rst new file mode 100644 index 0000000..0042170 --- /dev/null +++ b/docs/text/options/grid.rst @@ -0,0 +1,81 @@ +.. _options-grid: + +Grid coarsening and refinement +============================== + +Define uniform or directional coarsening and refinement. The detailed +array and range examples from the original introduction are retained below. + +.. program:: pycopm + +-c/--coarsening +----------------------- + +.. option:: -c , --coarsening + :no-contents-entry: + :no-typesetting: + +Level of coarsening in the x, y, and z dir (',' by default; either use this flag or the -x, -y, and -z ones). + +-x/--x_coarsening +-------------------------- + +.. option:: -x , --x_coarsening + :no-contents-entry: + :no-typesetting: + +Array of x-coarsening, e.g., if the grid has 6 cells in the x direction, then '0,2,0,2,0,2,0' would generate a coarsened model with 3 cells, while '0,2,2,2,2,2,0' would generate a coarser model with 1 cell, i.e., 0 keeps the pillars while 2 removes them. As an alternative, the range of the cells to coarse can be given separate them by commas, e.g., '1:3,5:6' generates a coarsened model with 3 cells where the cells with the first three and two last i indices are coarsened to one ('' by default), + +-y/--y_coarsening +-------------------------- + +.. option:: -y , --y_coarsening + :no-contents-entry: + :no-typesetting: + +Array of y-coarsening, see the description for -x ('' by default). + +-z/--z_coarsening +-------------------------- + +.. option:: -z , --z_coarsening + :no-contents-entry: + :no-typesetting: + +Array of z-coarsening, see the description for -x ('' by default). + +-g/--refinement +----------------------- + +.. option:: -g , --refinement + :no-contents-entry: + :no-typesetting: + +Level of grid refinement in the x, y, and z dir ('' by default). + +-rx/--x_refinement +--------------------------- + +.. option:: -rx , --x_refinement + :no-contents-entry: + :no-typesetting: + +Array of x-refinement, e.g., if the grid has 6 cells in the x direction, then '0,1,0,2,0,4' would generate a refined model with 13 cells, while 0,0,0,1,0,0 would generate a refined model with 7 cells ('' by default). + +-ry/--y_refinement +--------------------------- + +.. option:: -ry , --y_refinement + :no-contents-entry: + :no-typesetting: + +Array of y-refinement, see the description for -rx ('' by default). + +-rz/--z_refinement +--------------------------- + +.. option:: -rz , --z_refinement + :no-contents-entry: + :no-typesetting: + +Array of z-refinement, see the description for -rx ('' by default). diff --git a/docs/text/options/output.rst b/docs/text/options/output.rst new file mode 100644 index 0000000..990f05e --- /dev/null +++ b/docs/text/options/output.rst @@ -0,0 +1,72 @@ +.. _options-output: + +Deck rewriting and output +========================= + +Control completion rewriting, generated names, include prefixes, input +encoding, index mapping, explicit initialization, and numeric precision. + +.. program:: pycopm + +-r/--completion_removal_level <0|1|2> +------------------------------------- + +.. option:: -r <0|1|2>, --completion_removal_level <0|1|2> + :no-contents-entry: + :no-typesetting: + +Remove CONFACT and KH from COMPDAT ('1') and also remove PEQVR ('2') (ITEM 13, the last entry) to compute the well transmisibility connections internally in OPM Flow using the grid properties ('2' by default; set to '0' to not remove). + +-w/--output_deck_name +---------------------------- + +.. option:: -w , --output_deck_name + :no-contents-entry: + :no-typesetting: + +Name of the generated deck ('' by default, i.e., the name of the input deck plus _PYCOPM.DATA). + +-l/--include_prefix +---------------------------- + +.. option:: -l , --include_prefix + :no-contents-entry: + :no-typesetting: + +Added text before each generated .INC ('PYCOPM\_' by default, i.e., the modified porv is saved in PYCOPM_PORV.INC; set to '' to generate PORV.INC, PERMX.INC, etc). + +-e/--deck_encoding +----------------------------- + +.. option:: -e , --deck_encoding + :no-contents-entry: + :no-typesetting: + +Use 'utf8' or 'ISO-8859-1' encoding to read the deck ('ISO-8859-1' by default). + +-ijk/--requested_ijk +---------------------------- + +.. option:: -ijk , --requested_ijk + :no-contents-entry: + :no-typesetting: + +Given i,j,k indices in the input model, return the modified i,j,k corresponding positions ('' by default; if not empty, e.g., '1,2,3', then there will not be generation of modified files, only the i,j,k mapped indices in the terminal). + +-explicit/--write_explicit_solution <0|1> +----------------------------------------- + +.. option:: -explicit <0|1>, --write_explicit_solution <0|1> + :no-contents-entry: + :no-typesetting: + +Set to 1 to explicitly write the cell values in the SOLUTION section in the deck ('0' by default). + +-precision/--significant_digits <0..15> +--------------------------------------- + +.. option:: -precision <0..15>, --significant_digits <0..15> + :no-contents-entry: + :no-typesetting: + +Set the number of significant digits used when writing floating-point values, or 0 to use machine precision diff --git a/docs/text/options/submodels-transformations.rst b/docs/text/options/submodels-transformations.rst new file mode 100644 index 0000000..5588948 --- /dev/null +++ b/docs/text/options/submodels-transformations.rst @@ -0,0 +1,36 @@ +.. _options-submodels-transformations: + +Submodels, transformations, and dual coarsening +=============================================== + +Extract submodels by region, polygon, or well neighbourhood; transform +grid coordinates; or separate net and non-net cells during coarsening. + +.. program:: pycopm + +-v/--vicinity_specification +------------------------------------------- + +.. option:: -v , --vicinity_specification + :no-contents-entry: + :no-typesetting: + +The location to extract the sub model which can be assigned by region values, e.g., 'fipnum 2,4' extracts the cells with fipnums equal to 2 or 4, by a polygon given the xy locations in meters, e.g., 'xypolygon [0,0] [30,0] [30,30] [0,0]', or by the name of the well and three different options for the neighbourhood: box, diamond, and diamondxy, where for box the i, j, and k interval around the connections are given, e.g., 'welln box [-1,1] [-2,2] [0,3]' results in a vicinity with 1 pm cell in the x direction, 2 pm cells in the y direction and only 3 cells in the k positive direction, while the diamond considers only the given number of cells around the well connections (e.g., 'welln diamond 2') and diamondxy it is restricted to the xy plane ('' by default). + +-d/--grid_transformation +---------------------------------------- + +.. option:: -d , --grid_transformation + :no-contents-entry: + :no-typesetting: + +Options to transform the x,y,z coordinates: 'translate [10,-5,4]' adds the values in meters to the coordinates, 'scale [1,2,3]' multiplies the coordinates by the given values respectively, and 'rotatexy 45' applies a rotation in degrees in the xy plane (rotatexz and rotateyz applies a rotation around the y and x axis respectively) ('' by default). + +-dual/--dual_porosity_criterion +------------------------------------------- + +.. option:: -dual , --dual_porosity_criterion + :no-contents-entry: + :no-typesetting: + +Set the criterium to differentiate net and non-net in coarsening using a static variable, e.g., 'poro <= 0.1' ('' by default). diff --git a/docs/text/options/workflow.rst b/docs/text/options/workflow.rst new file mode 100644 index 0000000..ae7583f --- /dev/null +++ b/docs/text/options/workflow.rst @@ -0,0 +1,44 @@ +.. _options-workflow: + +Input and workflow +================== + +Select the input, output location, OPM Flow command, and processing stages. + +.. program:: pycopm + +-i/--input_deck_path +---------------------------- + +.. option:: -i , --input_deck_path + :no-contents-entry: + :no-typesetting: + +The base name of the :doc:`toml configuration file <../configuration_file>` or the name of the deck, e.g., 'DROGON.DATA', ('input.toml' by default). + +-o/--output_directory +------------------------------ + +.. option:: -o , --output_directory + :no-contents-entry: + :no-typesetting: + +The base name of the :doc:`output folder <../output_folder>` ('.' by default, i.e., the folder where pycopm is executed). + +-f/--flow_command +--------------------------- + +.. option:: -f , --flow_command + :no-contents-entry: + :no-typesetting: + +OPM Flow path to executable (e.g., /home/pycopm/build/opm-simulators/bin/flow), or just 'flow' if this was installed via binaries or if the folder where flow is built has been added to your path ('flow' by default). + +-m/--execution_mode +-------------------------- + +.. option:: -m , --execution_mode + :no-contents-entry: + :no-typesetting: + +Execute a dry run of the input deck to generate the static properties ('prep'), generate only the modified files ('deck'), only exectute a dry run on the generated model ('dry'), 'prep_deck', 'deck_dry', or do all ('all') ('prep_deck' by default). diff --git a/docs/text/output_folder.rst b/docs/text/output_folder.rst index 1c06710..e2371f0 100644 --- a/docs/text/output_folder.rst +++ b/docs/text/output_folder.rst @@ -1,8 +1,8 @@ -============= +.. _output-folder: + Output folder ============= - -Via configuration files +Legacy TOML workflow ----------------------- As described in the :doc:`introduction <./introduction>`, in the early development of **pycopm** the focus was on history matching studies using `ERT `_ for the @@ -19,12 +19,12 @@ on the drogon model. The generate ert.ert file can be run directly calling ERT for further studies, and some useful plots and files are generated in the postprocessing folder. The OPM simulation results can be visualized using `ResInsight `_. -Via an OPM Flow input deck --------------------------- +Deck-based workflow (recommended) +--------------------------------- The current development of **pycopm** focuses on creating modified models, i.e., all needed input files to run OPM Flow, by using the input deck and defining the approach (grid refinement, grid coarsening, submodels, and transformations including scalings, rotations, and translations). -The following screenshot shows the input deck and generated files in the selected output folder (coarser for this example) after executing **pycopm** on the :ref:`generic` +The following screenshot shows the input deck and generated files in the selected output folder (coarser for this example) after executing **pycopm** on the :doc:`examples` .. figure:: figs/output_generic.png diff --git a/docs/text/related.rst b/docs/text/related.rst index d80715c..33e6a6c 100644 --- a/docs/text/related.rst +++ b/docs/text/related.rst @@ -1,66 +1,59 @@ -======= -Related -======= +.. _related: -Below are some tools that might be of interest; check 'em out 🙂. - -********** -pyopmspe11 -********** - -.. image:: ./figs/pyopmspe11.gif - :scale: 50% - -`A Python framework using OPM Flow for the CSP SPE11 benchmark project `_. +Related projects +================ -***** -pofff -***** - -.. image:: ./figs/pofff.png - -`An image-based history-matching framework for the FluidFlower Benchmark using OPM Flow `_. +Below are some tools that might be of interest; check 'em out 🙂. -************* -pyopmnearwell -************* +.. grid:: 1 1 2 2 + :gutter: 3 + :class-container: related-projects -.. image:: ./figs/pyopmnearwell.png - :scale: 60% + .. grid-item-card:: pyopmspe11 + :link: https://github.com/OPM/pyopmspe11 + :link-type: url + :img-top: figs/pyopmspe11.gif -`A Python framework to simulate near well dynamics using OPM Flow `_. + A Python framework using OPM Flow for the CSP SPE11 benchmark project. -***** -plopm -***** + .. grid-item-card:: pofff + :link: https://github.com/cssr-tools/pofff + :link-type: url + :img-top: figs/pofff.png -.. image:: ./figs/plopm.png + An image-based history-matching framework for the FluidFlower Benchmark using OPM Flow. -`Quick generation of PNGs, GIFs, and VTKs from a OPM Flow type model `_. + .. grid-item-card:: pyopmnearwell + :link: https://github.com/cssr-tools/pyopmnearwell + :link-type: url + :img-top: figs/pyopmnearwell.png -******** -expreccs -******** + A Python framework to simulate near-well dynamics using OPM Flow. -.. image:: ./figs/expreccs.gif - :scale: 50% + .. grid-item-card:: plopm + :link: https://github.com/cssr-tools/plopm + :link-type: url + :img-top: figs/plopm.png -`A Python framework using OPM Flow to simulate regional and site reservoirs for CO2 storage `_. + Quick generation of PNGs, GIFs, and VTKs from an OPM Flow model. -******* -ad-micp -******* + .. grid-item-card:: expreccs + :link: https://github.com/cssr-tools/expreccs + :link-type: url + :img-top: figs/expreccs.gif -.. image:: ./figs/ad-micp.gif - :scale: 40% + A Python framework using OPM Flow to simulate regional and site reservoirs for CO2 storage. -`A module to study CO2 leakage remediation by microbially induced calcite precipitation (MICP) `_. + .. grid-item-card:: ad-micp + :link: https://github.com/daavid00/ad-micp + :link-type: url + :img-top: figs/ad-micp.gif -**** -pymm -**** + A module to study CO2 leakage remediation by microbially induced calcite precipitation. -.. image:: ./figs/pymm.gif - :scale: 15% + .. grid-item-card:: pymm + :link: https://github.com/cssr-tools/pymm + :link-type: url + :img-top: figs/pymm.gif -`An open-source image-based framework for CFD in microsystems `_. + An open-source image-based framework for CFD in microsystems. diff --git a/docs/text/theory.rst b/docs/text/theory.rst index e942d72..89d2640 100644 --- a/docs/text/theory.rst +++ b/docs/text/theory.rst @@ -1,167 +1,46 @@ -****** -Theory -****** +.. _theory: +Theory and methods +================== Here insight about the **pycopm** methods regarding grid coarsening, grid refinement, submodels, and transformations is given. -=============== -Grid coarsening -=============== -The coarsening approach implemented in **pycopm** is based on industry standard grid formats which is preserved during coarsening. -This allows for direct application in standard reservoir simulators. In coarsening accuracy is traded for speed. -This is often appropriate, but care must be taken to assure that the coarse solution preserve the key properties of the model. -The `Lie 2019 textbook `_ -gives an excellent introduction to grid coarsening and upscaling geological properties, and the methods implemented in **pycopm** follow similar techniques. +.. grid:: 1 2 2 2 + :gutter: 3 -Corner-point grids are commonly use in subsurface simulations, due to its flexibility to handle complex geometries such as faults. -The grid is defined by vertical pillars and horizontal lines connecting the pillars, resulting in cells that can have up to six faces (hexahedrons), -but also these grids allow to define connections between non-neighboring cells (NNC). A special case of corner-point grids are Cartesian regular grids. -These grids also feature inactive cells, which are cells with zero pore volume and no connected to the active cells. + .. grid-item-card:: Grid coarsening + :link: theory-coarsening + :link-type: ref + :img-top: figs/coarsening_plopm.png -We define the grid coarsening operator :math:`\Xi_{xyz}^C`, which creates from an input grid :math:`\Omega` a coarser grid :math:`\Omega^*` (we adopt the notation * for -coarser quantities, while :math:`{}^C` in the coarsening operator emphasize the different approaches for the cell clustering) by removing -vertical pillars and horizontal lines: + Cell clustering, activity, property upscaling, transmissibilities, NNCs, and conservation. -.. math:: - \Xi_{xyz}^C(\Omega)\rightarrow\Omega^* + .. grid-item-card:: Grid refinement + :link: theory-refinement + :link-type: ref + :img-top: figs/refinement.png -To give flexibility in the coarsening, the cell clustering can be given as x, y, and z arrays to define the pillars/lines to be removed. -One natural question is how to handle the inactive cells in a cluster, and for this, three options to define the coarser cells are implemented: -min, max, and mode (mode is the default, i.e., the coarser cells is active if the number of active cells is the most common value in the cluster). -Figure 1 shows a simple 2D corner-point grid with different coarsening using min, max, and mode. + Added grid lines, inherited properties, and mapped indexed data. -.. figure:: figs/coarsening_plopm.png + .. grid-item-card:: Submodels + :link: theory-submodels + :link-type: ref + :img-top: figs/submodel.png - Figure 1: Example of coarser models from a grid with 18 cells, where the cell #11 is inactive. - One application where the max option could be useful is for models where there are a lot of - inactive cells in the z direction, while the min option could be useful for models applying - coarsening in the xy plane, since using min results in coarser models that do not generate - new connections across inactive cells. + Region, polygon, and well-based extraction with boundary pore-volume methods. -.. tip:: + .. grid-item-card:: Transformations + :link: theory-transformations + :link-type: ref + :img-top: figs/transformation_plopm.png - The figure can be generated by executing in the terminal (`docs_theory_coarsening.sh `_): + Translation, scaling, and rotation of geological grids. - .. code-block:: bash +.. toctree:: + :hidden: + :maxdepth: 1 - . ./tests/scripts/docs_theory_coarsening.sh - - -For upscaling geophysical properties, naturally, the pore volume in a coarser cell :math:`\Phi_{i^*,j^*,k^*}^*` (:math:`i^*`, :math:`j^*`, and :math:`k^*` referring to the cell -coarse indices in the x, y, and z direction respectively) are equal to the sum of pore volume from the corresponding cells -:math:`\Phi_{i,j,k}` in the input model, which are part of the cluster :math:`\mathbb{C}_{i^*,j^*,k^*}`: - -.. math:: - \Phi_{i^*,j^*,k^*}^* = \sum_{(i,j,k)\in\mathbb{C}_{i^*,j^*,k^*}}\Phi_{i,j,k} - -For example, in Figure 1 when coarsening into two cells using mode/max resulted in two coarse cells, where: - -.. math:: - - \mathbb{C}_{1^*,1^*,1^*}=\{ (1,1,1),(2,1,1),(3,1,1),(1,1,2),(2,1,2),(3,1,2),(1,1,3),(2,1,3),(3,1,3) \}. - -From this definition, the porosity in the coarse model :math:`\phi_{i^*,j^*,k^*}^*` can be simply computed by: - -.. math:: - \phi_{i^*,j^*,k^*}^* = \frac{\Phi_{i^*,j^*,k^*}^*}{\mathbb{V}_{i^*,j^*,k^*}^*} - -where :math:`\mathbb{V}_{i^*,j^*,k^*}^*` is the geometric volume of the coarser cell. - -For the rock permeability, there are different upscaling methods (e.g., arithmetic or harmonic average) that are case dependent and perform different, -see the `Lie 2019 textbook `_ -for comparison of these methods. In **pycopm**, by default the permeability in the x and y directions are computed using the arithmetic average, while -the permeability in the z direction by the harmonic average. As additional options, the permeabilities in the coarser cells :math:`\mathbb{K}_{i^*,j^*,k^*}^*` can -be set to equal the min, max, mean, or pv-weighted mean (pvmean) values from the permeabilities in the corresponding cluster :math:`\mathbb{K}_{i,j,k}`. For example, using the max for -permeabilities could be useful for history matching studies, where the parameters to history match are saturation functions -(relative permeabilities and capillary pressure). - -The above line mentions the initial application to develop **pycopm** (coarsening to history match saturation functions), as such there are no upscaling -methods implemented in **pycopm** for saturation functions. In a geological model, it is common to define different regions (referred as satnum) to assign -different saturation function tables. Then, if a cluster :math:`\mathbb{C}_{i^*,j^*,k^*}` involves different values for satnum, the mode (the most frequent value) is used -to assign the value in the coarser cell (this is also used to assign additional discrete coarser values such as fluid-in-place regions (fipnum)). - -For grids with large number of non-neighboring connections (faults) and inactive cells, then a better approach is to upscale transmisibilities. -A drawback of upscaling transmissibilities is that permeabilities cannot be used in history matching, but instead, transmissibilities multipliers, -which increases the number of parameters to history match and might break history match workflows where different permeability fields are generated -from spatial correlations. To this end, two approaches to upscale transmissibilities are implemented in **pycopm**. The former computes the coarser -transmissibility - -.. math:: - \scriptsize\mathbb{T}_{i^*,j^*,k^*}^*=\left[ T_{i^*,j^*,k^*\rightarrow i^*+1,j^*,k^*}^*,T_{i^*-1,j^*,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*+1,k^*}^*,T_{i^*,j^*-1,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*,T_{i^*,j^*,k^*-1\rightarrow i^*,j^*,k^*}^* \right] - -using the armonic averaging along the transsmissibility direction and summing over these values over the cell coarser face. For example, -for the transmissibility in the z+ direction: - -.. math:: - T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*=\sum_{(i,j)\in\mathbb{C}_{i^*,j^*,k^*}}\left( \sum_{(i,j,k)\rightarrow(i,j,k+1)\in\mathbb{C}_{i^*,j^*,k^*}} \frac{1}{T_{i,j,k\rightarrow i,j,k+1}} \right)^{-1} - -For cases where the coarsening is only along one direction (e.g., the z direction), the second method sets the transmissibility on the coarse cell -faces in that direction equal to the overlapping cell face values in the corresponding cluster (instead of computing the armonic average). For input -models with a large number of inactive cells, this approach has resulted in better results with respect to the input model simulations than using the -armonic average. For both approaches, the transmissibilities are scaled with the ratio of the cell -face effective areas (input model) to the coarse cell area. For non-neighbouring connections, this approach is also implemented, i.e., -the non-neighbouring connections in the coarser model sum the values from the non-neighbouring connections in the input model, which is important in -order to honor the pressure connections along open faults communicating different formations. - -=============== -Grid refinement -=============== - -The grid refinement is achieved by adding vertical pillars and horizontal lines in the grid from the input model. The refinement can be defined globally -in any direction (i, j, or k), as well as localized in defined grid indices. Properties such as porosity, permeabilities, and region numbers are set to the -same value in the corresponding finner cells inside the unrefined cell. Model properties defined by i,j,k locations such as wells, faults, and -boundary conditions are mapped to the new range of refined indices (i.e., adding additional entries to the generated deck). - -.. figure:: figs/refinement.png -.. figure:: figs/refinement_plopm.png - - Figure 2: Faults and wells in `MODEL3.DATA `_ (left) and after grid refinement "-g 2,2,2" (right) (upper and lower visualizations using `ResInsight `_ and `plopm `_ respectively (see/run `docs_theory_refinements.sh `_)). - -========= -Submodels -========= - -The generation of a submodel, i.e., a selected region in the input model, makes possible to lower the number of active cells and focus on an area of interest in -the input model. This results in smaller size of the input files, and faster simulations using OPM Flow. The submodel can be defined by properties matching a -value, e.g., all cells with fipnum equal to 1, or by a polygon given the xy locations in meters. Model properties defined by i,j,k locations such as wells and faults -are shifted to their corresponding values. If the wells/faults are not inside the extracted submodel, then these are not written to the generated deck. - -Regarding the boundary conditions in the extracted model with respect to the pore volume outisde the submodel, four options are provided by **pycopm**: - -#. no correction for the pore volume -#. adding the pore volume in each cell on the submodel boundary by summing all cell pore volumes in their corresponding i and j directions. If there is pore volume in the outside corners, this is equally distributed among the boundary cells in the two corresponding sides. -#. distributing the pore volume equally among the boundary cells in the submodel. -#. distributing the pore volume equally among all cells in the submodel - -.. figure:: figs/submodel.png -.. figure:: figs/submodel_plopm.png - - Figure 3: The shape to extract the sudmodel corresponds to "-v 'xypolygon [50,90] [60,60] [90,60] [65,40] [75,10] [50,30] [25,10] [35,40] [10,60] [40,60] [50,90]'". - The j indices for the cells have been accordingly shifted in the extracted model, and the right figure shows the projected pore volume on the boundary. Upper figures - are generated from ResInsight (screenshots) while the lower figures using plopm (see/run `docs_theory_submodels.sh `_). - -In addition, it is possible to extract submodels around wells, with three different options for the neighbourhood: box, diamond, and diamondxy. The box option allows to define -the intervals to extract the cells, while the diamond and diamondxy results in fewer cells since the cells in the corners are trimmed. - -.. figure:: figs/submodelwell.png -.. figure:: figs/submodelwell_plopm.png - - Figure 4: The submodel in `norne `_ by executing "-v 'E-3H diamondxy 0' -p 1", "-v 'E-3H diamond 1' -p 1", and "-v 'E-3H box [-1,2] [-2,3] [-1,1]' -p 1" respectively. - The top figures are visualized using ResInsight, while the lower figures are the 2D front view of the models using plopm (see/run `docs_theory_submodels.sh `_). - -=============== -Transformations -=============== - -Affine transformations are widely used in diverse applications since they preserve points, straight lines, and planes. In the field of reservoir management, there are large -uncertainties in the characterization of geological formations (reservoirs are typically located several kilometers below the surface). Once a reservoir model is created, over time -additional information from field measurements (e.g., seismic data, addiitonal wells, well's pressures, production rates) can indicate a different model characterization. This is when having -tools like **pycopm** can be handy, i.e., to apply translations of the grid (e.g., a different depth which impacts the pressure), scaling (e.g., to ease comparison between models made by different -groups which missmatch in the thickness of layers), and rotations (e.g., to align grids betweens two different models). - -.. figure:: figs/transformation.png -.. figure:: figs/transformation_plopm.png - - Figure 5: Extracted shape in Figure 3 after a rotation "-d 'rotatexy 45'" (left) and scaling "-d 'scale [1,0.25,1]'" (right). - The top figures are visualized using ResInsight, while the lower figures are generated using plopm (see/run `docs_theory_transformations.sh `_). + theory/coarsening + theory/refinement + theory/submodels + theory/transformations diff --git a/docs/text/theory/coarsening.rst b/docs/text/theory/coarsening.rst new file mode 100644 index 0000000..e2a4ce1 --- /dev/null +++ b/docs/text/theory/coarsening.rst @@ -0,0 +1,99 @@ +.. _theory-coarsening: + +Grid coarsening +=============== +The coarsening approach implemented in **pycopm** is based on industry standard grid formats which is preserved during coarsening. +This allows for direct application in standard reservoir simulators. In coarsening accuracy is traded for speed. +This is often appropriate, but care must be taken to assure that the coarse solution preserve the key properties of the model. +The `Lie 2019 textbook `_ +gives an excellent introduction to grid coarsening and upscaling geological properties, and the methods implemented in **pycopm** follow similar techniques. + +Corner-point grids are commonly use in subsurface simulations, due to its flexibility to handle complex geometries such as faults. +The grid is defined by vertical pillars and horizontal lines connecting the pillars, resulting in cells that can have up to six faces (hexahedrons), +but also these grids allow to define connections between non-neighboring cells (NNC). A special case of corner-point grids are Cartesian regular grids. +These grids also feature inactive cells, which are cells with zero pore volume and no connected to the active cells. + +We define the grid coarsening operator :math:`\Xi_{xyz}^C`, which creates from an input grid :math:`\Omega` a coarser grid :math:`\Omega^*` (we adopt the notation * for +coarser quantities, while :math:`{}^C` in the coarsening operator emphasize the different approaches for the cell clustering) by removing +vertical pillars and horizontal lines: + +.. math:: + \Xi_{xyz}^C(\Omega)\rightarrow\Omega^* + +To give flexibility in the coarsening, the cell clustering can be given as x, y, and z arrays to define the pillars/lines to be removed. +One natural question is how to handle the inactive cells in a cluster, and for this, three options to define the coarser cells are implemented: +min, max, and mode (mode is the default, i.e., the coarser cells is active if the number of active cells is the most common value in the cluster). +Figure 1 shows a simple 2D corner-point grid with different coarsening using min, max, and mode. + +.. figure:: ../figs/coarsening_plopm.png + + Figure 1: Example of coarser models from a grid with 18 cells, where the cell #11 is inactive. + One application where the max option could be useful is for models where there are a lot of + inactive cells in the z direction, while the min option could be useful for models applying + coarsening in the xy plane, since using min results in coarser models that do not generate + new connections across inactive cells. + +.. tip:: + + The figure can be generated by executing in the terminal (`docs_theory_coarsening.sh `_): + + .. code-block:: bash + + . ./tests/scripts/docs_theory_coarsening.sh + + +For upscaling geophysical properties, naturally, the pore volume in a coarser cell :math:`\Phi_{i^*,j^*,k^*}^*` (:math:`i^*`, :math:`j^*`, and :math:`k^*` referring to the cell +coarse indices in the x, y, and z direction respectively) are equal to the sum of pore volume from the corresponding cells +:math:`\Phi_{i,j,k}` in the input model, which are part of the cluster :math:`\mathbb{C}_{i^*,j^*,k^*}`: + +.. math:: + \Phi_{i^*,j^*,k^*}^* = \sum_{(i,j,k)\in\mathbb{C}_{i^*,j^*,k^*}}\Phi_{i,j,k} + +For example, in Figure 1 when coarsening into two cells using mode/max resulted in two coarse cells, where: + +.. math:: + + \mathbb{C}_{1^*,1^*,1^*}=\{ (1,1,1),(2,1,1),(3,1,1),(1,1,2),(2,1,2),(3,1,2),(1,1,3),(2,1,3),(3,1,3) \}. + +From this definition, the porosity in the coarse model :math:`\phi_{i^*,j^*,k^*}^*` can be simply computed by: + +.. math:: + \phi_{i^*,j^*,k^*}^* = \frac{\Phi_{i^*,j^*,k^*}^*}{\mathbb{V}_{i^*,j^*,k^*}^*} + +where :math:`\mathbb{V}_{i^*,j^*,k^*}^*` is the geometric volume of the coarser cell. + +For the rock permeability, there are different upscaling methods (e.g., arithmetic or harmonic average) that are case dependent and perform different, +see the `Lie 2019 textbook `_ +for comparison of these methods. In **pycopm**, by default the permeability in the x and y directions are computed using the arithmetic average, while +the permeability in the z direction by the harmonic average. As additional options, the permeabilities in the coarser cells :math:`\mathbb{K}_{i^*,j^*,k^*}^*` can +be set to equal the min, max, mean, or pv-weighted mean (pvmean) values from the permeabilities in the corresponding cluster :math:`\mathbb{K}_{i,j,k}`. For example, using the max for +permeabilities could be useful for history matching studies, where the parameters to history match are saturation functions +(relative permeabilities and capillary pressure). + +The above line mentions the initial application to develop **pycopm** (coarsening to history match saturation functions), as such there are no upscaling +methods implemented in **pycopm** for saturation functions. In a geological model, it is common to define different regions (referred as satnum) to assign +different saturation function tables. Then, if a cluster :math:`\mathbb{C}_{i^*,j^*,k^*}` involves different values for satnum, the mode (the most frequent value) is used +to assign the value in the coarser cell (this is also used to assign additional discrete coarser values such as fluid-in-place regions (fipnum)). + +For grids with large number of non-neighboring connections (faults) and inactive cells, then a better approach is to upscale transmisibilities. +A drawback of upscaling transmissibilities is that permeabilities cannot be used in history matching, but instead, transmissibilities multipliers, +which increases the number of parameters to history match and might break history match workflows where different permeability fields are generated +from spatial correlations. To this end, two approaches to upscale transmissibilities are implemented in **pycopm**. The former computes the coarser +transmissibility + +.. math:: + \scriptsize\mathbb{T}_{i^*,j^*,k^*}^*=\left[ T_{i^*,j^*,k^*\rightarrow i^*+1,j^*,k^*}^*,T_{i^*-1,j^*,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*+1,k^*}^*,T_{i^*,j^*-1,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*,T_{i^*,j^*,k^*-1\rightarrow i^*,j^*,k^*}^* \right] + +using the armonic averaging along the transsmissibility direction and summing over these values over the cell coarser face. For example, +for the transmissibility in the z+ direction: + +.. math:: + T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*=\sum_{(i,j)\in\mathbb{C}_{i^*,j^*,k^*}}\left( \sum_{(i,j,k)\rightarrow(i,j,k+1)\in\mathbb{C}_{i^*,j^*,k^*}} \frac{1}{T_{i,j,k\rightarrow i,j,k+1}} \right)^{-1} + +For cases where the coarsening is only along one direction (e.g., the z direction), the second method sets the transmissibility on the coarse cell +faces in that direction equal to the overlapping cell face values in the corresponding cluster (instead of computing the armonic average). For input +models with a large number of inactive cells, this approach has resulted in better results with respect to the input model simulations than using the +armonic average. For both approaches, the transmissibilities are scaled with the ratio of the cell +face effective areas (input model) to the coarse cell area. For non-neighbouring connections, this approach is also implemented, i.e., +the non-neighbouring connections in the coarser model sum the values from the non-neighbouring connections in the input model, which is important in +order to honor the pressure connections along open faults communicating different formations. diff --git a/docs/text/theory/refinement.rst b/docs/text/theory/refinement.rst new file mode 100644 index 0000000..4c837af --- /dev/null +++ b/docs/text/theory/refinement.rst @@ -0,0 +1,13 @@ +.. _theory-refinement: + +Grid refinement +=============== +The grid refinement is achieved by adding vertical pillars and horizontal lines in the grid from the input model. The refinement can be defined globally +in any direction (i, j, or k), as well as localized in defined grid indices. Properties such as porosity, permeabilities, and region numbers are set to the +same value in the corresponding finner cells inside the unrefined cell. Model properties defined by i,j,k locations such as wells, faults, and +boundary conditions are mapped to the new range of refined indices (i.e., adding additional entries to the generated deck). + +.. figure:: ../figs/refinement.png +.. figure:: ../figs/refinement_plopm.png + + Figure 2: Faults and wells in `MODEL3.DATA `_ (left) and after grid refinement "-g 2,2,2" (right) (upper and lower visualizations using `ResInsight `_ and `plopm `_ respectively (see/run `docs_theory_refinements.sh `_)). diff --git a/docs/text/theory/submodels.rst b/docs/text/theory/submodels.rst new file mode 100644 index 0000000..8eb8711 --- /dev/null +++ b/docs/text/theory/submodels.rst @@ -0,0 +1,31 @@ +.. _theory-submodels: + +Submodels +========= +The generation of a submodel, i.e., a selected region in the input model, makes possible to lower the number of active cells and focus on an area of interest in +the input model. This results in smaller size of the input files, and faster simulations using OPM Flow. The submodel can be defined by properties matching a +value, e.g., all cells with fipnum equal to 1, or by a polygon given the xy locations in meters. Model properties defined by i,j,k locations such as wells and faults +are shifted to their corresponding values. If the wells/faults are not inside the extracted submodel, then these are not written to the generated deck. + +Regarding the boundary conditions in the extracted model with respect to the pore volume outisde the submodel, four options are provided by **pycopm**: + +#. no correction for the pore volume +#. adding the pore volume in each cell on the submodel boundary by summing all cell pore volumes in their corresponding i and j directions. If there is pore volume in the outside corners, this is equally distributed among the boundary cells in the two corresponding sides. +#. distributing the pore volume equally among the boundary cells in the submodel. +#. distributing the pore volume equally among all cells in the submodel + +.. figure:: ../figs/submodel.png +.. figure:: ../figs/submodel_plopm.png + + Figure 3: The shape to extract the sudmodel corresponds to "-v 'xypolygon [50,90] [60,60] [90,60] [65,40] [75,10] [50,30] [25,10] [35,40] [10,60] [40,60] [50,90]'". + The j indices for the cells have been accordingly shifted in the extracted model, and the right figure shows the projected pore volume on the boundary. Upper figures + are generated from ResInsight (screenshots) while the lower figures using plopm (see/run `docs_theory_submodels.sh `_). + +In addition, it is possible to extract submodels around wells, with three different options for the neighbourhood: box, diamond, and diamondxy. The box option allows to define +the intervals to extract the cells, while the diamond and diamondxy results in fewer cells since the cells in the corners are trimmed. + +.. figure:: ../figs/submodelwell.png +.. figure:: ../figs/submodelwell_plopm.png + + Figure 4: The submodel in `norne `_ by executing "-v 'E-3H diamondxy 0' -p 1", "-v 'E-3H diamond 1' -p 1", and "-v 'E-3H box [-1,2] [-2,3] [-1,1]' -p 1" respectively. + The top figures are visualized using ResInsight, while the lower figures are the 2D front view of the models using plopm (see/run `docs_theory_submodels.sh `_). diff --git a/docs/text/theory/transformations.rst b/docs/text/theory/transformations.rst new file mode 100644 index 0000000..49cf1d4 --- /dev/null +++ b/docs/text/theory/transformations.rst @@ -0,0 +1,15 @@ +.. _theory-transformations: + +Transformations +=============== +Affine transformations are widely used in diverse applications since they preserve points, straight lines, and planes. In the field of reservoir management, there are large +uncertainties in the characterization of geological formations (reservoirs are typically located several kilometers below the surface). Once a reservoir model is created, over time +additional information from field measurements (e.g., seismic data, addiitonal wells, well's pressures, production rates) can indicate a different model characterization. This is when having +tools like **pycopm** can be handy, i.e., to apply translations of the grid (e.g., a different depth which impacts the pressure), scaling (e.g., to ease comparison between models made by different +groups which missmatch in the thickness of layers), and rotations (e.g., to align grids betweens two different models). + +.. figure:: ../figs/transformation.png +.. figure:: ../figs/transformation_plopm.png + + Figure 5: Extracted shape in Figure 3 after a rotation "-d 'rotatexy 45'" (left) and scaling "-d 'scale [1,0.25,1]'" (right). + The top figures are visualized using ResInsight, while the lower figures are generated using plopm (see/run `docs_theory_transformations.sh `_). diff --git a/docs/text/tutorial.rst b/docs/text/tutorial.rst new file mode 100644 index 0000000..1eef150 --- /dev/null +++ b/docs/text/tutorial.rst @@ -0,0 +1,22 @@ +.. _tutorial: + +Tutorial +======== + +Learn the actively developed generic deck workflow with the bundled ``HELLO_WORLD.DATA`` model. The tutorial covers coarsening, active-cell and pore-volume handling, submodel extraction, refinement, transformations, visualization, and explicit initialization. + +.. grid:: 1 1 1 1 + :gutter: 3 + + .. grid-item-card:: Hello World: generic deck workflow + :link: tutorial-hello-world + :link-type: ref + :img-top: figs/hello_world_3.png + + Work through all four generic model operations without losing the original example detail. + +.. toctree:: + :hidden: + :maxdepth: 1 + + tutorial/hello-world diff --git a/docs/text/tutorial/hello-world.rst b/docs/text/tutorial/hello-world.rst new file mode 100644 index 0000000..30a6f13 --- /dev/null +++ b/docs/text/tutorial/hello-world.rst @@ -0,0 +1,90 @@ +.. _tutorial-hello-world: + +Hello World: generic deck workflow +================================== + +This is the recommended starting point for new pycopm users. + +Learning objectives +------------------- + +* Coarsen and run a small OPM Flow deck. +* Control pore-volume and active-cell aggregation. +* Extract a polygonal submodel. +* Refine selected intervals. +* Rotate a generated grid. +* Visualize outputs and optionally write explicit solution values. + +Complete workflow +----------------- + +For the `HELLO_WORLD.DATA `_ deck, by executing: + +.. code-block:: bash + + # From inside the main pycopm folder + cd examples/decks + pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output + +.. note:: + + If the folder to flow is not added to your path, then pass the full path to the flow executable using the flag **-f /path/to/flow**. + +Using `plopm `_, then we can visualize the generated files in the output folder: + +.. code-block:: bash + + flow HELLO_WORLD + flow HELLO_WORLD_PYCOPM + plopm -i HELLO_WORLD -v porv -s ,,1 -ge 'black,1e-2' -fn hello_world_1_left + plopm -i HELLO_WORLD_PYCOPM -v porv -s ,,1 -ge 'black,1e-2' -fn hello_world_1_right + +.. figure:: ../figs/hello_world_1.png + + Dry run from the input cloned deck (left) and (right) coarsened model. Adding the flag **-p 1** would add the remove pore volume to the neighbouring cells (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). + +As mentioned above, if you do not have ResInsight, then to visualize the results in ParaView run + +.. code-block:: bash + + flow HELLO_WORLD.DATA --enable-vtk-output=true + flow HELLO_WORLD_PYCOPM.DATA --enable-vtk-output=true + +To make active the coarsened cell where there is only one active cell, this can be achieved by: + +.. code-block:: bash + + pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -a max + +.. figure:: ../figs/hello_world_2.png + + Dry run from the input cloned deck (left) and (right) coarsened model. The region numbers by default are given by the mode, e.g., use the flag **-n max** to keep the maximum integer (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). + +As described in the :doc:`theory <../theory>`, **pycopm** can be not only used for grid coarsening, but also to apply grid refinements, submodels, and transformations. +Then, with the following commands first we substract a submodel around the isolated grid cell proyecting the outside pore volume on the boundaries, after +we apply a grid refinement on the cells in the middle x and y location, and finally we rotate the model 45 degrees. + +.. code-block:: bash + + pycopm -i HELLO_WORLD.DATA -v 'xypolygon [4,8.5] [4,16.5] [11.5,16.5] [11.5,8.5] [4,8.5]' -p 1 -m all + pycopm -i HELLO_WORLD_PYCOPM.DATA -rx 0,0,0,2,0,0,0 -ry 0,0,0,2,0,0,0 -m all + pycopm -i HELLO_WORLD_PYCOPM_PYCOPM.DATA -d 'rotatexy 45' -m all + +.. figure:: ../figs/hello_world_3.png + + Extracted region with the projected pore volumes (bottom left), refinement around the center cells (top right), and rotation (bottom right). + The text in the legends highlight that the pore volume is conserved (35.58) and the number of active cells is reduced from 351 to 25 in the + submodel and after increased to 41 due to the grid refinement (figures using plopm, see/run `docs_via_deck_hello_world.sh `_). + +.. note:: + + To write the cell values for the SOLUTION section instead of using the EQUIL keyword, this can be achieved by the flag **-explicit 1**; the + only requirement is that the EQUIL keyword needs to be in the main input DATA file and no via INCLUDE files. + + +Next steps +---------- + +* Read :doc:`../theory` for equations and method details. +* Use :doc:`../command-line` for defaults and compatibility. +* Continue with :doc:`../examples` for larger models. diff --git a/docs/theory.html b/docs/theory.html index 062996a..c65c2a8 100644 --- a/docs/theory.html +++ b/docs/theory.html @@ -1,277 +1,705 @@ - - - - - - - Theory — pycopm documentation - - - - - - - - - - - - + + + + + + + + + Theory and methods — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + -
- - -
- -
-
-
-
    -
  • - -
  • - View page source -
  • + +
    + + +
    +
    + +
    +
    + +
    + +
    + +
    -
    -
    - -
    -

    Theory

    + +
    + + +
    +
    + + + + + +
    + +
    +

    Theory and methods#

    Here insight about the pycopm methods regarding grid coarsening, grid refinement, submodels, and transformations is given.

    -
    -

    Grid coarsening

    -

    The coarsening approach implemented in pycopm is based on industry standard grid formats which is preserved during coarsening. -This allows for direct application in standard reservoir simulators. In coarsening accuracy is traded for speed. -This is often appropriate, but care must be taken to assure that the coarse solution preserve the key properties of the model. -The Lie 2019 textbook -gives an excellent introduction to grid coarsening and upscaling geological properties, and the methods implemented in pycopm follow similar techniques.

    -

    Corner-point grids are commonly use in subsurface simulations, due to its flexibility to handle complex geometries such as faults. -The grid is defined by vertical pillars and horizontal lines connecting the pillars, resulting in cells that can have up to six faces (hexahedrons), -but also these grids allow to define connections between non-neighboring cells (NNC). A special case of corner-point grids are Cartesian regular grids. -These grids also feature inactive cells, which are cells with zero pore volume and no connected to the active cells.

    -

    We define the grid coarsening operator \(\Xi_{xyz}^C\), which creates from an input grid \(\Omega\) a coarser grid \(\Omega^*\) (we adopt the notation * for -coarser quantities, while \({}^C\) in the coarsening operator emphasize the different approaches for the cell clustering) by removing -vertical pillars and horizontal lines:

    -
    -\[\Xi_{xyz}^C(\Omega)\rightarrow\Omega^*\]
    -

    To give flexibility in the coarsening, the cell clustering can be given as x, y, and z arrays to define the pillars/lines to be removed. -One natural question is how to handle the inactive cells in a cluster, and for this, three options to define the coarser cells are implemented: -min, max, and mode (mode is the default, i.e., the coarser cells is active if the number of active cells is the most common value in the cluster). -Figure 1 shows a simple 2D corner-point grid with different coarsening using min, max, and mode.

    -
    -_images/coarsening_plopm.png -
    -

    Figure 1: Example of coarser models from a grid with 18 cells, where the cell #11 is inactive. -One application where the max option could be useful is for models where there are a lot of -inactive cells in the z direction, while the min option could be useful for models applying -coarsening in the xy plane, since using min results in coarser models that do not generate -new connections across inactive cells.

    -
    -
    -
    -

    Tip

    -

    The figure can be generated by executing in the terminal (docs_theory_coarsening.sh):

    -
    . ./tests/scripts/docs_theory_coarsening.sh
    -
    +
    +
    +
    +
    + +
    +
    +Grid coarsening
    +

    Cell clustering, activity, property upscaling, transmissibilities, NNCs, and conservation.

    +Grid coarsening
    +
    +
    +
    + +
    +
    +Grid refinement
    +

    Added grid lines, inherited properties, and mapped indexed data.

    +
    +Grid refinement
    +
    +
    +
    + +
    +
    +Submodels
    +

    Region, polygon, and well-based extraction with boundary pore-volume methods.

    +
    +Submodels
    +
    +
    +
    + +
    +
    +Transformations
    +

    Translation, scaling, and rotation of geological grids.

    +
    +Transformations
    +
    +
    +
    +
    -

    For upscaling geophysical properties, naturally, the pore volume in a coarser cell \(\Phi_{i^*,j^*,k^*}^*\) (\(i^*\), \(j^*\), and \(k^*\) referring to the cell -coarse indices in the x, y, and z direction respectively) are equal to the sum of pore volume from the corresponding cells -\(\Phi_{i,j,k}\) in the input model, which are part of the cluster \(\mathbb{C}_{i^*,j^*,k^*}\):

    -
    -\[\Phi_{i^*,j^*,k^*}^* = \sum_{(i,j,k)\in\mathbb{C}_{i^*,j^*,k^*}}\Phi_{i,j,k}\]
    -

    For example, in Figure 1 when coarsening into two cells using mode/max resulted in two coarse cells, where:

    -
    -\[\mathbb{C}_{1^*,1^*,1^*}=\{ (1,1,1),(2,1,1),(3,1,1),(1,1,2),(2,1,2),(3,1,2),(1,1,3),(2,1,3),(3,1,3) \}.\]
    -

    From this definition, the porosity in the coarse model \(\phi_{i^*,j^*,k^*}^*\) can be simply computed by:

    -
    -\[\phi_{i^*,j^*,k^*}^* = \frac{\Phi_{i^*,j^*,k^*}^*}{\mathbb{V}_{i^*,j^*,k^*}^*}\]
    -

    where \(\mathbb{V}_{i^*,j^*,k^*}^*\) is the geometric volume of the coarser cell.

    -

    For the rock permeability, there are different upscaling methods (e.g., arithmetic or harmonic average) that are case dependent and perform different, -see the Lie 2019 textbook -for comparison of these methods. In pycopm, by default the permeability in the x and y directions are computed using the arithmetic average, while -the permeability in the z direction by the harmonic average. As additional options, the permeabilities in the coarser cells \(\mathbb{K}_{i^*,j^*,k^*}^*\) can -be set to equal the min, max, mean, or pv-weighted mean (pvmean) values from the permeabilities in the corresponding cluster \(\mathbb{K}_{i,j,k}\). For example, using the max for -permeabilities could be useful for history matching studies, where the parameters to history match are saturation functions -(relative permeabilities and capillary pressure).

    -

    The above line mentions the initial application to develop pycopm (coarsening to history match saturation functions), as such there are no upscaling -methods implemented in pycopm for saturation functions. In a geological model, it is common to define different regions (referred as satnum) to assign -different saturation function tables. Then, if a cluster \(\mathbb{C}_{i^*,j^*,k^*}\) involves different values for satnum, the mode (the most frequent value) is used -to assign the value in the coarser cell (this is also used to assign additional discrete coarser values such as fluid-in-place regions (fipnum)).

    -

    For grids with large number of non-neighboring connections (faults) and inactive cells, then a better approach is to upscale transmisibilities. -A drawback of upscaling transmissibilities is that permeabilities cannot be used in history matching, but instead, transmissibilities multipliers, -which increases the number of parameters to history match and might break history match workflows where different permeability fields are generated -from spatial correlations. To this end, two approaches to upscale transmissibilities are implemented in pycopm. The former computes the coarser -transmissibility

    -
    -\[\scriptsize\mathbb{T}_{i^*,j^*,k^*}^*=\left[ T_{i^*,j^*,k^*\rightarrow i^*+1,j^*,k^*}^*,T_{i^*-1,j^*,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*+1,k^*}^*,T_{i^*,j^*-1,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*,T_{i^*,j^*,k^*-1\rightarrow i^*,j^*,k^*}^* \right]\]
    -

    using the armonic averaging along the transsmissibility direction and summing over these values over the cell coarser face. For example, -for the transmissibility in the z+ direction:

    -
    -\[T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*=\sum_{(i,j)\in\mathbb{C}_{i^*,j^*,k^*}}\left( \sum_{(i,j,k)\rightarrow(i,j,k+1)\in\mathbb{C}_{i^*,j^*,k^*}} \frac{1}{T_{i,j,k\rightarrow i,j,k+1}} \right)^{-1}\]
    -

    For cases where the coarsening is only along one direction (e.g., the z direction), the second method sets the transmissibility on the coarse cell -faces in that direction equal to the overlapping cell face values in the corresponding cluster (instead of computing the armonic average). For input -models with a large number of inactive cells, this approach has resulted in better results with respect to the input model simulations than using the -armonic average. For both approaches, the transmissibilities are scaled with the ratio of the cell -face effective areas (input model) to the coarse cell area. For non-neighbouring connections, this approach is also implemented, i.e., -the non-neighbouring connections in the coarser model sum the values from the non-neighbouring connections in the input model, which is important in -order to honor the pressure connections along open faults communicating different formations.

    -
    -
    -

    Grid refinement

    -

    The grid refinement is achieved by adding vertical pillars and horizontal lines in the grid from the input model. The refinement can be defined globally -in any direction (i, j, or k), as well as localized in defined grid indices. Properties such as porosity, permeabilities, and region numbers are set to the -same value in the corresponding finner cells inside the unrefined cell. Model properties defined by i,j,k locations such as wells, faults, and -boundary conditions are mapped to the new range of refined indices (i.e., adding additional entries to the generated deck).

    -
    -_images/refinement.png -
    -
    -_images/refinement_plopm.png -
    -

    Figure 2: Faults and wells in MODEL3.DATA (left) and after grid refinement “-g 2,2,2” (right) (upper and lower visualizations using ResInsight and plopm respectively (see/run docs_theory_refinements.sh)).

    -
    -
    -
    -
    -

    Submodels

    -

    The generation of a submodel, i.e., a selected region in the input model, makes possible to lower the number of active cells and focus on an area of interest in -the input model. This results in smaller size of the input files, and faster simulations using OPM Flow. The submodel can be defined by properties matching a -value, e.g., all cells with fipnum equal to 1, or by a polygon given the xy locations in meters. Model properties defined by i,j,k locations such as wells and faults -are shifted to their corresponding values. If the wells/faults are not inside the extracted submodel, then these are not written to the generated deck.

    -

    Regarding the boundary conditions in the extracted model with respect to the pore volume outisde the submodel, four options are provided by pycopm:

    -
      -
    1. no correction for the pore volume

    2. -
    3. adding the pore volume in each cell on the submodel boundary by summing all cell pore volumes in their corresponding i and j directions. If there is pore volume in the outside corners, this is equally distributed among the boundary cells in the two corresponding sides.

    4. -
    5. distributing the pore volume equally among the boundary cells in the submodel.

    6. -
    7. distributing the pore volume equally among all cells in the submodel

    8. -
    -
    -_images/submodel.png -
    -
    -_images/submodel_plopm.png -
    -

    Figure 3: The shape to extract the sudmodel corresponds to “-v ‘xypolygon [50,90] [60,60] [90,60] [65,40] [75,10] [50,30] [25,10] [35,40] [10,60] [40,60] [50,90]’”. -The j indices for the cells have been accordingly shifted in the extracted model, and the right figure shows the projected pore volume on the boundary. Upper figures -are generated from ResInsight (screenshots) while the lower figures using plopm (see/run docs_theory_submodels.sh).

    -
    -
    -

    In addition, it is possible to extract submodels around wells, with three different options for the neighbourhood: box, diamond, and diamondxy. The box option allows to define -the intervals to extract the cells, while the diamond and diamondxy results in fewer cells since the cells in the corners are trimmed.

    -
    -_images/submodelwell.png -
    -
    -_images/submodelwell_plopm.png -
    -

    Figure 4: The submodel in norne by executing “-v ‘E-3H diamondxy 0’ -p 1”, “-v ‘E-3H diamond 1’ -p 1”, and “-v ‘E-3H box [-1,2] [-2,3] [-1,1]’ -p 1” respectively. -The top figures are visualized using ResInsight, while the lower figures are the 2D front view of the models using plopm (see/run docs_theory_submodels.sh).

    -
    -
    -
    -
    -

    Transformations

    -

    Affine transformations are widely used in diverse applications since they preserve points, straight lines, and planes. In the field of reservoir management, there are large -uncertainties in the characterization of geological formations (reservoirs are typically located several kilometers below the surface). Once a reservoir model is created, over time -additional information from field measurements (e.g., seismic data, addiitonal wells, well’s pressures, production rates) can indicate a different model characterization. This is when having -tools like pycopm can be handy, i.e., to apply translations of the grid (e.g., a different depth which impacts the pressure), scaling (e.g., to ease comparison between models made by different -groups which missmatch in the thickness of layers), and rotations (e.g., to align grids betweens two different models).

    -
    -_images/transformation.png -
    -
    -_images/transformation_plopm.png -
    -

    Figure 5: Extracted shape in Figure 3 after a rotation “-d ‘rotatexy 45’” (left) and scaling “-d ‘scale [1,0.25,1]’” (right). -The top figures are visualized using ResInsight, while the lower figures are generated using plopm (see/run docs_theory_transformations.sh).

    -
    -
    -
    -
    -
    -
    + + + + +
+ +
+ + + + + +
+ +
+ + + - + + + + + +
+ - +
+ \ No newline at end of file diff --git a/docs/theory/coarsening.html b/docs/theory/coarsening.html new file mode 100644 index 0000000..af7456d --- /dev/null +++ b/docs/theory/coarsening.html @@ -0,0 +1,738 @@ + + + + + + + + + + + Grid coarsening — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Grid coarsening#

+

The coarsening approach implemented in pycopm is based on industry standard grid formats which is preserved during coarsening. +This allows for direct application in standard reservoir simulators. In coarsening accuracy is traded for speed. +This is often appropriate, but care must be taken to assure that the coarse solution preserve the key properties of the model. +The Lie 2019 textbook +gives an excellent introduction to grid coarsening and upscaling geological properties, and the methods implemented in pycopm follow similar techniques.

+

Corner-point grids are commonly use in subsurface simulations, due to its flexibility to handle complex geometries such as faults. +The grid is defined by vertical pillars and horizontal lines connecting the pillars, resulting in cells that can have up to six faces (hexahedrons), +but also these grids allow to define connections between non-neighboring cells (NNC). A special case of corner-point grids are Cartesian regular grids. +These grids also feature inactive cells, which are cells with zero pore volume and no connected to the active cells.

+

We define the grid coarsening operator \(\Xi_{xyz}^C\), which creates from an input grid \(\Omega\) a coarser grid \(\Omega^*\) (we adopt the notation * for +coarser quantities, while \({}^C\) in the coarsening operator emphasize the different approaches for the cell clustering) by removing +vertical pillars and horizontal lines:

+
+\[\Xi_{xyz}^C(\Omega)\rightarrow\Omega^*\]
+

To give flexibility in the coarsening, the cell clustering can be given as x, y, and z arrays to define the pillars/lines to be removed. +One natural question is how to handle the inactive cells in a cluster, and for this, three options to define the coarser cells are implemented: +min, max, and mode (mode is the default, i.e., the coarser cells is active if the number of active cells is the most common value in the cluster). +Figure 1 shows a simple 2D corner-point grid with different coarsening using min, max, and mode.

+
+../_images/coarsening_plopm.png +
+

Figure 1: Example of coarser models from a grid with 18 cells, where the cell #11 is inactive. +One application where the max option could be useful is for models where there are a lot of +inactive cells in the z direction, while the min option could be useful for models applying +coarsening in the xy plane, since using min results in coarser models that do not generate +new connections across inactive cells.#

+
+
+
+

Tip

+

The figure can be generated by executing in the terminal (docs_theory_coarsening.sh):

+
. ./tests/scripts/docs_theory_coarsening.sh
+
+
+
+

For upscaling geophysical properties, naturally, the pore volume in a coarser cell \(\Phi_{i^*,j^*,k^*}^*\) (\(i^*\), \(j^*\), and \(k^*\) referring to the cell +coarse indices in the x, y, and z direction respectively) are equal to the sum of pore volume from the corresponding cells +\(\Phi_{i,j,k}\) in the input model, which are part of the cluster \(\mathbb{C}_{i^*,j^*,k^*}\):

+
+\[\Phi_{i^*,j^*,k^*}^* = \sum_{(i,j,k)\in\mathbb{C}_{i^*,j^*,k^*}}\Phi_{i,j,k}\]
+

For example, in Figure 1 when coarsening into two cells using mode/max resulted in two coarse cells, where:

+
+\[\mathbb{C}_{1^*,1^*,1^*}=\{ (1,1,1),(2,1,1),(3,1,1),(1,1,2),(2,1,2),(3,1,2),(1,1,3),(2,1,3),(3,1,3) \}.\]
+

From this definition, the porosity in the coarse model \(\phi_{i^*,j^*,k^*}^*\) can be simply computed by:

+
+\[\phi_{i^*,j^*,k^*}^* = \frac{\Phi_{i^*,j^*,k^*}^*}{\mathbb{V}_{i^*,j^*,k^*}^*}\]
+

where \(\mathbb{V}_{i^*,j^*,k^*}^*\) is the geometric volume of the coarser cell.

+

For the rock permeability, there are different upscaling methods (e.g., arithmetic or harmonic average) that are case dependent and perform different, +see the Lie 2019 textbook +for comparison of these methods. In pycopm, by default the permeability in the x and y directions are computed using the arithmetic average, while +the permeability in the z direction by the harmonic average. As additional options, the permeabilities in the coarser cells \(\mathbb{K}_{i^*,j^*,k^*}^*\) can +be set to equal the min, max, mean, or pv-weighted mean (pvmean) values from the permeabilities in the corresponding cluster \(\mathbb{K}_{i,j,k}\). For example, using the max for +permeabilities could be useful for history matching studies, where the parameters to history match are saturation functions +(relative permeabilities and capillary pressure).

+

The above line mentions the initial application to develop pycopm (coarsening to history match saturation functions), as such there are no upscaling +methods implemented in pycopm for saturation functions. In a geological model, it is common to define different regions (referred as satnum) to assign +different saturation function tables. Then, if a cluster \(\mathbb{C}_{i^*,j^*,k^*}\) involves different values for satnum, the mode (the most frequent value) is used +to assign the value in the coarser cell (this is also used to assign additional discrete coarser values such as fluid-in-place regions (fipnum)).

+

For grids with large number of non-neighboring connections (faults) and inactive cells, then a better approach is to upscale transmisibilities. +A drawback of upscaling transmissibilities is that permeabilities cannot be used in history matching, but instead, transmissibilities multipliers, +which increases the number of parameters to history match and might break history match workflows where different permeability fields are generated +from spatial correlations. To this end, two approaches to upscale transmissibilities are implemented in pycopm. The former computes the coarser +transmissibility

+
+\[\scriptsize\mathbb{T}_{i^*,j^*,k^*}^*=\left[ T_{i^*,j^*,k^*\rightarrow i^*+1,j^*,k^*}^*,T_{i^*-1,j^*,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*+1,k^*}^*,T_{i^*,j^*-1,k^*\rightarrow i^*,j^*,k^*}^*,T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*,T_{i^*,j^*,k^*-1\rightarrow i^*,j^*,k^*}^* \right]\]
+

using the armonic averaging along the transsmissibility direction and summing over these values over the cell coarser face. For example, +for the transmissibility in the z+ direction:

+
+\[T_{i^*,j^*,k^*\rightarrow i^*,j^*,k^*+1}^*=\sum_{(i,j)\in\mathbb{C}_{i^*,j^*,k^*}}\left( \sum_{(i,j,k)\rightarrow(i,j,k+1)\in\mathbb{C}_{i^*,j^*,k^*}} \frac{1}{T_{i,j,k\rightarrow i,j,k+1}} \right)^{-1}\]
+

For cases where the coarsening is only along one direction (e.g., the z direction), the second method sets the transmissibility on the coarse cell +faces in that direction equal to the overlapping cell face values in the corresponding cluster (instead of computing the armonic average). For input +models with a large number of inactive cells, this approach has resulted in better results with respect to the input model simulations than using the +armonic average. For both approaches, the transmissibilities are scaled with the ratio of the cell +face effective areas (input model) to the coarse cell area. For non-neighbouring connections, this approach is also implemented, i.e., +the non-neighbouring connections in the coarser model sum the values from the non-neighbouring connections in the input model, which is important in +order to honor the pressure connections along open faults communicating different formations.

+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/theory/refinement.html b/docs/theory/refinement.html new file mode 100644 index 0000000..f0337e8 --- /dev/null +++ b/docs/theory/refinement.html @@ -0,0 +1,674 @@ + + + + + + + + + + + Grid refinement — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Grid refinement#

+

The grid refinement is achieved by adding vertical pillars and horizontal lines in the grid from the input model. The refinement can be defined globally +in any direction (i, j, or k), as well as localized in defined grid indices. Properties such as porosity, permeabilities, and region numbers are set to the +same value in the corresponding finner cells inside the unrefined cell. Model properties defined by i,j,k locations such as wells, faults, and +boundary conditions are mapped to the new range of refined indices (i.e., adding additional entries to the generated deck).

+
+../_images/refinement.png +
+
+../_images/refinement_plopm.png +
+

Figure 2: Faults and wells in MODEL3.DATA (left) and after grid refinement "-g 2,2,2" (right) (upper and lower visualizations using ResInsight and plopm respectively (see/run docs_theory_refinements.sh)).#

+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/theory/submodels.html b/docs/theory/submodels.html new file mode 100644 index 0000000..ad69a15 --- /dev/null +++ b/docs/theory/submodels.html @@ -0,0 +1,695 @@ + + + + + + + + + + + Submodels — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Submodels#

+

The generation of a submodel, i.e., a selected region in the input model, makes possible to lower the number of active cells and focus on an area of interest in +the input model. This results in smaller size of the input files, and faster simulations using OPM Flow. The submodel can be defined by properties matching a +value, e.g., all cells with fipnum equal to 1, or by a polygon given the xy locations in meters. Model properties defined by i,j,k locations such as wells and faults +are shifted to their corresponding values. If the wells/faults are not inside the extracted submodel, then these are not written to the generated deck.

+

Regarding the boundary conditions in the extracted model with respect to the pore volume outisde the submodel, four options are provided by pycopm:

+
    +
  1. no correction for the pore volume

  2. +
  3. adding the pore volume in each cell on the submodel boundary by summing all cell pore volumes in their corresponding i and j directions. If there is pore volume in the outside corners, this is equally distributed among the boundary cells in the two corresponding sides.

  4. +
  5. distributing the pore volume equally among the boundary cells in the submodel.

  6. +
  7. distributing the pore volume equally among all cells in the submodel

  8. +
+
+../_images/submodel.png +
+
+../_images/submodel_plopm.png +
+

Figure 3: The shape to extract the sudmodel corresponds to "-v 'xypolygon [50,90] [60,60] [90,60] [65,40] [75,10] [50,30] [25,10] [35,40] [10,60] [40,60] [50,90]'". +The j indices for the cells have been accordingly shifted in the extracted model, and the right figure shows the projected pore volume on the boundary. Upper figures +are generated from ResInsight (screenshots) while the lower figures using plopm (see/run docs_theory_submodels.sh).#

+
+
+

In addition, it is possible to extract submodels around wells, with three different options for the neighbourhood: box, diamond, and diamondxy. The box option allows to define +the intervals to extract the cells, while the diamond and diamondxy results in fewer cells since the cells in the corners are trimmed.

+
+../_images/submodelwell.png +
+
+../_images/submodelwell_plopm.png +
+

Figure 4: The submodel in norne by executing "-v 'E-3H diamondxy 0' -p 1", "-v 'E-3H diamond 1' -p 1", and "-v 'E-3H box [-1,2] [-2,3] [-1,1]' -p 1" respectively. +The top figures are visualized using ResInsight, while the lower figures are the 2D front view of the models using plopm (see/run docs_theory_submodels.sh).#

+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/theory/transformations.html b/docs/theory/transformations.html new file mode 100644 index 0000000..88b9658 --- /dev/null +++ b/docs/theory/transformations.html @@ -0,0 +1,676 @@ + + + + + + + + + + + Transformations — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Transformations#

+

Affine transformations are widely used in diverse applications since they preserve points, straight lines, and planes. In the field of reservoir management, there are large +uncertainties in the characterization of geological formations (reservoirs are typically located several kilometers below the surface). Once a reservoir model is created, over time +additional information from field measurements (e.g., seismic data, addiitonal wells, well's pressures, production rates) can indicate a different model characterization. This is when having +tools like pycopm can be handy, i.e., to apply translations of the grid (e.g., a different depth which impacts the pressure), scaling (e.g., to ease comparison between models made by different +groups which missmatch in the thickness of layers), and rotations (e.g., to align grids betweens two different models).

+
+../_images/transformation.png +
+
+../_images/transformation_plopm.png +
+

Figure 5: Extracted shape in Figure 3 after a rotation "-d 'rotatexy 45'" (left) and scaling "-d 'scale [1,0.25,1]'" (right). +The top figures are visualized using ResInsight, while the lower figures are generated using plopm (see/run docs_theory_transformations.sh).#

+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/tutorial.html b/docs/tutorial.html new file mode 100644 index 0000000..8444a7e --- /dev/null +++ b/docs/tutorial.html @@ -0,0 +1,672 @@ + + + + + + + + + + + Tutorial — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Tutorial#

+

Learn the actively developed generic deck workflow with the bundled HELLO_WORLD.DATA model. The tutorial covers coarsening, active-cell and pore-volume handling, submodel extraction, refinement, transformations, visualization, and explicit initialization.

+
+
+
+
+ +
+
+Hello World: generic deck workflow
+

Work through all four generic model operations without losing the original example detail.

+
+Hello World: generic deck workflow
+
+
+
+
+
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/docs/tutorial/hello-world.html b/docs/tutorial/hello-world.html new file mode 100644 index 0000000..087c88f --- /dev/null +++ b/docs/tutorial/hello-world.html @@ -0,0 +1,754 @@ + + + + + + + + + + + Hello World: generic deck workflow — pycopm documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + +
+
+ + + + +
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+ +
+

Hello World: generic deck workflow#

+

This is the recommended starting point for new pycopm users.

+
+

Learning objectives#

+
    +
  • Coarsen and run a small OPM Flow deck.

  • +
  • Control pore-volume and active-cell aggregation.

  • +
  • Extract a polygonal submodel.

  • +
  • Refine selected intervals.

  • +
  • Rotate a generated grid.

  • +
  • Visualize outputs and optionally write explicit solution values.

  • +
+
+
+

Complete workflow#

+

For the HELLO_WORLD.DATA deck, by executing:

+
# From inside the main pycopm folder
+cd examples/decks
+pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -o output
+
+
+
+

Note

+

If the folder to flow is not added to your path, then pass the full path to the flow executable using the flag -f /path/to/flow.

+
+

Using plopm, then we can visualize the generated files in the output folder:

+
flow HELLO_WORLD
+flow HELLO_WORLD_PYCOPM
+plopm -i HELLO_WORLD -v porv -s ,,1 -ge 'black,1e-2' -fn hello_world_1_left
+plopm -i HELLO_WORLD_PYCOPM -v porv -s ,,1 -ge 'black,1e-2' -fn hello_world_1_right
+
+
+
+../_images/hello_world_1.png +
+

Dry run from the input cloned deck (left) and (right) coarsened model. Adding the flag -p 1 would add the remove pore volume to the neighbouring cells (figures using plopm, see/run docs_via_deck_hello_world.sh).#

+
+
+

As mentioned above, if you do not have ResInsight, then to visualize the results in ParaView run

+
flow HELLO_WORLD.DATA --enable-vtk-output=true
+flow HELLO_WORLD_PYCOPM.DATA --enable-vtk-output=true
+
+
+

To make active the coarsened cell where there is only one active cell, this can be achieved by:

+
pycopm -i HELLO_WORLD.DATA -c 5,5,1 -m all -a max
+
+
+
+../_images/hello_world_2.png +
+

Dry run from the input cloned deck (left) and (right) coarsened model. The region numbers by default are given by the mode, e.g., use the flag -n max to keep the maximum integer (figures using plopm, see/run docs_via_deck_hello_world.sh).#

+
+
+

As described in the theory, pycopm can be not only used for grid coarsening, but also to apply grid refinements, submodels, and transformations. +Then, with the following commands first we substract a submodel around the isolated grid cell proyecting the outside pore volume on the boundaries, after +we apply a grid refinement on the cells in the middle x and y location, and finally we rotate the model 45 degrees.

+
pycopm -i HELLO_WORLD.DATA -v 'xypolygon [4,8.5] [4,16.5] [11.5,16.5] [11.5,8.5] [4,8.5]' -p 1 -m all
+pycopm -i HELLO_WORLD_PYCOPM.DATA -rx 0,0,0,2,0,0,0 -ry 0,0,0,2,0,0,0 -m all
+pycopm -i HELLO_WORLD_PYCOPM_PYCOPM.DATA -d 'rotatexy 45' -m all
+
+
+
+../_images/hello_world_3.png +
+

Extracted region with the projected pore volumes (bottom left), refinement around the center cells (top right), and rotation (bottom right). +The text in the legends highlight that the pore volume is conserved (35.58) and the number of active cells is reduced from 351 to 25 in the +submodel and after increased to 41 due to the grid refinement (figures using plopm, see/run docs_via_deck_hello_world.sh).#

+
+
+
+

Note

+

To write the cell values for the SOLUTION section instead of using the EQUIL keyword, this can be achieved by the flag -explicit 1; the +only requirement is that the EQUIL keyword needs to be in the main input DATA file and no via INCLUDE files.

+
+
+
+

Next steps#

+ +
+
+ + +
+ + + + + + + +
+ + + + +
+ + +
+
+ +
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/examples/configurations/drogon/hm.toml b/examples/configurations/drogon/hm.toml index 3bf394e..1e3dc17 100644 --- a/examples/configurations/drogon/hm.toml +++ b/examples/configurations/drogon/hm.toml @@ -35,7 +35,7 @@ let_parameters = [["lw", 3, 1, "UNIFORM", 1.1, 5], ["tg", 3, 1, "UNIFORM", 1.1, 5], ["log", 3, 1, "UNIFORM", 1.1, 5], ["eog", 1, 1, "UNIFORM", -1, 2], -["tog", 1, 1, "UNIFORM", 1.1, 5], +["tog", 1.1, 1, "UNIFORM", 1.1, 5], ["lmlto", 1.5, 1, "UNIFORM", 1, 2], ["lmltg", 1.5, 1, "UNIFORM", 1, 2], ["kwow", 1, 1, "UNIFORM", 0.1, 1], diff --git a/examples/configurations/drogon/input.toml b/examples/configurations/drogon/input.toml index a9a3a59..9e8302e 100644 --- a/examples/configurations/drogon/input.toml +++ b/examples/configurations/drogon/input.toml @@ -35,7 +35,7 @@ let_parameters = [["lw", 3, 1, "UNIFORM", 1.1, 5], ["tg", 3, 1, "UNIFORM", 1.1, 5], ["log", 3, 1, "UNIFORM", 1.1, 5], ["eog", 1, 1, "UNIFORM", -1, 2], -["tog", 1, 1, "UNIFORM", 1.1, 5], +["tog", 1.1, 1, "UNIFORM", 1.1, 5], ["lmlto", 1.5, 1, "UNIFORM", 1, 2], ["lmltg", 1.5, 1, "UNIFORM", 1, 2], ["kwow", 1, 1, "UNIFORM", 0.1, 1], diff --git a/examples/configurations/norne/input.toml b/examples/configurations/norne/input.toml index 0359914..b391459 100644 --- a/examples/configurations/norne/input.toml +++ b/examples/configurations/norne/input.toml @@ -36,7 +36,7 @@ let_parameters = [["lw", 3, 1, "UNIFORM", 1.1, 5], ["tg", 3, 1, "UNIFORM", 1.1, 5], ["log", 3, 1, "UNIFORM", 1.1, 5], ["eog", 1, 1, "UNIFORM", -1, 2], -["tog", 1, 1, "UNIFORM", 1.1, 5], +["tog", 1.1, 1, "UNIFORM", 1.1, 5], ["lmlto", 1.5, 1, "UNIFORM", 1, 2], ["emlto", 1, 1, "UNIFORM", 0.9, 2.1], ["tmlto", 1.5, 1, "UNIFORM", 1, 2], diff --git a/pyproject.toml b/pyproject.toml index e5f1eaf..f254bfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,11 +23,11 @@ classifiers = [ "Operating System :: OS Independent" ] dependencies = [ - "alive_progress<=3.3.0", - "ert==24.0.0", - "h5py<=3.16.0", - "mako<=1.4.1", - "opm<=2026.4" + "alive_progress", + "ert==25.0.1", + "h5py", + "mako", + "opm" ] requires-python = ">=3.12" diff --git a/src/pycopm/core/pycopm.py b/src/pycopm/core/pycopm.py index 45ce19c..b397e07 100644 --- a/src/pycopm/core/pycopm.py +++ b/src/pycopm/core/pycopm.py @@ -29,6 +29,14 @@ from pycopm.utils.generate_decks import create_deck from pycopm.utils.input_values import create_deck_config, load_toml_config from pycopm.utils.runs_executer import generate_postprocessing_plots, run_simulations +from pycopm.utils.terminal import ( + cli_correct_value, + cli_error_value, + cli_info_value, + pycopm_error, + pycopm_info, + pycopm_success, +) def main(argv: list[str] | None = None) -> None: @@ -145,7 +153,10 @@ def main(argv: list[str] | None = None) -> None: int(cmdargs.significant_digits), ) cfg.flow_command = _check_flow(cmdargs.flow_command, cfg.flow_command, input_file) - print(f"\npycopm is generating the input files for {cfg.model_name}, please wait.") + pycopm_info( + f"generating the input files for {cli_info_value(cfg.model_name)}, " + "please wait..." + ) for folder in ["preprocessing", "parameters", "jobs", "observations"]: (output_folder / folder).mkdir(parents=True, exist_ok=True) @@ -169,9 +180,10 @@ def main(argv: list[str] | None = None) -> None: ) destination_include = output_folder / "preprocessing" / include_folder shutil.copytree(source_include, destination_include, dirs_exist_ok=True) - print(f"\nThe generated files have been written to {cfg.output_directory}") + pycopm_success("input files required by ERT written to ", cfg.output_directory, []) if cfg.execution_mode in ["single-run", "ert"]: - print("\nRunning the simulations, please wait.") + + pycopm_info("running ERT, please wait...") # Run OPM Flow or the selected ERT workflow run_simulations(cfg) @@ -207,7 +219,7 @@ def _parse_arguments(argv: list[str] | None = None) -> argparse.Namespace: "--input_deck_path", type=str.strip, default="input.toml", - help="The base name of the toml configuration file or the name of the deck", + help="The base name of the TOML configuration file or the name of the deck", ) parser.add_argument( "-o", @@ -448,14 +460,14 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: input_file = cmdargs.input_deck_path # Select the workflow from the input filename extension if not input_file.endswith((".DATA", ".toml")): - print( - f"\nInvalid extension for input file '-i {input_file}', " - "valid extensions are .DATA or .toml\n" + pycopm_error( + f"invalid extension {cli_error_value(f'-i {input_file}')}, valid extensions " + f"are {cli_correct_value('.DATA')} or {cli_correct_value('.toml')}." ) - raise SystemExit(1) if not cmdargs.output_directory: - print("\nInvalid value for '-o', the output folder cannot be empty.\n") - raise SystemExit(1) + pycopm_error( + f"invalid value {cli_error_value('-o')}, the output folder cannot be empty." + ) # Only -i, -o, -f, and -precision apply to TOML configuration files if input_file.endswith(".toml"): data_options = { @@ -491,12 +503,11 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: if getattr(cmdargs, name) != default ] if invalid_options: - print( - "\nInvalid option for a toml configuration file; only '-i', '-o', " + pycopm_error( + "invalid option for a TOML configuration file; only '-i', '-o', " "'-f', and '-precision' can be used. Invalid options: " - f"{', '.join(invalid_options)}.\n" + f"{', '.join(invalid_options)}." ) - raise SystemExit(1) return # Verify the complete Flow command, including any launcher and arguments try: @@ -504,8 +515,9 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: except ValueError: flow_arguments = [] if not flow_arguments: - print(f"\nInvalid OPM flow command '-f {cmdargs.flow_command}'.\n") - raise SystemExit(1) + pycopm_error( + f"invalid OPM Flow command {cli_error_value(f'-f {cmdargs.flow_command}')}." + ) try: flow_result = subprocess.run( [*flow_arguments, "-h"], @@ -516,11 +528,10 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: except OSError: flow_result = None if flow_result is None or flow_result.returncode != 0: - print( - f"\nThe OPM flow executable '-f {cmdargs.flow_command}' " - "is not available or not working.\n" + pycopm_error( + f"the OPM Flow executable '-f {cmdargs.flow_command}' " + "is not available or not working." ) - raise SystemExit(1) coarsening = cmdargs.coarsening x_coarsening = cmdargs.x_coarsening y_coarsening = cmdargs.y_coarsening @@ -537,46 +548,39 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: has_refinement = bool(refinement or directional_refinement) # General and directional coarsening options are mutually exclusive if coarsening and directional_coarsening: - print( - "\nInvalid combination, either set '-c' or the '-x', '-y', and '-z' " - "flags.\n" + pycopm_error( + "invalid combination, either set '-c' or the '-x', '-y', and '-z' flags." ) - raise SystemExit(1) # General and directional refinement options are mutually exclusive if refinement and directional_refinement: - print( - "\nInvalid combination, either set '-g' or the '-rx', '-ry', and " - "'-rz' flags.\n" + pycopm_error( + "invalid combination, either set '-g' or the '-rx', '-ry', and " + "'-rz' flags." ) - raise SystemExit(1) # Coarsening and refinement are mutually exclusive if has_coarsening and has_refinement: - print("\nInvalid combination, either set coarsening or refinement options.\n") - raise SystemExit(1) + pycopm_error( + "invalid combination, either set coarsening or refinement options." + ) # Vicinity extraction, transformation, and refinement are mutually exclusive if vicinity and transformation: - print("\nInvalid combination, either set '-v' or '-d'.\n") - raise SystemExit(1) + pycopm_error("invalid combination, either set '-v' or '-d'.") if vicinity and has_refinement: - print("\nInvalid combination, either set '-v' or refinement options.\n") - raise SystemExit(1) + pycopm_error("invalid combination, either set '-v' or refinement options.") if transformation and has_refinement: - print("\nInvalid combination, either set '-d' or refinement options.\n") - raise SystemExit(1) + pycopm_error("invalid combination, either set '-d' or refinement options.") # Validate uniform coarsening and refinement levels level_pattern = re.compile(r"\d+,\d+,\d+") if coarsening and not level_pattern.fullmatch(coarsening): - print( - f"\nInvalid value '-c {coarsening}', expected three non-negative " - "integers separated by commas, e.g., '-c 2,2,1'.\n" + pycopm_error( + f"invalid value {cli_error_value(f'-c {coarsening}')}, expected three non-negative " + f"integers separated by commas, {cli_correct_value('e.g., -c 2,2,1')}." ) - raise SystemExit(1) if refinement and not level_pattern.fullmatch(refinement): - print( - f"\nInvalid value '-g {refinement}', expected three non-negative " - "integers separated by commas, e.g., '-g 2,2,1'.\n" + pycopm_error( + f"invalid value {cli_error_value(f'-g {refinement}')}, expected three non-negative " + f"integers separated by commas, {cli_correct_value('e.g., -g 2,2,1')}." ) - raise SystemExit(1) # Validate directional coarsening arrays, indices, and ranges coarsening_array_pattern = re.compile(r"\d+(?:,\d+)*") coarsening_group_pattern = re.compile( @@ -591,23 +595,21 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: coarsening_array_pattern.fullmatch(value) or coarsening_group_pattern.fullmatch(value) ): - print( - f"\nInvalid value '{option} {value}', expected a non-negative " + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, expected a non-negative " "coarsening array or positive indices and ranges separated by " - "commas.\n" + "commas." ) - raise SystemExit(1) if ":" in value: for entry in value.split(","): if ":" not in entry: continue start, end = (int(index) for index in entry.split(":")) if start > end: - print( - f"\nInvalid range '{entry}' in '{option} {value}', " - "the end must not be smaller than the start.\n" + pycopm_error( + f"invalid range '{entry}' in '{option} {value}', " + "the end must not be smaller than the start." ) - raise SystemExit(1) # Validate directional refinement arrays refinement_array_pattern = re.compile(r"\d+(?:,\d+)*") for option, value in [ @@ -616,11 +618,10 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: ("-rz", z_refinement), ]: if value and not refinement_array_pattern.fullmatch(value): - print( - f"\nInvalid value '{option} {value}', expected non-negative " - "integers separated by commas.\n" + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, expected non-negative " + "integers separated by commas." ) - raise SystemExit(1) # Validate aggregation methods aggregation_options = [ ("-a", "active_cell_methods", ["min", "max", "mode"]), @@ -636,58 +637,64 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: value = getattr(cmdargs, name).strip() methods = value.split(",") if value else [] if any(method not in valid_methods for method in methods): - print( - f"\nInvalid value '{option} {value}', valid values are " - f"{', '.join(valid_methods)}.\n" + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, valid values are " + f"{cli_correct_value(', '.join(valid_methods))}." ) - raise SystemExit(1) if len(methods) > 1 and not z_groups: - print( - f"\nInvalid value '{option} {value}', multiple aggregation " - "methods require range coarsening with '-z'.\n" + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, multiple aggregation " + "methods require range coarsening with '-z'." ) - raise SystemExit(1) if len(methods) > 1 and len(methods) != len(z_groups): - print( - f"\nInvalid value '{option} {value}', expected one aggregation " - "method for each index or range provided with '-z'.\n" + pycopm_error( + f"invalid value {cli_error_value(f'{option} {value}')}, expected one aggregation " + "method for each index or range provided with '-z'." ) - raise SystemExit(1) # Options controlling property aggregation require coarsening if not has_coarsening: if cmdargs.active_cell_methods != "mode": - print("\nInvalid combination, '-a' can only be used with coarsening.\n") - raise SystemExit(1) + pycopm_error( + f"invalid combination, {cli_error_value('-a')} can only be used with coarsening." + ) if cmdargs.discrete_aggregation_method != "mode": - print("\nInvalid combination, '-n' can only be used with coarsening.\n") - raise SystemExit(1) + pycopm_error( + f"invalid combination, {cli_error_value('-n')} can only be used with coarsening." + ) if cmdargs.continuous_aggregation_method: - print("\nInvalid combination, '-s' can only be used with coarsening.\n") - raise SystemExit(1) + pycopm_error( + f"invalid combination, {cli_error_value('-s')} can only be used with coarsening." + ) if cmdargs.transmissibility_coarsening_method != "0": - print("\nInvalid combination, '-t' can only be used with coarsening.\n") - raise SystemExit(1) + pycopm_error( + f"invalid combination, {cli_error_value('-t')} can only be used with coarsening." + ) if cmdargs.jump_thresholds: - print("\nInvalid combination, '-j' can only be used with coarsening.\n") - raise SystemExit(1) + pycopm_error( + f"invalid combination, {cli_error_value('-j')} can only be used with coarsening." + ) if cmdargs.dual_porosity_criterion: - print("\nInvalid combination, '-dual' can only be used with coarsening.\n") - raise SystemExit(1) + pycopm_error( + f"invalid combination, {cli_error_value('-dual')} can only be used with coarsening." + ) # Fluid-in-place correction is not supported for extracted submodels if vicinity and cmdargs.correct_fluid_in_place == "1": - print("\nInvalid combination, '-q' cannot be used with '-v'.\n") - raise SystemExit(1) + pycopm_error( + f"invalid combination, {cli_error_value('-q')} cannot be used " + "with {cli_error_value('-v')}." + ) # Validate pore-volume correction combinations pore_volume_correction = cmdargs.pore_volume_correction if pore_volume_correction == "1" and not (has_coarsening or vicinity): - print("\nInvalid combination, '-p 1' requires coarsening or '-v'.\n") - raise SystemExit(1) + pycopm_error( + f"invalid combination, {cli_error_value('-p 1')} requires coarsening or " + f"{cli_correct_value('-v')}." + ) if pore_volume_correction in ["2", "3", "4"] and not vicinity: - print( - f"\nInvalid combination, '-p {pore_volume_correction}' can only be " - "used with '-v'.\n" + pycopm_error( + f"invalid combination, {cli_error_value(f'-p {pore_volume_correction}')} can only be " + "used with '-v'." ) - raise SystemExit(1) # Validate the jump thresholds jump_thresholds = cmdargs.jump_thresholds if jump_thresholds: @@ -696,22 +703,20 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: except ValueError: jump_values = [] if not jump_values or any(value <= 0 for value in jump_values): - print( - f"\nInvalid value '-j {jump_thresholds}', expected positive " - "numbers separated by commas.\n" + pycopm_error( + f"invalid value {cli_error_value(f'-j {jump_thresholds}')}, expected positive " + "numbers separated by commas." ) - raise SystemExit(1) # Validate requested input-model indices requested_ijk = cmdargs.requested_ijk if requested_ijk and not re.fullmatch( r"[1-9]\d*\s*,\s*[1-9]\d*\s*,\s*[1-9]\d*", requested_ijk, ): - print( - f"\nInvalid value '-ijk {requested_ijk}', expected three positive " - "indices separated by commas, e.g., '-ijk 1,2,3'.\n" + pycopm_error( + f"invalid value {cli_error_value(f'-ijk {requested_ijk}')}, expected three positive " + f"indices separated by commas, {cli_correct_value('e.g., -ijk 1,2,3')}." ) - raise SystemExit(1) # Validate coordinate transformations number = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?" vector_transformation = re.fullmatch( @@ -723,20 +728,18 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: transformation, ) if transformation and not (vector_transformation or rotation_transformation): - print( - f"\nInvalid value '-d {transformation}', expected " + pycopm_error( + f"invalid value {cli_error_value(f'-d {transformation}')}, expected " "'translate [x,y,z]', 'scale [x,y,z]', or 'rotatexy', 'rotatexz', " - "or 'rotateyz' followed by an angle.\n" + "or 'rotateyz' followed by an angle." ) - raise SystemExit(1) if vector_transformation and vector_transformation.group(1) == "scale": coordinates = re.findall(number, transformation) if any(float(value) == 0 for value in coordinates): - print( - f"\nInvalid value '-d {transformation}', scale values cannot be " - "zero.\n" + pycopm_error( + f"invalid value {cli_error_value(f'-d {transformation}')}, scale values cannot be " + "zero." ) - raise SystemExit(1) # Validate vicinity extraction specifications region_vicinity = re.fullmatch( r"[A-Za-z][A-Za-z0-9_]*\s+[1-9]\d*(?:\s*,\s*[1-9]\d*)*", @@ -758,33 +761,30 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: if vicinity and not ( region_vicinity or polygon_vicinity or box_vicinity or diamond_vicinity ): - print( - f"\nInvalid value '-v {vicinity}', expected a region selection, an " + pycopm_error( + f"invalid value {cli_error_value(f'-v {vicinity}')}, expected a region selection, an " "'xypolygon' specification, or a well followed by 'box', " - "'diamond', or 'diamondxy'.\n" + "'diamond', or 'diamondxy'." ) - raise SystemExit(1) if polygon_vicinity: polygon_points = re.findall(polygon_point, vicinity) first_point = re.findall(number, polygon_points[0]) last_point = re.findall(number, polygon_points[-1]) if first_point != last_point: - print( - f"\nInvalid value '-v {vicinity}', the first and last " - "xypolygon points must be equal.\n" + pycopm_error( + f"invalid value {cli_error_value(f'-v {vicinity}')}, the first and last " + "xypolygon points must be equal." ) - raise SystemExit(1) if box_vicinity: intervals = re.findall( r"\[\s*(-?\d+)\s*,\s*(-?\d+)\s*\]", vicinity, ) if any(int(start) > int(end) for start, end in intervals): - print( - f"\nInvalid value '-v {vicinity}', the end of each box interval " - "must not be smaller than its start.\n" + pycopm_error( + f"invalid value {cli_error_value(f'-v {vicinity}')}, the end of each box interval " + "must not be smaller than its start." ) - raise SystemExit(1) # Validate the dual-porosity criterion dual_porosity_criterion = cmdargs.dual_porosity_criterion dual_criterion_pattern = re.compile( @@ -795,12 +795,11 @@ def _check_cmdargs(cmdargs: argparse.Namespace) -> None: if dual_porosity_criterion and not dual_criterion_pattern.fullmatch( dual_porosity_criterion ): - print( - f"\nInvalid value '-dual {dual_porosity_criterion}', expected a " + pycopm_error( + f"invalid value {cli_error_value(f'-dual {dual_porosity_criterion}')}, expected a " "static property criterion such as 'poro <= 0.1', optionally " - "followed by ', vertical TF = 0'.\n" + "followed by ', vertical TF = 0'." ) - raise SystemExit(1) def _check_flow(flow_cmdargs: str, flow_toml: str, input_file: str) -> str: @@ -828,11 +827,10 @@ def _check_flow(flow_cmdargs: str, flow_toml: str, input_file: str) -> str: next((value for value in shlex.split(flow_toml) if "flow" in value), False) ) if not flowpth: - print( - f"\nflow is not included in the configuration file {input_file}.\n" - "See the pycopm documentation.\n" + pycopm_error( + f"flow is not included in the configuration file {cli_error_value(input_file)}. " + "see the pycopm documentation." ) - raise SystemExit(1) toml_command = shlex.split(flowpth) + ["-h"] flag_command = shlex.split(flow_cmdargs) + ["-h"] @@ -855,15 +853,14 @@ def flow_exists(command: list[str]) -> bool: flag_ok = flow_exists(flag_command) if not (toml_ok or flag_ok): - print( - f"\nThe OPM flow executable '{flowpth}' is not found; " - "try to install it following the pycopm documentation.\nIf it was " + pycopm_error( + f"the OPM Flow executable '{flowpth}' is not found; " + "try to install it following the pycopm documentation. If it was " "built from source, then either add the folder location to your path, " - "or write the path\nto flow in the toml configuration file " - "(e.g., flow = '/home/pycopm/build/opm-simulators/bin/flow'),\n" - "or using the command flag -f or --flow.\n" + "or write the path to flow in the TOML configuration file " + "(e.g., flow = '/home/pycopm/build/opm-simulators/bin/flow'), " + "or using the command flag -f or --flow." ) - raise SystemExit(1) if toml_ok: flow_command = flow_toml else: diff --git a/src/pycopm/template_scripts/common/plot_post.mako b/src/pycopm/template_scripts/common/plot_post.mako index a485dee..b9f17d0 100644 --- a/src/pycopm/template_scripts/common/plot_post.mako +++ b/src/pycopm/template_scripts/common/plot_post.mako @@ -16,6 +16,14 @@ from opm.io.ecl import EGrid as OpmGrid from opm.io.ecl import EclFile as OpmFile from opm.io.ecl import ESmry as OpmSummary +from pycopm.utils.terminal import ( + pycopm_error, + pycopm_info, + pycopm_tip, + pycopm_success, + pycopm_warning, +) + def visualizeData(): """Visualize ensemble time series, saturation functions, parameters, and diagnostics.""" @@ -426,7 +434,6 @@ def visualizeData(): normalized_mean = plot_cumulative(fcum, rfcum, field_types, "Cumulative final mass [%]", "cumulative_mismatch_mass_normalized", [19.06, 4.88], normalize=total_mass, mass_labels=True) if total_mass else np.nan goal = 4.88 - normalized_mean - print(f"\nThe postprocessing files have been written to {postprocessing}") errors_file = postprocessing / "errors.txt" with errors_file.open("w", encoding="utf8") as stream: if num_realizations > 1: @@ -445,14 +452,16 @@ def visualizeData(): single_error = error_ens[0][0] if np.isfinite(error_ens[0][0]) else np.nan stream.write(f"Mismatch (standard simulation from opm-test deck): {error_standard:.4e}\n") stream.write(f"Mismatch (single simulation): {single_error:.4e}\n") - print(errors_file.read_text(encoding="utf8"), end="") + pycopm_info("") + print(errors_file.read_text(encoding="utf8")[:-2]) %if model_name=='drogon': - print(f"Difference (webviz - pycopm): {goal:.2f} (a positive number (percentage) is the goal)") - print(f"See {postprocessing}/cumulative_mismatch_mass_normalized_ite-{selected_iterations[-1]}.png\n") + pycopm_info(f"difference (webviz - pycopm): {goal:.2f} (a positive number (percentage) is the goal), " + f"see {postprocessing}/cumulative_mismatch_mass_normalized_ite-{selected_iterations[-1]}.png") if goal <= 0: - print("To improve the goal, for example, run a history-matching study (mode = 'ert') and increase the number") - print("of ensembles (mep), iterations (--weights), distribution type/intervals, or change the random seed (rds).\n") + pycopm_tip("to improve the goal, for example, run a history-matching study (mode = 'ert') and increase the number " + "of ensembles (mep), iterations (--weights), distribution type/intervals, or change the random seed (rds).") %endif + pycopm_success(f"the postprocessing files have been written to ", postprocessing, []) if __name__ == "__main__": diff --git a/src/pycopm/template_scripts/common/time_eval.mako b/src/pycopm/template_scripts/common/time_eval.mako index 513fb02..3efc995 100644 --- a/src/pycopm/template_scripts/common/time_eval.mako +++ b/src/pycopm/template_scripts/common/time_eval.mako @@ -19,7 +19,7 @@ def get_time_simulation(): solData.append(row) with open("time_sim.txt", 'w') as file: file.write(f'{float(solData[-23][-1])}') - except: + except ValueError: print(f'Problem with the DBG file') if __name__ == "__main__": diff --git a/src/pycopm/utils/coarsening.py b/src/pycopm/utils/coarsening.py index f3ea625..f724ba9 100644 --- a/src/pycopm/utils/coarsening.py +++ b/src/pycopm/utils/coarsening.py @@ -35,6 +35,7 @@ write_reference_to_coarse_map, ) from pycopm.utils.input_values import parse_axis_modifications +from pycopm.utils.terminal import pycopm_error, pycopm_info @dataclass(slots=True) @@ -148,7 +149,7 @@ def create_coarsening_maps( elif comparison_operator == "!=": matrix_mask[active_cells] = property_values == comparison_value else: - raise ValueError(f"Unknown criterion for non-net cells: {dual_criterion}") + pycopm_error(f"unknown criterion for non-net cells: {dual_criterion}") directions = ("x", "y", "z") original_sizes = ( dck.original_nx, @@ -411,7 +412,7 @@ def coarsen_properties( coarsening: CoarseningMaps, modified_deck: list[str], wellcind: list[int], -) -> tuple[NDArray, NDArray, NDArray]: +) -> tuple[NDArray, NDArray, NDArray, list[str]]: """Aggregate reservoir properties onto the coarsened grid. Continuous properties use their configured or property-specific aggregation; @@ -431,8 +432,10 @@ def coarsen_properties( Returns ------- - cluster_minimum, cluster_maximum, removal_mask - Activity summaries and the mask used to remove depth-jump cells.""" + cluster_minimum, cluster_maximum, removal_mask, generated_files + Activity summaries, the mask used to remove depth-jump cells, + and the generated include file names.""" + generated_files = [] actnum = np.zeros(dck.original_cell_count, dtype=int) top_depths = np.full(dck.original_cell_count, np.nan) base_depths = np.full(dck.original_cell_count, np.nan) @@ -626,7 +629,7 @@ def coarsen_properties( dual_pore_volume = np.array([], dtype=float) dck.output_porv = matrix_pore_volume - print("Coarsening continuous quantities (e.g., PORO)") + pycopm_info("coarsening continuous quantities (e.g., PORO)") number_values = dck.output_nx * dck.output_ny * dck.output_nz dual_properties = ("porv", "poro", "tranx", "trany", "tranz") zero_dual_properties = ("permx", "permy", "permz") @@ -1190,6 +1193,7 @@ def coarsen_properties( ) if property_inlined: continue + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") if dck.dual_porosity_criterion and property_name in [ "poro", "tranz", @@ -1236,13 +1240,17 @@ def coarsen_properties( False, "_DUAL_TMP_PYCOPM", ) - _compact_permeability_properties(dck, permx, permy, permz, modified_deck) + removed = _compact_permeability_properties(dck, permx, permy, permz, modified_deck) + if "PERMY" in removed: + generated_files.remove(f"{dck.include_prefix}PERMY.INC") + if "PERMZ" in removed: + generated_files.remove(f"{dck.include_prefix}PERMZ.INC") show_progress = sys.stdout.isatty() if show_progress: bar_ctx = alive_bar(len(dck.regions_keywords + dck.grids_keywords), bar="fish") else: bar_ctx = nullcontext() - print("Coarsening discrete quantities (e.g., SATNUM)") + pycopm_info("coarsening discrete quantities (e.g., SATNUM)") with bar_ctx as bar_animation: for property_name in dck.regions_keywords + dck.grids_keywords: if show_progress: @@ -1310,6 +1318,7 @@ def coarsen_properties( modified_deck, not dck.dual_porosity_criterion, ) + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") if property_inlined: continue if dck.dual_porosity_criterion: @@ -1339,7 +1348,9 @@ def coarsen_properties( write_reference_to_coarse_map(dck, np.array(coarsening.reference_to_coarse)) - return cluster_minimum, cluster_maximum, removal_mask + generated_files.append(f"{dck.original_deck_name}_OPERNUM_PYCOPM_REFTOCOA.INC") + + return cluster_minimum, cluster_maximum, removal_mask, generated_files def _interleave_dual_property( @@ -1401,8 +1412,9 @@ def _compact_permeability_properties( permy: NDArray, permz: NDArray, modified_deck: list[str], -) -> None: +) -> list[str]: """Use COPY and MULTIPLY if PERMY and PERMZ can be generated from PERMX.""" + removed: list[str] = [] copy_permy = np.array_equal(permx, permy) copy_permz = False output_path = Path(dck.output_directory) @@ -1425,7 +1437,7 @@ def _compact_permeability_properties( include_line = f"'{dck.include_prefix}PERMZ.INC' /\n" include_statements.append(_find_include_statement(modified_deck, include_line)) if not include_statements: - return + return removed insertion_index = min(start_index for start_index, _ in include_statements) for start_index, end_index in sorted(include_statements, reverse=True): del modified_deck[start_index:end_index] @@ -1449,11 +1461,14 @@ def _compact_permeability_properties( permy_path.unlink(missing_ok=True) permy_path = output_path / f"{dck.include_prefix}PERMY_DUAL_TMP_PYCOPM.INC" permy_path.unlink(missing_ok=True) + removed.append("PERMY") if copy_permz: permz_path = output_path / f"{dck.include_prefix}PERMZ.INC" permz_path.unlink(missing_ok=True) permz_path = output_path / f"{dck.include_prefix}PERMZ_DUAL_TMP_PYCOPM.INC" permz_path.unlink(missing_ok=True) + removed.append("PERMZ") + return removed def redistribute_removed_pore_volume( @@ -1514,8 +1529,8 @@ def redistribute_removed_pore_volume( distance = 0 offset += 1 if not neighbor_indices: - raise ValueError( - "No active cell found to receive pore volume from " + pycopm_error( + "no active cell found to receive pore volume from " f"cluster {cluster_id}" ) pore_volume_increment = grouped_pore_volume[cluster_id] / len(neighbor_indices) @@ -1721,7 +1736,7 @@ def build_dual_porosity_grid( bar_ctx = alive_bar(cells_per_layer * dck.output_nz, bar="fish") else: bar_ctx = nullcontext() - print("Handling the dual connectivity") + pycopm_info("processing the dual connectivity") with bar_ctx as bar_animation: for row_index in range(dck.original_ny): for column_index in range(dck.original_nx): @@ -1924,7 +1939,9 @@ def _collect_removed_zcorn_indices( return removal_indices -def map_nnc_transmissibilities(dck: ConfigViaDeck, coarsening: CoarseningMaps) -> None: +def map_nnc_transmissibilities( + dck: ConfigViaDeck, coarsening: CoarseningMaps +) -> list[str]: """Map original non-neighbouring transmissibilities to the coarse grid. Connections that become Cartesian neighbours are accumulated in ``TRANX`` or @@ -1935,7 +1952,13 @@ def map_nnc_transmissibilities(dck: ConfigViaDeck, coarsening: CoarseningMaps) - dck Deck configuration and source NNC data. coarsening - Coarse mapping updated with transmissibilities and NNC text.""" + Coarse mapping updated with transmissibilities and NNC text. + + Returns + ------- + generated_files + Names of the written include files.""" + generated_files = [] output_directory = Path(dck.output_directory) original_grid = OpmFile(f"{dck.input_deck_name}.EGRID") coarsened_init = OpmFile(str(output_directory / f"{dck.output_deck_name}.INIT")) @@ -1958,7 +1981,6 @@ def map_nnc_transmissibilities(dck: ConfigViaDeck, coarsening: CoarseningMaps) - first_cell_indices = first_connection_cells[:connection_count] - 1 second_cell_indices = second_connection_cells[:connection_count] - 1 connection_transmissibilities = connection_transmissibilities[:connection_count] - original_cell_count = dck.original_nx * dck.original_ny * dck.original_nz coarsened_porv = np.asarray(coarsened_init["PORV"], dtype=float).reshape(-1) tranx_c = np.zeros(coarsened_porv.size, dtype=float) trany_c = np.zeros(coarsened_porv.size, dtype=float) @@ -2011,8 +2033,6 @@ def map_nnc_transmissibilities(dck: ConfigViaDeck, coarsening: CoarseningMaps) - second_output_j = output_j_map[second_cell_j] second_output_k = output_k_map[second_cell_k] coarsening_mask = np.asarray(coarsening.matrix_mask).reshape(-1) - if coarsening_mask.size < original_cell_count: - raise ValueError("The coarsening mask is smaller than the original grid") first_cell_mask = coarsening_mask[first_cell_indices] second_cell_mask = coarsening_mask[second_cell_indices] different_continuum = first_cell_mask != second_cell_mask @@ -2132,12 +2152,12 @@ def map_nnc_transmissibilities(dck: ConfigViaDeck, coarsening: CoarseningMaps) - bar_ctx = alive_bar(connection_count, bar="fish") else: bar_ctx = nullcontext() - print("Handling the dual connectivity") - print("Processing non-neighbouring transmissibilities (input model)") + pycopm_info("processing non-neighbouring transmissibilities NNC (input model)") with bar_ctx as bar_animation: if show_progress and connection_count: bar_animation(connection_count) if not dck.dual_porosity_criterion: + generated_files.append(f"{dck.include_prefix}TRANX.INC") property_path = output_directory / f"{dck.include_prefix}TRANX.INC" write_property(property_path, "TRANX", tranx_c, num_dig) property_path = output_directory / f"{dck.include_prefix}TRANY.INC" @@ -2145,6 +2165,7 @@ def map_nnc_transmissibilities(dck: ConfigViaDeck, coarsening: CoarseningMaps) - else: coarsening.coarse_tranx = tranx_c coarsening.coarse_trany = trany_c + return generated_files def create_coarsening_map(cfg: ConfigViaTOML) -> NDArray: @@ -2281,8 +2302,6 @@ def _read_satnum( satnum = np.load(reference_folder / "satnum.npy") elif cfg.satnum_generation_method > 0: satnum_files = {1: "satnum_5.out", 3: "satnum_60.out"} - if cfg.satnum_generation_method not in satnum_files: - raise ValueError("satnum_generation_method must be 0, 1, or 3") satnum_values = [] with open( reference_folder / satnum_files[cfg.satnum_generation_method], @@ -2320,7 +2339,7 @@ def coarsen_and_write_properties(cfg: ConfigViaTOML, coa_map: NDArray) -> int: / cfg.model_name / cfg.reference_case_name ) - print("Coarsening and writing the static properties") + pycopm_info("coarsening and writing the static properties") preprocessing_path = Path(cfg.output_directory) / "preprocessing" num_cells = cfg.output_nx * cfg.output_ny * cfg.output_nz num_dig = cfg.significant_digits diff --git a/src/pycopm/utils/generate_decks.py b/src/pycopm/utils/generate_decks.py index 1fc9b35..f07df00 100644 --- a/src/pycopm/utils/generate_decks.py +++ b/src/pycopm/utils/generate_decks.py @@ -39,6 +39,15 @@ refine_grid, refine_properties, ) +from pycopm.utils.terminal import ( + cli_correct_value, + cli_error_value, + cli_info_value, + pycopm_error, + pycopm_info, + pycopm_success, + pycopm_warning, +) from pycopm.utils.transformation import ( transform_grid, transform_properties, @@ -67,9 +76,9 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: Parsed command arguments used to build coarsening or refinement maps.""" output_directory = Path(dck.output_directory) source_deck = Path(f"{dck.input_deck_path}.DATA") + generated_files = [] if dck.requested_ijk[0]: dck.requested_ijk = [int(value) for value in dck.requested_ijk[0].split(",")] - dck.execution_mode = "deck" if not dck.output_deck_name: dck.output_deck_name = f"{dck.input_deck_name}_PYCOPM" flags_dry_run = ( @@ -96,10 +105,10 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: "".join(f"{deck_line}\n" for deck_line in modified_deck), encoding="utf8", ) - print( - f"\nTemporal {dry_run_deck.name} from {source_deck} for the initial " - "run to generate the grid (.EGRID), static (.INIT), and initial " - "(.UNRST) properties\n" + pycopm_info( + f"temporary {cli_info_value(dry_run_deck.name)} created from " + f"{cli_info_value(str(source_deck))} for the initial run that generates " + "the grid (.EGRID), static (.INIT), and initial (.UNRST) properties." ) subprocess.run( [ @@ -114,12 +123,16 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: ) dry_run_deck.unlink(missing_ok=True) copy2(source_deck, dry_run_deck) - print(f"\nCloning {source_deck} to {dry_run_deck.name} \n") + pycopm_info( + f"cloning {cli_info_value(str(source_deck))} to " + f"{cli_info_value(dry_run_deck.name)}." + ) else: copy2(source_deck, dry_run_deck) - print( - f"\nCloning {source_deck} to {dry_run_deck.name} for the initial " - "dry run to generate the grid (.EGRID) and static (.INIT) properties\n" + pycopm_info( + f"cloning {cli_info_value(str(source_deck))} to " + f"{cli_info_value(dry_run_deck.name)} for the initial dry run that " + "generates the grid (.EGRID) and static (.INIT) properties." ) subprocess.run( [dck.flow_command, dry_run_deck.name, *flags_dry_run.split()], @@ -130,25 +143,27 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: output_file = output_directory / f"{dry_run_name}{output_type}" if not output_file.is_file(): if output_type == ".INIT": - print( - f"\nThe {output_file} is not found, try adding the keyword INIT " - f"in the GRID section in the original deck {dck.input_deck_name}.DATA\n" + pycopm_error( + f"required file {cli_error_value(str(output_file))} was not found; " + f"add {cli_correct_value('INIT')} to the GRID section of " + f"{cli_error_value(f'{dck.input_deck_name}.DATA')}." ) elif output_type == ".EGRID": - print( - f"\nThe {output_file} is not found, try removing the keyword " - f"GRIDFILE in the GRID section in the original deck {dck.input_deck_name}" - ".DATA\n" + pycopm_error( + f"required file {cli_error_value(str(output_file))} was not found; " + f"remove {cli_error_value('GRIDFILE')} from the GRID section of " + f"{cli_error_value(f'{dck.input_deck_name}.DATA')}." ) else: - print( - f"\nThe {output_file} is not found, check the input deck " - f"{dck.input_deck_name}.DATA\n" + pycopm_error( + f"required file {cli_error_value(str(output_file))} was not found; " + f"check input deck {cli_error_value(f'{dck.input_deck_name}.DATA')}." ) - sys.exit() - print( - f"\nThe initial/dry run of {dck.input_deck_name}.DATA succeeded " - f"(see {output_directory}/)" + pycopm_success( + f"initial dry run succeeded; 3 files ({dry_run_name}.DATA, .EGRID, and .INIT)\n" + " written to ", + str(output_directory), + [], ) if dck.execution_mode in ("prep_deck", "deck", "deck_dry", "all"): dck.original_deck_name = dck.input_deck_name @@ -156,23 +171,21 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: for output_type in output_types: output_file = Path(f"{dck.input_deck_name}{output_type}") if not output_file.is_file(): - print( - f"\nThe {output_file} is not found, try running pycopm with " - "-m prep_deck and without -ijk" + pycopm_error( + f"required file {cli_error_value(str(output_file))} was not found; " + f"run pycopm with {cli_correct_value('-m prep_deck')} and without " + f"{cli_error_value('-ijk')}." ) - sys.exit() dck.props_keywords = ["permx", "permy", "permz", "poro"] dck.base_keywords = dck.props_keywords + ["grid"] if dck.refinement_enabled: - print("\nInitializing pycopm to generate the refined files, please wait.") + pycopm_info("generating the refined files, please wait...") elif dck.vicinity_specification: - print("\nInitializing pycopm to generate the submodel files, please wait.") + pycopm_info("generating the submodel files, please wait...") elif dck.grid_transformation: - print( - "\nInitializing pycopm to generate the transformed files, please wait." - ) + pycopm_info("generating the transformed files, please wait...") else: - print("\nInitializing pycopm to generate the coarsened files, please wait.") + pycopm_info("generating the coarsened files, please wait...") _initialize_deck_data(dck) dck.original_cell_count = dck.original_nx * dck.original_ny * dck.original_nz if dck.transmissibility_coarsening_method > 0: @@ -190,33 +203,41 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: if not dck.grid_transformation: _create_index_mappings(dck, vicinity, refinement, coarsening) if dck.requested_ijk[0]: - print( - dck.original_to_output_i[dck.requested_ijk[0][0]], - dck.original_to_output_j[dck.requested_ijk[0][1]], - dck.original_to_output_k[dck.requested_ijk[0][2]], + pycopm_success( + f"mapped indices: " + f"{cli_info_value(str(dck.original_to_output_i[dck.requested_ijk[0]]))}, " + f"{cli_info_value(str(dck.original_to_output_j[dck.requested_ijk[1]]))}, " + f"{cli_info_value(str(dck.original_to_output_k[dck.requested_ijk[2]]))}", + "", + [], ) - sys.exit() + sys.exit(0) modified_deck, wellcind = process_deck(dck, vicinity) - print("Processing the mappings") + pycopm_info("processing the mappings") cr, zc = np.array([]), np.array([]) if dck.grid_transformation: - transform_properties(dck, modified_deck) + generated_files.extend(transform_properties(dck, modified_deck)) transform_grid(dck) elif dck.refinement_enabled: - refine_properties(dck, refinement, modified_deck) + generated_files.extend(refine_properties(dck, refinement, modified_deck)) refine_grid(dck, refinement) elif dck.vicinity_specification: - map_vicinity_properties(dck, vicinity, modified_deck) + generated_files.extend( + map_vicinity_properties(dck, vicinity, modified_deck) + ) extract_vicinity_grid(dck, vicinity) apply_boundary_pore_volume_correction(dck, vicinity) write_porv(dck, modified_deck) else: - cluster_minimum, cluster_maximum, removed_cells = coarsen_properties( - dck, - coarsening, - modified_deck, - wellcind, + cluster_minimum, cluster_maximum, removed_cells, file_names = ( + coarsen_properties( + dck, + coarsening, + modified_deck, + wellcind, + ) ) + generated_files.extend(file_names) if dck.pore_volume_correction == 1: redistribute_removed_pore_volume( dck, @@ -226,7 +247,9 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: removed_cells, ) write_porv(dck, modified_deck) + generated_files.append(f"{dck.include_prefix}PORV.INC") cr, zc = coarsen_corner_point_grid(dck, coarsening) + generated_files.append(f"{dck.include_prefix}GRID.INC") generated_deck = output_directory / f"{dck.output_deck_name}.DATA" generated_deck.write_text( "".join(f"{deck_line}\n" for deck_line in modified_deck), @@ -239,8 +262,10 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: and dck.egrid_file.count("NNC1") and dck.transmissibility_coarsening_method > 0 ): - print("\nCall OPM Flow for a dry run of the generated model.\n") - print("\nThis is needed for the nnctrans, please wait.\n") + pycopm_info( + "calling OPM Flow for a dry run of the generated model, " + "needed for the NNC transmissibilities" + ) subprocess.run( [dck.flow_command, generated_deck.name, *flags_dry_run.split()], cwd=output_directory, @@ -251,9 +276,12 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: OpmFile(str(generated_grid)).count("NNC1") or OpmFile(f"{dck.input_deck_name}.EGRID").count("NNC1") ) and dck.transmissibility_coarsening_method > 0: - map_nnc_transmissibilities(dck, coarsening) + generated_files.extend(map_nnc_transmissibilities(dck, coarsening)) else: - print("\nNo nnctrans found.") + pycopm_warning("no NNC transmissibilities were found.") + generated_grid.unlink() + tmp = output_directory / f"{dck.output_deck_name}.INIT" + tmp.unlink() if dck.coarsening_enabled: if dck.dual_porosity_criterion: cr, zc = build_dual_porosity_grid(dck, coarsening, cr, zc) @@ -287,6 +315,7 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: output_directory / f"{dck.include_prefix}NNC.INC", "".join(coarsening.nnc_text), ) + generated_files.append(f"{dck.include_prefix}NNC.INC") elif coarsening.nnc_text != "NNC\n": grid_include_index = modified_deck.index( f"'{dck.include_prefix}GRID.INC' /\n" @@ -304,28 +333,34 @@ def create_deck(dck: ConfigViaDeck, cmdargs: argparse.Namespace) -> None: output_directory / f"{dck.include_prefix}NNC.INC", "".join(coarsening.nnc_text), ) - print( - f"\nThe generation of files succeeded, see {generated_deck} and " - f"{output_directory}/{dck.include_prefix}*.INC\n" + generated_files.append(f"{dck.include_prefix}NNC.INC") + generated_files.append(f"{dck.output_deck_name}.DATA") + pycopm_success( + "", + str(output_directory), + sorted(set(generated_files)), ) if dck.execution_mode in ("deck_dry", "dry", "all"): - print("\nCall OPM Flow for a dry run of the generated model.\n") + pycopm_info("calling OPM Flow for a dry run of the generated model.") completed_process = subprocess.run( [dck.flow_command, f"{dck.output_deck_name}.DATA", *flags_dry_run.split()], cwd=output_directory, check=False, ) if completed_process.returncode != 0: - print( - "\nThe dry run of the generated model " - f"{output_directory}/{dck.output_deck_name}.DATA failed. Check the Flow " - "output in the terminal for the error, which might be possible to " - f"fix by correcting the input deck {source_deck} or the generated " - "deck; otherwise, please raise an issue at " - "https://github.com/cssr-tools/pycopm/issues" + pycopm_error( + "the dry run of " + f"{cli_error_value(str(output_directory / (dck.output_deck_name + '.DATA')))} " + "failed. Check the OPM Flow output in the terminal. Correct the input " + f"deck {cli_error_value(str(source_deck))} or the generated deck; otherwise, " + "raise an issue at https://github.com/cssr-tools/pycopm/issues." ) else: - print(f"\nThe dryrun results have been written to {output_directory}/") + pycopm_success( + "dry-run results of the generated deck by pycopm were written to ", + str(output_directory), + [], + ) def _correct_fluid_in_place(dck: ConfigViaDeck, modified_deck: list[str]) -> None: @@ -376,9 +411,9 @@ def _correct_fluid_in_place(dck: ConfigViaDeck, modified_deck: list[str]) -> Non "".join(f"{deck_line}\n" for deck_line in deckcorr), encoding="utf8", ) - print( - f"\nRunning {one_step_deck} and {correction_deck} to correct the " - "pore volume\n" + pycopm_info( + f"running {cli_info_value(str(one_step_deck))} and " + f"{cli_info_value(str(correction_deck))} to correct the pore volume." ) subprocess.run( [dck.flow_command, str(correction_deck), *flags_one_step.split()], @@ -446,7 +481,9 @@ def _correct_fluid_in_place(dck: ConfigViaDeck, modified_deck: list[str]) -> Non corrected_porv[np.isnan(corrected_porv)] = 0 dck.output_porv = corrected_porv write_porv(dck, modified_deck) - print(f"\nRunning {correction_deck} with the corrected pore volume\n") + pycopm_info( + f"running {cli_info_value(str(correction_deck))} with the corrected pore volume." + ) subprocess.run( [dck.flow_command, str(correction_deck), *flags_one_step.split()], check=False, diff --git a/src/pycopm/utils/input_values.py b/src/pycopm/utils/input_values.py index a696557..f7b68ae 100644 --- a/src/pycopm/utils/input_values.py +++ b/src/pycopm/utils/input_values.py @@ -1,18 +1,120 @@ # SPDX-FileCopyrightText: 2024-2026 NORCE Research AS # SPDX-License-Identifier: GPL-3.0 -# pylint: disable=R1702 - +# pylint: disable=R0912,R0913,R0914,R0915,R0917,R1702 """Create configuration objects from command-line arguments and TOML files.""" import argparse +import datetime as dt +import math import tomllib from pathlib import Path +from typing import Any, TypeGuard import numpy as np from numpy.typing import NDArray from opm.io.ecl import EGrid as OpmGrid from pycopm.config.config import ConfigViaDeck, ConfigViaTOML +from pycopm.utils.terminal import ( + cli_correct_value, + cli_error_value, + cli_warning_value, + pycopm_error, + pycopm_warning, +) + +TOML_KEYS = { + "flow_command", + "model_name", + "execution_mode", + "ensemble_size", + "max_parallel_realizations", + "max_realization_runtime_seconds", + "min_successful_realizations", + "random_seed", + "saturation_function_method", + "pore_volume_correction", + "initialization_method", + "observation_relative_errors", + "observation_minimum_errors", + "history_matching_end_date", + "ert_arguments", + "let_parameters", + "rock_property_settings", + "x_coarsening", + "y_coarsening", + "z_coarsening", + "satnum_generation_method", + "cleanup_file_suffixes", +} + +INTERNAL_KEYS = { + "output_directory", + "resource_directory", + "reference_case_name", + "use_let_tables", + "significant_digits", + "original_nx", + "original_ny", + "original_nz", + "output_nx", + "output_ny", + "output_nz", + "original_cell_count", + "original_to_output_i", + "original_to_output_j", + "original_to_output_k", +} + +MODEL_AXIS_LENGTHS = { + "norne": {"x_coarsening": 47, "y_coarsening": 113, "z_coarsening": 23}, + "drogon": {"x_coarsening": 47, "y_coarsening": 74, "z_coarsening": 32}, +} + +MODEL_LET_NAMES = { + "norne": [ + "lw", + "ew", + "tw", + "lo", + "eo", + "to", + "lg", + "eg", + "tg", + "log", + "eog", + "tog", + "lmlto", + "emlto", + "tmlto", + "lmltg", + "emltg", + "tmltg", + ], + "drogon": [ + "lw", + "ew", + "tw", + "lo", + "eo", + "to", + "lg", + "eg", + "tg", + "log", + "eog", + "tog", + "lmlto", + "lmltg", + "kwow", + "kwoo", + "kwgw", + "kwgg", + ], +} + +ROCK_PROPERTY_NAMES = {"PERMX", "PERMY", "PERMZ"} def create_deck_config(cmdargs: argparse.Namespace) -> ConfigViaDeck: @@ -20,13 +122,13 @@ def create_deck_config(cmdargs: argparse.Namespace) -> ConfigViaDeck: Parameters ---------- - cmdargs - Arguments returned by the command-line parser. + cmdargs : argparse.Namespace + Command-line arguments for the deck-based workflow. Returns ------- ConfigViaDeck - Configuration for a deck-based workflow.""" + Configuration populated from the command-line values.""" return ConfigViaDeck( output_directory=str(Path(cmdargs.output_directory).expanduser().resolve()), flow_command=cmdargs.flow_command, @@ -70,15 +172,14 @@ def create_deck_config(cmdargs: argparse.Namespace) -> ConfigViaDeck: def parse_axis_modifications(uniform: str, localized: list) -> tuple[NDArray, list]: """Parse uniform or axis-specific grid modifications. - Uniform input contains one value for each axis. Axis-specific input can contain - explicit arrays; coarsening also accepts one-based indices and inclusive ranges - such as ``2:4,7``. + Uniform input contains one value for each axis. Axis-specific coarsening + also accepts one-based indices and inclusive ranges such as ``2:4,7``. Parameters ---------- - uniform + uniform : str Comma-separated x, y, and z modification values. - localized + localized : list Axis-specific specifications in x, y, and z order. Returns @@ -119,36 +220,666 @@ def parse_axis_modifications(uniform: str, localized: list) -> tuple[NDArray, li return cijk, refs +def _is_finite_number(value: Any) -> TypeGuard[int | float]: + """Check whether a value is a finite non-Boolean number. + + Parameters + ---------- + value : Any + Value to inspect. + + Returns + ------- + bool + Whether the value is a finite integer or floating-point number.""" + return ( + isinstance(value, (int, float)) + and not isinstance(value, bool) + and math.isfinite(value) + ) + + +def _is_integer(value: Any) -> TypeGuard[int]: + """Check whether a value is a non-Boolean integer. + + Parameters + ---------- + value : Any + Value to inspect. + + Returns + ------- + bool + Whether the value is an integer and not a Boolean.""" + return isinstance(value, int) and not isinstance(value, bool) + + +def _add_validation_error(errors: list[str], message: str) -> None: + """Add a TOML validation error. + + Parameters + ---------- + errors : list[str] + Validation messages collected during the current validation pass. + message : str + Human-readable validation message.""" + errors.append(message) + + +def _warn(message: str) -> None: + """Display a TOML validation warning. + + Parameters + ---------- + message : str + Human-readable warning passed to the shared terminal helper.""" + pycopm_warning(message) + + +def _validate_string(cfg_file: dict[str, Any], key: str, errors: list[str]) -> bool: + """Check that a TOML variable is a non-empty string. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + key : str + Configuration variable name. + errors : list[str] + Validation messages collected during the current validation pass. + + Returns + ------- + bool + Whether the variable is present and contains a non-empty string.""" + if key not in cfg_file: + return False + value = cfg_file[key] + if not isinstance(value, str) or not value.strip(): + _add_validation_error( + errors, + f"variable {cli_error_value(key)} has invalid value " + f"{cli_error_value(str(value))}, expected " + f"{cli_correct_value('a non-empty string')}.", + ) + return False + return True + + +def _validate_integer( + cfg_file: dict[str, Any], key: str, errors: list[str], minimum: int = 0 +) -> bool: + """Check that a TOML variable is an integer within its lower bound. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + key : str + Configuration variable name. + errors : list[str] + Validation messages collected during the current validation pass. + minimum : int, optional + Inclusive lower bound for accepted values. + + Returns + ------- + bool + Whether the variable is present and satisfies the integer constraint.""" + if key not in cfg_file: + return False + value = cfg_file[key] + if not _is_integer(value) or value < minimum: + _add_validation_error( + errors, + f"variable {cli_error_value(key)} has invalid value " + f"{cli_error_value(str(value))}, expected an integer " + f"greater than or equal to {minimum}.", + ) + return False + return True + + +def _validate_number_array( + cfg_file: dict[str, Any], + key: str, + errors: list[str], + *, + length: int | None = None, + minimum: float | None = None, + maximum: float | None = None, +) -> bool: + """Check the shape and values of a numeric TOML array. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + key : str + Configuration variable name. + errors : list[str] + Validation messages collected during the current validation pass. + length : int | None, optional + Required number of entries. + minimum : float | None, optional + Inclusive lower bound for every entry. + maximum : float | None, optional + Inclusive upper bound for every entry. + + Returns + ------- + bool + Whether the variable is present and satisfies all array constraints.""" + if key not in cfg_file: + return False + value = cfg_file[key] + if not isinstance(value, list): + _add_validation_error(errors, f"variable '{key}' must be an array.") + return False + valid = True + if length is not None and len(value) != length: + _add_validation_error( + errors, f"variable '{key}' has {len(value)} entries, expected {length}." + ) + valid = False + for index, entry in enumerate(value): + if not _is_finite_number(entry): + _add_validation_error( + errors, + f"variable {cli_error_value(f'{key}[{index}]')} has invalid value " + f"{cli_error_value(str(entry))}, expected " + f"{cli_correct_value('a finite number')}.", + ) + valid = False + elif minimum is not None and entry < minimum: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{key}[{index}]')} has invalid value " + f"{cli_error_value(str(entry))}, expected a value greater than or " + f"equal to {cli_correct_value(str(minimum))}.", + ) + valid = False + elif maximum is not None and entry > maximum: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{key}[{index}]')} has invalid value " + f"{cli_error_value(str(entry))}, expected a value less than or " + f"equal to {cli_correct_value(str(maximum))}.", + ) + valid = False + return valid + + +def _validate_coarsening( + cfg_file: dict[str, Any], key: str, expected_length: int, errors: list[str] +) -> None: + """Validate one model-specific axis coarsening array. + + The array must contain non-negative integers, match the number of grid + boundaries for the selected model, and retain both outer boundaries. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + key : str + Name of the x, y, or z coarsening variable. + expected_length : int + Required number of entries for the selected reference model. + errors : list[str] + Validation messages collected during the current validation pass.""" + if key not in cfg_file: + return + values = cfg_file[key] + if not isinstance(values, list): + _add_validation_error(errors, f"variable '{key}' must be an array.") + return + if len(values) != expected_length: + _add_validation_error( + errors, + f"variable '{key}' has {len(values)} entries, expected {expected_length}.", + ) + for index, value in enumerate(values): + if not _is_integer(value) or value < 0: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{key}[{index}]')} has invalid value " + f"{cli_error_value(str(value))}, " + "expected a non-negative integer.", + ) + if values and values[0] != 0: + _add_validation_error( + errors, f"variable '{key}[0]' must be 0 to retain the first grid boundary." + ) + if values and values[-1] != 0: + _add_validation_error( + errors, f"variable '{key}[-1]' must be 0 to retain the last grid boundary." + ) + + +def _validate_let_parameters( + cfg_file: dict[str, Any], model: str | None, errors: list[str] +) -> None: + """Validate the ordered LET-parameter definitions. + + Each row contains a coefficient name, initial value, estimation flag, + distribution name, lower bound, and upper bound. The order is checked because + downstream table generation addresses coefficients by position. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + model : str | None + Normalized reference-model name, when valid. + errors : list[str] + Validation messages collected during the current validation pass.""" + if "let_parameters" not in cfg_file: + return + rows = cfg_file["let_parameters"] + if not isinstance(rows, list): + _add_validation_error( + errors, "variable 'let_parameters' must be an array of arrays." + ) + return + expected_names = MODEL_LET_NAMES.get(model) if model is not None else None + if expected_names and len(rows) != len(expected_names): + _add_validation_error( + errors, + f"variable 'let_parameters' has {len(rows)} rows, expected " + f"{len(expected_names)} for model '{model}'.", + ) + seen: set[str] = set() + for index, row in enumerate(rows): + name = f"let_parameters[{index}]" + if not isinstance(row, list) or len(row) != 6: + size = len(row) if isinstance(row, list) else type(row).__name__ + _add_validation_error( + errors, + f"variable {cli_error_value(name)} has invalid shape " + f"{cli_error_value(str(size))}, expected " + f"{cli_correct_value('6 entries')}.", + ) + continue + coefficient, initial, estimated, distribution, lower, upper = row + if not isinstance(coefficient, str) or not coefficient.strip(): + _add_validation_error( + errors, f"variable '{name}[0]' must be a non-empty coefficient name." + ) + elif coefficient in seen: + _add_validation_error( + errors, f"duplicate LET-parameter name '{coefficient}'." + ) + else: + seen.add(coefficient) + if ( + expected_names + and index < len(expected_names) + and coefficient != expected_names[index] + ): + _add_validation_error( + errors, + f"variable {cli_error_value(f'{name}[0]')} has invalid value " + f"{cli_error_value(str(coefficient))}, expected " + f"{cli_correct_value(expected_names[index])} at this position.", + ) + if not _is_finite_number(initial): + _add_validation_error( + errors, f"variable '{name}[1]' must be a finite number." + ) + if not _is_integer(estimated) or estimated not in {0, 1}: + _add_validation_error(errors, f"variable '{name}[2]' must be 0 or 1.") + if not isinstance(distribution, str) or not distribution.strip(): + _add_validation_error( + errors, f"variable '{name}[3]' must be a non-empty distribution name." + ) + if not _is_finite_number(lower) or not _is_finite_number(upper): + _add_validation_error( + errors, f"variables '{name}[4:6]' must be finite numbers." + ) + elif lower >= upper: + _add_validation_error( + errors, + f"variable {cli_error_value(name)} has invalid uniform bounds " + f"{cli_error_value(str([lower, upper]))}, expected the lower " + f"bound to be less than {cli_correct_value('the upper bound')}.", + ) + elif ( + isinstance(distribution, str) + and distribution.lower() == "uniform" + and _is_finite_number(initial) + and not lower <= initial <= upper + ): + _add_validation_error( + errors, + f"variable {cli_error_value(f'{name}[1]')} has invalid value " + f"{cli_error_value(str(initial))}, expected a value between " + f"{cli_correct_value(str(lower))} and " + f"{cli_correct_value(str(upper))} for the uniform distribution.", + ) + + +def _validate_rock_properties(cfg_file: dict[str, Any], errors: list[str]) -> None: + """Validate rock-property history matching settings. + + Each row contains a permeability name, estimation flag, and aggregation + method. Property names are normalized to uppercase after validation. + + Parameters + ---------- + cfg_file : dict[str, Any] + TOML configuration values. + errors : list[str] + Validation messages collected during the current validation pass.""" + if "rock_property_settings" not in cfg_file: + return + rows = cfg_file["rock_property_settings"] + if not isinstance(rows, list): + _add_validation_error( + errors, "variable 'rock_property_settings' must be an array of arrays." + ) + return + seen: set[str] = set() + for index, row in enumerate(rows): + name = f"rock_property_settings[{index}]" + if not isinstance(row, list) or len(row) != 3: + _add_validation_error( + errors, + f"variable '{name}' must contain name, estimation flag, and aggregation method.", + ) + continue + prop, estimated, method = row + if not isinstance(prop, str) or prop.upper() not in ROCK_PROPERTY_NAMES: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{name}[0]')} has invalid value " + f"{cli_error_value(str(prop))}, expected " + "PERMX, PERMY, or PERMZ.", + ) + elif prop.upper() in seen: + _add_validation_error(errors, f"duplicate rock property '{prop.upper()}'.") + else: + seen.add(prop.upper()) + row[0] = prop.upper() + if not _is_integer(estimated) or estimated not in {0, 1}: + _add_validation_error(errors, f"variable '{name}[1]' must be 0 or 1.") + if method not in {"max", "mean"}: + _add_validation_error( + errors, + f"variable {cli_error_value(f'{name}[2]')} has invalid value " + f"{cli_error_value(str(method))}, expected " + f"{cli_correct_value('max')} or {cli_correct_value('mean')}.", + ) + missing = ROCK_PROPERTY_NAMES - seen + if missing: + _add_validation_error( + errors, f"missing rock property settings for {', '.join(sorted(missing))}." + ) + + +def _validate_toml(cfg_file: dict[str, Any]) -> dict[str, Any]: + """Validate and normalize TOML configuration values. + + Unknown and internally managed variables are reported and removed. Remaining + errors are collected so the user receives one complete validation report. + + Parameters + ---------- + cfg_file : dict[str, Any] + Raw values loaded from the TOML configuration. + + Returns + ------- + dict[str, Any] + Validated and normalized values suitable for ``ConfigViaTOML``. + + Raises + ------ + SystemExit + If one or more configuration values are invalid.""" + if not isinstance(cfg_file, dict): + pycopm_error( + f"invalid TOML content {cli_error_value(type(cfg_file).__name__)}, " + f"expected {cli_correct_value('a dictionary of configuration variables')}." + ) + cfg_file = cfg_file.copy() + errors: list[str] = [] + + internal = sorted(INTERNAL_KEYS & cfg_file.keys()) + if internal: + formatted = ", ".join(cli_warning_value(key) for key in internal) + plural = len(internal) != 1 + pycopm_warning( + f"variable{'s' if plural else ''} {formatted} " + f"{'are' if plural else 'is'} managed internally and will be ignored." + ) + for key in internal: + cfg_file.pop(key) + unknown = sorted(cfg_file.keys() - TOML_KEYS) + if unknown: + formatted = ", ".join(cli_warning_value(key) for key in unknown) + plural = len(unknown) != 1 + pycopm_warning( + f"unknown TOML variable{'s' if plural else ''} {formatted} will be ignored." + ) + for key in unknown: + cfg_file.pop(key) + + required = set(TOML_KEYS) - {"satnum_generation_method"} + for key in sorted(required - cfg_file.keys()): + _add_validation_error(errors, f"missing required TOML variable '{key}'.") + + _validate_string(cfg_file, "flow_command", errors) + if _validate_string(cfg_file, "model_name", errors): + model = cfg_file["model_name"].lower() + cfg_file["model_name"] = model + if model not in MODEL_AXIS_LENGTHS: + _add_validation_error( + errors, + f"variable {cli_error_value('model_name')} has invalid value " + f"{cli_error_value(str(model))}, expected " + f"{cli_correct_value('norne')} or {cli_correct_value('drogon')}.", + ) + else: + model = None + + if _validate_string(cfg_file, "execution_mode", errors): + mode = cfg_file["execution_mode"].lower() + cfg_file["execution_mode"] = mode + if mode not in {"single-run", "files", "ert"}: + _add_validation_error( + errors, + f"variable {cli_error_value('execution_mode')} has invalid value " + f"{cli_error_value(str(mode))}, " + "expected 'single-run', 'files', or 'ert'.", + ) + else: + mode = None + + for key, minimum in ( + ("ensemble_size", 1), + ("max_parallel_realizations", 1), + ("max_realization_runtime_seconds", 0), + ("min_successful_realizations", 1), + ("random_seed", 0), + ): + _validate_integer(cfg_file, key, errors, minimum) + + ensemble = cfg_file.get("ensemble_size") + parallel = cfg_file.get("max_parallel_realizations") + successful = cfg_file.get("min_successful_realizations") + if _is_integer(ensemble): + if _is_integer(parallel) and parallel > ensemble: + _add_validation_error( + errors, + "variable 'max_parallel_realizations' cannot exceed 'ensemble_size'.", + ) + if _is_integer(successful) and successful > ensemble: + _add_validation_error( + errors, + "variable 'min_successful_realizations' cannot exceed 'ensemble_size'.", + ) + + for key, choices in ( + ("saturation_function_method", {0, 1}), + ("pore_volume_correction", {0, 1}), + ("initialization_method", {0, 1}), + ): + if _validate_integer(cfg_file, key, errors) and cfg_file[key] not in choices: + _add_validation_error( + errors, + f"variable {cli_error_value(key)} has invalid value " + f"{cli_error_value(str(cfg_file[key]))}, " + f"expected one of {sorted(choices)}.", + ) + + if "satnum_generation_method" in cfg_file: + if _validate_integer(cfg_file, "satnum_generation_method", errors) and cfg_file[ + "satnum_generation_method" + ] not in {0, 1, 2}: + _add_validation_error( + errors, "variable 'satnum_generation_method' must be 0, 1, or 2." + ) + if model == "drogon": + pycopm_warning( + f"variable {cli_warning_value('satnum_generation_method')} is only " + f"effective for {cli_correct_value('model_name = norne')} and will be ignored." + ) + cfg_file.pop("satnum_generation_method", None) + elif model == "norne": + cfg_file["satnum_generation_method"] = 0 + + _validate_number_array( + cfg_file, "observation_relative_errors", errors, length=3, minimum=0, maximum=1 + ) + _validate_number_array( + cfg_file, "observation_minimum_errors", errors, length=3, minimum=0 + ) + + if "history_matching_end_date" in cfg_file: + value = cfg_file["history_matching_end_date"] + if isinstance(value, dt.datetime): + cfg_file["history_matching_end_date"] = value.date().isoformat() + elif isinstance(value, dt.date): + cfg_file["history_matching_end_date"] = value.isoformat() + elif isinstance(value, str): + try: + cfg_file["history_matching_end_date"] = dt.date.fromisoformat( + value + ).isoformat() + except ValueError: + _add_validation_error( + errors, + "variable 'history_matching_end_date' must be an ISO date in " + "YYYY-MM-DD format.", + ) + else: + _add_validation_error( + errors, + "variable 'history_matching_end_date' must be a TOML date or ISO date string.", + ) + + _validate_string(cfg_file, "ert_arguments", errors) + if mode != "ert" and "ert_arguments" in cfg_file: + pycopm_warning( + f"variable {cli_warning_value('ert_arguments')} is not executed for " + f"{cli_warning_value(f'execution_mode = {mode}')}, but is retained for " + "generated ERT files." + ) + + if model in MODEL_AXIS_LENGTHS: + for key, length in MODEL_AXIS_LENGTHS[model].items(): + _validate_coarsening(cfg_file, key, length, errors) + + if "cleanup_file_suffixes" in cfg_file: + suffixes = cfg_file["cleanup_file_suffixes"] + if not isinstance(suffixes, list): + _add_validation_error( + errors, "variable 'cleanup_file_suffixes' must be an array of strings." + ) + else: + for index, suffix in enumerate(suffixes): + if not isinstance(suffix, str) or not suffix.strip(): + _add_validation_error( + errors, + f"variable 'cleanup_file_suffixes[{index}]' must be a non-empty string.", + ) + elif any(character in suffix for character in "'\"/*?[]"): + _add_validation_error( + errors, + f"variable 'cleanup_file_suffixes[{index}]' contains unsafe " + "filename-pattern characters.", + ) + + _validate_let_parameters(cfg_file, model, errors) + _validate_rock_properties(cfg_file, errors) + + if cfg_file.get("saturation_function_method") == 1 and not cfg_file.get( + "let_parameters" + ): + _add_validation_error( + errors, "LET saturation functions require a non-empty 'let_parameters'." + ) + + estimated_let = any( + isinstance(row, list) and len(row) >= 3 and row[2] == 1 + for row in cfg_file.get("let_parameters", []) + ) + estimated_rock = any( + isinstance(row, list) and len(row) >= 2 and row[1] == 1 + for row in cfg_file.get("rock_property_settings", []) + ) + if mode == "ert" and not (estimated_let or estimated_rock): + _add_validation_error( + errors, + "execution_mode 'ert' requires at least one estimated LET or rock-property parameter.", + ) + + if errors: + details = "\n".join(f" - {error}" for error in errors) + pycopm_error(f"invalid TOML configuration:\n{details}") + return cfg_file + + def load_toml_config( input_file: str, output_directory: str, resource_directory: str, significant_digits: int, ) -> ConfigViaTOML: - """Load a TOML configuration and derive reference-grid dimensions. + """Load, validate, and initialize a TOML configuration. + + Validation and normalization occur before the reference EGRID is opened and + before ``ConfigViaTOML`` is constructed. Parameters ---------- - input_file + input_file : str TOML configuration path. - output_directory + output_directory : str Generated-project directory. - resource_directory + resource_directory : str Directory containing templates and reference simulations. - significant_digits + significant_digits : int Precision used when writing floating-point values. Returns ------- ConfigViaTOML - Validated configuration populated with reference-grid metadata.""" - with open(input_file, "rb") as f: - cfg_file = tomllib.load(f) + Validated configuration populated with reference-grid metadata. + + Raises + ------ + SystemExit + If the TOML configuration is invalid.""" + with open(input_file, "rb") as file_handle: + cfg_file = _validate_toml(tomllib.load(file_handle)) + suffixes = cfg_file["cleanup_file_suffixes"] cfg_file["cleanup_file_suffixes"] = ",".join(f"'{suffix}'" for suffix in suffixes) - cfg_file["x_coarsening"] = np.array(cfg_file["x_coarsening"]) - cfg_file["y_coarsening"] = np.array(cfg_file["y_coarsening"]) - cfg_file["z_coarsening"] = np.array(cfg_file["z_coarsening"]) + for key in ("x_coarsening", "y_coarsening", "z_coarsening"): + cfg_file[key] = np.asarray(cfg_file[key], dtype=int) + name = "NORNE_ATW2013" if cfg_file["model_name"] == "norne" else "DROGON" case_path = ( Path(resource_directory) diff --git a/src/pycopm/utils/parser_deck.py b/src/pycopm/utils/parser_deck.py index f9e11ab..54100ba 100644 --- a/src/pycopm/utils/parser_deck.py +++ b/src/pycopm/utils/parser_deck.py @@ -186,7 +186,6 @@ def _include_contains_endbox(dck: ConfigViaDeck, nrwo: str) -> bool: deck_path = Path(f"{dck.input_deck_name}.DATA").absolute() include_path = (deck_path.parent / include_text).absolute() if not include_path.exists(): - print(f"Include not found: {include_path}") return False with include_path.open("r", encoding=dck.deck_encoding) as include_file: for row in csv.reader(include_file): @@ -1696,8 +1695,6 @@ def _scan_deck_file( resolved_include = Path(os.path.normpath(base_directory / include_path)) if resolved_include.exists(): includes.append(str(resolved_include)) - else: - print(f"Include not found: {resolved_include}") include_pending = False continue mults = _mark_multiplier_keyword(deck_line, mults) diff --git a/src/pycopm/utils/refinement.py b/src/pycopm/utils/refinement.py index c6b2a25..71b7fb6 100644 --- a/src/pycopm/utils/refinement.py +++ b/src/pycopm/utils/refinement.py @@ -100,7 +100,7 @@ def create_refinement_maps( def refine_properties( dck: ConfigViaDeck, refinement: RefinementMaps, modified_deck: list[str] -) -> None: +) -> list[str]: """Map reservoir properties onto the refined grid. Properties are copied to generated cells. ``PORV`` is divided equally among @@ -113,7 +113,13 @@ def refine_properties( refinement Refinement maps created by :func:`create_refinement_maps`. modified_deck - Deck lines updated with generated property includes.""" + Deck lines updated with generated property includes. + + Returns + ------- + generated_files + Names of the written include files.""" + generated_files = [] number_values = dck.output_nx * dck.output_ny * dck.output_nz property_names = ( dck.props_keywords @@ -168,6 +174,7 @@ def refine_properties( ) if property_name == "porv": dck.output_actnum = (refined_values > 0).astype(int) + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") write_property_inc( dck, property_name, @@ -176,6 +183,7 @@ def refine_properties( modified_deck, True, ) + return generated_files def create_coord_axis_map( diff --git a/src/pycopm/utils/runs_executer.py b/src/pycopm/utils/runs_executer.py index 7aa486c..1801dea 100644 --- a/src/pycopm/utils/runs_executer.py +++ b/src/pycopm/utils/runs_executer.py @@ -13,6 +13,7 @@ from mako.template import Template from pycopm.config.config import ConfigViaTOML +from pycopm.utils.terminal import pycopm_info, pycopm_success def run_simulations(cfg: ConfigViaTOML) -> None: @@ -68,7 +69,7 @@ def run_simulations(cfg: ConfigViaTOML) -> None: cwd=project_path, check=True, ) - print(f"\nThe simulation results have been written to {project_path}") + pycopm_success("the results have been written to ", str(project_path), []) def generate_postprocessing_plots( @@ -120,7 +121,7 @@ def generate_postprocessing_plots( ) plotting_path = project_path / "jobs" / "plotting.py" plotting_path.write_text(rendered_template, encoding="utf8") - print("\nRunning the postprocessing methods, please wait.") + pycopm_info("running the postprocessing methods, please wait...") subprocess.run( [sys.executable, str(plotting_path)], cwd=project_path, diff --git a/src/pycopm/utils/terminal.py b/src/pycopm/utils/terminal.py new file mode 100644 index 0000000..1ed939a --- /dev/null +++ b/src/pycopm/utils/terminal.py @@ -0,0 +1,224 @@ +# SPDX-FileCopyrightText: 2026 NORCE Research AS +# SPDX-License-Identifier: GPL-3.0 + +"""Format command-line messages for pycopm. + +ANSI colors are applied when supported by the selected stream. The module +provides consistent formatting for invalid, accepted, deprecated, and +informational values, plus fatal errors, warnings, tips, progress messages, and +generated-file reports.""" + +import os +import sys +from typing import NoReturn + +ANSI_BOLD_RED = "1;31" +ANSI_BOLD_YELLOW = "1;33" +ANSI_BOLD_GREEN = "1;32" +ANSI_BOLD_BLUE = "1;34" +ANSI_BOLD_MAGENTA = "1;35" +ANSI_YELLOW = "1;33" +ANSI_GREEN = "1;32" +ANSI_CYAN = "36" +ANSI_RED = "31" +ANSI_BLUE = "1;34" + + +def _supports_color(stream: object = sys.stderr) -> bool: + """Check whether an output stream supports ANSI colors. + + Parameters + ---------- + stream : object, optional + Output stream used to determine ANSI-color support. + + Returns + ------- + bool + Whether ANSI color output is enabled for the stream.""" + return ( + hasattr(stream, "isatty") + and stream.isatty() + and os.environ.get("NO_COLOR") is None + and os.environ.get("TERM") != "dumb" + ) + + +def _colorize( + text: str, + code: str, + stream: object = sys.stderr, +) -> str: + """Wrap text in an ANSI color sequence when supported. + + Parameters + ---------- + text : str + Text to format. + code : str + ANSI Select Graphic Rendition code. + stream : object, optional + Output stream used to determine ANSI-color support. + + Returns + ------- + str + Colored text, or unchanged text when colors are disabled.""" + if not _supports_color(stream): + return text + return f"\033[{code}m{text}\033[0m" + + +def cli_warning_value(value: str) -> str: + """Format a deprecated CLI option or value. + + Parameters + ---------- + value : str + Value to inspect or format. + + Returns + ------- + str + Quoted value colored as a warning when supported.""" + return _colorize(repr(value), ANSI_YELLOW) + + +def cli_correct_value(value: str) -> str: + """Format a correct CLI option or value. + + Parameters + ---------- + value : str + Value to inspect or format. + + Returns + ------- + str + Quoted value colored as a valid alternative when supported.""" + return _colorize(repr(value), ANSI_GREEN) + + +def cli_error_value(value: str) -> str: + """Format an invalid CLI option or value. + + Parameters + ---------- + value : str + Value to inspect or format. + + Returns + ------- + str + Quoted value colored as invalid when supported.""" + return _colorize(repr(value), ANSI_RED) + + +def cli_info_value(value: str) -> str: + """Format an informational CLI option or value. + + Parameters + ---------- + value : str + Value to inspect or format. + + Returns + ------- + str + Quoted value colored as information when supported.""" + return _colorize(repr(value), ANSI_BLUE) + + +def pycopm_error(message: str) -> NoReturn: + """Raise a fatal command-line error. + + Parameters + ---------- + message : str + Human-readable message to display or append. + + Raises + ------ + SystemExit + Always raised with the formatted error message.""" + label = _colorize("error", ANSI_BOLD_RED) + raise SystemExit(f"{pycopm_name()}: {label}: {message}") + + +def pycopm_warning(message: str) -> None: + """Display a non-fatal command-line warning. + + Parameters + ---------- + message : str + Human-readable message to display or append.""" + label = _colorize("warning", ANSI_BOLD_YELLOW) + print(f"{pycopm_name()}: {label}: {message}", file=sys.stderr) + + +def pycopm_info(message: str) -> None: + """Display an informational command-line message. + + Parameters + ---------- + message : str + Human-readable message to display or append.""" + label = _colorize("info", ANSI_BOLD_BLUE, sys.stdout) + print(f"{pycopm_name()}: {label}: {message}") + + +def pycopm_tip(message: str) -> None: + """Display a command-line suggestion. + + Parameters + ---------- + message : str + Human-readable message to display or append.""" + label = _colorize("tip", ANSI_BOLD_MAGENTA, sys.stdout) + print(f"{pycopm_name(sys.stdout)}: {label}: {message}") + + +def pycopm_success(msg: str, output_dir: str, filenames: list[str]) -> None: + """Display the generated output location and filenames. + + Parameters + ---------- + msg : str + Optional success text printed before the output location. + output_dir : str + Directory containing the generated files. + filenames : list[str] + Generated filenames to report.""" + label = _colorize("success", ANSI_BOLD_GREEN, sys.stdout) + if not filenames: + print(f"{pycopm_name()}: {label}: {msg}{output_dir}") + elif len(filenames) == 1: + print(f"{pycopm_name()}: {label}: {msg}{output_dir}/{filenames[0]}") + elif len(filenames) <= 5: + print(f"{pycopm_name()}: {label}{msg}") + print(f" Output directory: {output_dir}") + print(f" Files ({len(filenames)}): {', '.join(filenames)}") + else: + print(f"{pycopm_name()}: {label}{msg}") + print(f" Output directory: {output_dir}") + print(f" Files ({len(filenames)}):") + for filename in filenames: + print(f" - {filename}") + + +def pycopm_name(stream: object = sys.stderr) -> str: + """Format the pycopm program name. + + Parameters + ---------- + stream : object, optional + Output stream used to determine ANSI-color support. + + Returns + ------- + str + Formatted program name.""" + characters = [("pycopm", "1")] + return "".join( + _colorize(character, color, stream) for character, color in characters + ) diff --git a/src/pycopm/utils/transformation.py b/src/pycopm/utils/transformation.py index 0bfbb73..b2449cf 100644 --- a/src/pycopm/utils/transformation.py +++ b/src/pycopm/utils/transformation.py @@ -114,7 +114,7 @@ def transform_grid(dck: ConfigViaDeck) -> None: write_grid(dck, cr, zc, False) -def transform_properties(dck: ConfigViaDeck, modified_deck: list[str]) -> None: +def transform_properties(dck: ConfigViaDeck, modified_deck: list[str]) -> list[str]: """Rewrite reservoir properties for a transformed grid. Property values are unchanged because transformations modify only geometry. @@ -124,7 +124,13 @@ def transform_properties(dck: ConfigViaDeck, modified_deck: list[str]) -> None: dck Deck configuration containing source properties and output dimensions. modified_deck - Deck lines updated with generated property includes.""" + Deck lines updated with generated property includes. + + Returns + ------- + generated_files + Names of the written include files.""" + generated_files = [] property_names = ( dck.props_keywords + dck.regions_keywords @@ -161,3 +167,5 @@ def transform_properties(dck: ConfigViaDeck, modified_deck: list[str]) -> None: modified_deck, True, ) + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") + return generated_files diff --git a/src/pycopm/utils/vicinity.py b/src/pycopm/utils/vicinity.py index 45d3af1..325e990 100644 --- a/src/pycopm/utils/vicinity.py +++ b/src/pycopm/utils/vicinity.py @@ -366,7 +366,7 @@ def create_vicinity_maps(dck: ConfigViaDeck) -> VicinityMaps: def map_vicinity_properties( dck: ConfigViaDeck, vicinity: VicinityMaps, modified_deck: list[str] -) -> None: +) -> list[str]: """Map reservoir properties into the submodel bounding box. Cells inside the bounding box but outside the selection are written as @@ -379,7 +379,13 @@ def map_vicinity_properties( vicinity Selection and bounds created by :func:`create_vicinity_maps`. modified_deck - Deck lines updated with generated property includes.""" + Deck lines updated with generated property includes. + + Returns + ------- + generated_files + Names of the written include files.""" + generated_files = [] submodel_cells = dck.output_nx * dck.output_ny * dck.output_nz dck.original_active_cell_mask = np.asarray(dck.original_porv) > 0 vicinity.active_counts = np.zeros(dck.output_nz) @@ -449,7 +455,9 @@ def map_vicinity_properties( modified_deck, True, ) + generated_files.append(f"{dck.include_prefix}{property_name.upper()}.INC") dck.output_actnum = (np.asarray(dck.output_porv) > 0).astype(int) + return generated_files def extract_vicinity_grid(dck: ConfigViaDeck, vicinity: VicinityMaps) -> None: diff --git a/tests/scripts/docs_config_views.sh b/tests/scripts/docs_config_views.sh index 2470925..8d16e6d 100644 --- a/tests/scripts/docs_config_views.sh +++ b/tests/scripts/docs_config_views.sh @@ -1,4 +1,4 @@ WHR="src/pycopm/reference_simulation/norne/NORNE_ATW2013" OUT="test_outputs/docs_config_views" . tests/scripts/initialize_output_folders.sh $OUT -plopm -i "$WHR $WHR $WHR" -v "index_i,index_j,index_k" -o $OUT -s ',,: ,,: ,:,' -remove 0,0,0,1 -c cet_glasbey -cnum 5 -subfigs 1,3 -z 0 -clabel "Index [-]" -d 18,10 -xunits km -yunits km -xformat .1f -yformat .1f -suptitle 0 -cbsfax 0.1,0.95,0.8,0.02 -x '[455.5e3,463e3] [455.5e3,463e3] [453.2e3,454.8e3]' -y '[7319.5e3,7327e3] [7319.5e3,7327e3] [3.22e3,2.8e3]' -t "Index I Index J Index K" -save index_plopm \ No newline at end of file +plopm -i "$WHR $WHR $WHR" -v "index_i,index_j,index_k" -o $OUT -s ',,: ,,: ,:,' -hide 0,0,0,1 -c cet_glasbey -cbn 5 -sg 1,3 -asp 0 -cbl "Index [-]" -fs 18,10 -xu km -yu km -xf .1f -yf .1f -st 0 -cbp 0.1,0.95,0.8,0.02 -x '[455.5e3,463e3] [455.5e3,463e3] [453.2e3,454.8e3]' -y '[7319.5e3,7327e3] [7319.5e3,7327e3] [3.22e3,2.8e3]' -t "Index I Index J Index K" -fn index_plopm diff --git a/tests/scripts/docs_theory_coarsening.sh b/tests/scripts/docs_theory_coarsening.sh index 194f507..45ef692 100644 --- a/tests/scripts/docs_theory_coarsening.sh +++ b/tests/scripts/docs_theory_coarsening.sh @@ -5,4 +5,4 @@ OUT="test_outputs/docs_theory_coarsening" pycopm -i $WHR/THEORY0.DATA -o $OUT -c 3,1,3 -m all -a max -w coarsening_max_two_cells pycopm -i $WHR/THEORY0.DATA -o $OUT -c 3,1,3 -m all -a min -w coarsening_min_two_cells pycopm -i $WHR/THEORY0.DATA -o $OUT -c 6,1,3 -m all -a max -w coarsening_max_one_cell -plopm -i "$OUT/THEORY0_PREP_PYCOPM_DRYRUN $OUT/COARSENING_MAX_TWO_CELLS $OUT/COARSENING_MIN_TWO_CELLS $OUT/COARSENING_MAX_ONE_CELL" -v fipnum -subfigs 1,4 -o $OUT -d 15,4 -clabel "Number of cell" -delax 1 -suptitle 0 -cbsfax 0.1,0.95,0.8,0.02 -t "Input grid (18 cells, 17 active cells) Coarsening into two cells (using mode/max) Coarsening into two cells (using min) Coarsening into one cell (using mode/max)" -f 10 -save coarsening_plopm +plopm -i "$OUT/THEORY0_PREP_PYCOPM_DRYRUN $OUT/COARSENING_MAX_TWO_CELLS $OUT/COARSENING_MIN_TWO_CELLS $OUT/COARSENING_MAX_ONE_CELL" -v fipnum -sg 1,4 -o $OUT -fs 15,4 -cbl "Number of cell" -rdl 1 -st 0 -cbp 0.1,0.95,0.8,0.02 -t "Input grid (18 cells, 17 active cells) Coarsening into two cells (using mode/max) Coarsening into two cells (using min) Coarsening into one cell (using mode/max)" -fz 10 -fn coarsening_plopm diff --git a/tests/scripts/docs_theory_refinements.sh b/tests/scripts/docs_theory_refinements.sh index d0ad5cc..9f9d310 100644 --- a/tests/scripts/docs_theory_refinements.sh +++ b/tests/scripts/docs_theory_refinements.sh @@ -3,4 +3,4 @@ OUT="test_outputs/docs_theory_refinements" . tests/scripts/initialize_output_folders.sh $OUT . tests/scripts/get_plopm.sh pycopm -i $WHR/MODEL3.DATA -o $OUT -g 2,2,2 -m all -plopm -i "$OUT/MODEL3_PREP_PYCOPM_DRYRUN $OUT/MODEL3_PYCOPM" -v wells -xunits km -yunits km -yformat .0f -xformat .0f -ylnum 5 -xlnum 5 -s ,,: -grid black,1e-2 -subfigs 1,2 -o $OUT -d 10,4 -delax 1 -suptitle 0 -cbsfax 0.1,0.95,0.8,0.02 -t "Input grid Refined grid" -save refinement_plopm +plopm -i "$OUT/MODEL3_PREP_PYCOPM_DRYRUN $OUT/MODEL3_PYCOPM" -v wells -xu km -yu km -yf .0f -xf .0f -ynt 5 -xnt 5 -s ,,: -ge black,1e-2 -sg 1,2 -o $OUT -fs 10,4 -rdl 1 -st 0 -cbp 0.1,0.95,0.8,0.02 -t "Input grid Refined grid" -fn refinement_plopm diff --git a/tests/scripts/docs_theory_submodels.sh b/tests/scripts/docs_theory_submodels.sh index 7b4737b..d38962e 100644 --- a/tests/scripts/docs_theory_submodels.sh +++ b/tests/scripts/docs_theory_submodels.sh @@ -5,10 +5,10 @@ OUT="test_outputs/docs_theory_submodels" . tests/scripts/run_norne.sh WHR="examples/decks" pycopm -i $WHR/THEORY1.DATA -o $OUT -p 1 -v "xypolygon [50,90] [60,60] [90,60] [65,40] [75,10] [50,30] [25,10] [35,40] [10,60] [40,60] [50,90]" -m all -plopm -i "$OUT/THEORY1_PREP_PYCOPM_DRYRUN $OUT/THEORY1_PYCOPM" -c nipy_spectral -v index_j -s ,,1 -o $OUT -cnum 5 -d 10,6 -subfigs 1,2 -t "INPUT GRID SUBMODEL" -suptitle 0 -cbsfax 0.20,0.001,0.6,0.02 -save submodel_plopm -plopm -i $OUT/THEORY1_PYCOPM -v porv -s ,,1 -o $OUT -cnum 5 -save submodel_porv_plopm +plopm -i "$OUT/THEORY1_PREP_PYCOPM_DRYRUN $OUT/THEORY1_PYCOPM" -c nipy_spectral -v index_j -s ,,1 -o $OUT -cbn 5 -fs 10,6 -sg 1,2 -t "INPUT GRID SUBMODEL" -st 0 -cbp 0.20,0.001,0.6,0.02 -fn submodel_plopm +plopm -i $OUT/THEORY1_PYCOPM -v porv -s ,,1 -o $OUT -cbn 5 -fn submodel_porv_plopm WHR="test_outputs/opm-data/norne" pycopm -i $WHR/NORNE_ATW2013.DATA -o $WHR -v 'E-3H diamondxy 0' -w diamondxy -m all -p 1 pycopm -i $WHR/NORNE_ATW2013.DATA -o $WHR -v 'E-3H diamond 1' -w diamond1 -m all -p 1 pycopm -i $WHR/NORNE_ATW2013.DATA -o $WHR -v 'E-3H box [-1,2] [-2,3] [-1,1]' -w box -m all -p 1 -plopm -i "$WHR/DIAMONDXY $WHR/DIAMOND1 $WHR/BOX" -d 20,12 -v porv -s :,, -xunits km -yunits km -xformat .1f -yformat .1f -subfigs 1,3 -o $OUT -log 1 -cbsfax 0.20,0.0,0.6,0.005 -suptitle 0 -z 0 -save submodelwell_plopm -f 20 -grid black,1e-2 +plopm -i "$WHR/DIAMONDXY $WHR/DIAMOND1 $WHR/BOX" -fs 20,12 -v porv -s :,, -xu km -yu km -xf .1f -yf .1f -sg 1,3 -o $OUT -clog 1 -cbp 0.20,0.0,0.6,0.005 -st 0 -asp 0 -fn submodelwell_plopm -fz 20 -ge black,1e-2 diff --git a/tests/scripts/docs_theory_transformations.sh b/tests/scripts/docs_theory_transformations.sh index 4a48fdf..00041a4 100644 --- a/tests/scripts/docs_theory_transformations.sh +++ b/tests/scripts/docs_theory_transformations.sh @@ -5,4 +5,4 @@ OUT="test_outputs/docs_theory_transformations" pycopm -i $WHR/THEORY1.DATA -o $OUT -p 1 -v "xypolygon [50,90] [60,60] [90,60] [65,40] [75,10] [50,30] [25,10] [35,40] [10,60] [40,60] [50,90]" -m all pycopm -i $OUT/THEORY1_PYCOPM.DATA -o $OUT -d 'rotatexy 45' -m all pycopm -i $OUT/THEORY1_PYCOPM_PYCOPM.DATA -o $OUT -d 'scale [1,0.25,1]' -m all -plopm -i "$OUT/THEORY1_PYCOPM_PYCOPM $OUT/THEORY1_PYCOPM_PYCOPM_PYCOPM" -c nipy_spectral -v index_j -s ,,1 -o $OUT -cnum 5 -d 12,6 -subfigs 1,2 -t "ROTATED ROTATED AND SCALED" -suptitle 0 -cbsfax 0.20,0.001,0.6,0.02 -save transformation_plopm +plopm -i "$OUT/THEORY1_PYCOPM_PYCOPM $OUT/THEORY1_PYCOPM_PYCOPM_PYCOPM" -c nipy_spectral -v index_j -s ,,1 -o $OUT -cbn 5 -fs 12,6 -sg 1,2 -t "ROTATED ROTATED AND SCALED" -st 0 -cbp 0.20,0.001,0.6,0.02 -fn transformation_plopm diff --git a/tests/scripts/docs_via_config_drogon.sh b/tests/scripts/docs_via_config_drogon.sh index e2c9313..b0a544a 100644 --- a/tests/scripts/docs_via_config_drogon.sh +++ b/tests/scripts/docs_via_config_drogon.sh @@ -4,4 +4,4 @@ REF="src/pycopm/reference_simulation/drogon/DROGON" . tests/scripts/initialize_output_folders.sh $OUT . tests/scripts/get_plopm.sh pycopm -i $WHR -o $OUT -plopm -i "$REF $OUT/postprocessing/closest_to_obs/DROGON_COARSER" -v soil -xlnum 4 -ylnum 4 -cnum 5 -subfigs 1,2 -d 10,7 -delax 1 -cbsfax 0.1,0.95,0.8,0.02 -suptitle 0 -clabel 'Initial pore-volume weighted oil saturation [-]' -s ,,: -rotate -30 -xunits km -yunits km -xformat .0f -yformat .1f -o $OUT -r 0 -save drogon_coarser_plopm +plopm -i "$REF $OUT/postprocessing/closest_to_obs/DROGON_COARSER" -v soil -xnt 4 -ynt 4 -cbn 5 -sg 1,2 -fs 10,7 -rdl 1 -cbp 0.1,0.95,0.8,0.02 -st 0 -cbl 'Initial pore-volume weighted oil saturation [-]' -s ,,: -rot -30 -xu km -yu km -xf .0f -yf .1f -o $OUT -r 0 -fn drogon_coarser_plopm diff --git a/tests/scripts/docs_via_deck_drogon.sh b/tests/scripts/docs_via_deck_drogon.sh index 2b8e955..106dd0f 100644 --- a/tests/scripts/docs_via_deck_drogon.sh +++ b/tests/scripts/docs_via_deck_drogon.sh @@ -8,6 +8,6 @@ pycopm -i $WHR/DROGON_HIST_PYCOPM.DATA -c 1,3,1 -p 1 -q 1 -j 2.5 -l C2 -m all -o pycopm -i $WHR/DROGON_HIST.DATA -c 2,2,2 -p 1 -q 1 -j 4 -w DROGON_2TIMES_COARSER -m all -o $WHR flow $WHR/DROGON_HIST_PYCOPM_PYCOPM.DATA flow $WHR/DROGON_2TIMES_COARSER -plopm -i "$WHR/DROGON_HIST $WHR/DROGON_HIST_PYCOPM_PYCOPM" -o $OUT -v poro -subfigs 1,2 -save drogon_generic_plopm -s ,,: -rotate '-30' -xunits km -yunits km -xformat .1f -yformat .1f -d 11,8 -delax 1 -suptitle 0 -cbsfax 0.1,0.95,0.8,0.02 -cnum 5 -cformat .2f -t "Drogon Coarsened Drogon" -f 17 -xlnum 2 -ylnum 2 -plopm -i "$WHR/DROGON_HIST $WHR/DROGON_HIST_PYCOPM_PYCOPM $WHR/DROGON_2TIMES_COARSER" -o $OUT -v 'FOIP,FOPR,TCPU' -tunits y -f 14 -subfigs 2,2 -delax 1 -loc empty,empty,empty,center -d 10,5 -xformat '.1f' -xlnum 6 -ylabel 'sm$^3$ sm$^3$/day seconds' -t 'Field oil in place Field oil production rate Simulation time' -labels 'DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER' -save drogon_pycopm_comparison -yformat '.2e,.0f,.0f' -plopm -i "$WHR/DROGON_HIST $WHR/DROGON_HIST_PYCOPM_PYCOPM $WHR/DROGON_2TIMES_COARSER" -o $OUT -v sgas -subfigs 1,3 -d 15,11 -cnum 5 -m gif -xlnum 4 -ylnum 4 -dpi 300 -t "DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER" -f 16 -interval 2000 -loop 1 -cformat .2f -cbsfax 0.15,0.93,0.7,0.02 -s ,,1 -rotate '-30' -xunits km -yunits km -xformat .0f -yformat .0f -c cet_rainbow_bgyrm_35_85_c69 -delax 1 -tunits tstep +plopm -i "$WHR/DROGON_HIST $WHR/DROGON_HIST_PYCOPM_PYCOPM" -o $OUT -v poro -sg 1,2 -fn drogon_generic_plopm -s ,,: -rot '-30' -xu km -yu km -xf .1f -yf .1f -fs 11,8 -rdl 1 -st 0 -cbp 0.1,0.95,0.8,0.02 -cbn 5 -cbf .2f -t "Drogon Coarsened Drogon" -fz 17 -xnt 2 -ynt 2 +plopm -i "$WHR/DROGON_HIST $WHR/DROGON_HIST_PYCOPM_PYCOPM $WHR/DROGON_2TIMES_COARSER" -o $OUT -v 'FOIP,FOPR,TCPU' -tu y -fz 14 -sg 2,2 -rdl 1 -ll empty,empty,empty,center -fs 10,5 -xf '.1f' -xnt 6 -yl 'sm$^3$ sm$^3$/day seconds' -t 'Field oil in place Field oil production rate Simulation time' -llb 'DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER' -fn drogon_pycopm_comparison -yf '.2e,.0f,.0f' +plopm -i "$WHR/DROGON_HIST $WHR/DROGON_HIST_PYCOPM_PYCOPM $WHR/DROGON_2TIMES_COARSER" -o $OUT -v sgas -sg 1,3 -fs 15,11 -cbn 5 -m gif -xnt 4 -ynt 4 -dpi 300 -t "DROGON DROGON 3XZ COARSER DROGON 2XYZ COARSER" -fz 16 -gi 2000 -gl 1 -cbf .2f -cbp 0.15,0.93,0.7,0.02 -s ,,1 -rot '-30' -xu km -yu km -xf .0f -yf .0f -c cet_rainbow_bgyrm_35_85_c69 -rdl 1 -tu tstep diff --git a/tests/scripts/docs_via_deck_dual_coarsening.sh b/tests/scripts/docs_via_deck_dual_coarsening.sh index e0a428b..db6020f 100644 --- a/tests/scripts/docs_via_deck_dual_coarsening.sh +++ b/tests/scripts/docs_via_deck_dual_coarsening.sh @@ -7,4 +7,4 @@ pycopm -i $WHR -o $OUT -z 1:4 -w DUAL -dual 'poro <= 0.1' -l D -t 2 -a max flow $WHR --output-dir=$OUT flow $OUT/STANDARD.DATA flow $OUT/DUAL.DATA -plopm -i "$OUT/MODEL6 $OUT/STANDARD $OUT/DUAL" -v 'pressure - 0pressure' -subfigs 1,3 -delax 1 -cbsfax 0.1,0.95,0.8,0.02 -d 12,4 -suptitle 0 -z 0 -clabel 'Pressure increase end of simulation [bar]' -grid 'black,1e-2' -o $OUT +plopm -i "$OUT/MODEL6 $OUT/STANDARD $OUT/DUAL" -v 'pressure - 0pressure' -sg 1,3 -rdl 1 -cbp 0.1,0.95,0.8,0.02 -fs 12,4 -st 0 -asp 0 -cbl 'Pressure increase end of simulation [bar]' -ge 'black,1e-2' -o $OUT diff --git a/tests/scripts/docs_via_deck_hello_world.sh b/tests/scripts/docs_via_deck_hello_world.sh index c76cc4b..f0d3af1 100644 --- a/tests/scripts/docs_via_deck_hello_world.sh +++ b/tests/scripts/docs_via_deck_hello_world.sh @@ -3,12 +3,12 @@ OUT="test_outputs/docs_via_deck_hello_world" . tests/scripts/initialize_output_folders.sh $OUT . tests/scripts/get_plopm.sh pycopm -i $WHR.DATA -o $OUT -c 5,5,1 -m all -l f1 -a max -plopm -i $OUT/HELLO_WORLD_PREP_PYCOPM_DRYRUN -v porv -o $OUT -s ,,1 -grid 'black,1e-2' -save hello_world_1_left -plopm -i $OUT/HELLO_WORLD_PYCOPM -v porv -o $OUT -s ,,1 -grid 'black,1e-2' -save hello_world_1_right -plopm -i "$OUT/HELLO_WORLD_PREP_PYCOPM_DRYRUN $OUT/HELLO_WORLD_PYCOPM" -v fipnum -s ,,1 -grid 'black,1e-2' -subfigs 1,2 -d 16,8 -delax 1 -suptitle 0 -cbsfax 0.1,0.95,0.8,0.02 -o $OUT -c cet_glasbey_hv -save hello_world_2 -t "Input model Coarsened model" -f 20 +plopm -i $OUT/HELLO_WORLD_PREP_PYCOPM_DRYRUN -v porv -o $OUT -s ,,1 -ge 'black,1e-2' -fn hello_world_1_left +plopm -i $OUT/HELLO_WORLD_PYCOPM -v porv -o $OUT -s ,,1 -ge 'black,1e-2' -fn hello_world_1_right +plopm -i "$OUT/HELLO_WORLD_PREP_PYCOPM_DRYRUN $OUT/HELLO_WORLD_PYCOPM" -v fipnum -s ,,1 -ge 'black,1e-2' -sg 1,2 -fs 16,8 -rdl 1 -st 0 -cbp 0.1,0.95,0.8,0.02 -o $OUT -c cet_glasbey_hv -fn hello_world_2 -t "Input model Coarsened model" -fz 20 pycopm -i $WHR.DATA -v 'xypolygon [4,8.5] [4,16.5] [11.5,16.5] [11.5,8.5] [4,8.5]' -p 1 -m all -o $OUT -l f2 pycopm -i $OUT/HELLO_WORLD_PYCOPM.DATA -rx 0,0,0,2,0,0,0 -ry 0,0,0,2,0,0,0 -m all -o $OUT -l f3 pycopm -i $OUT/HELLO_WORLD_PYCOPM_PYCOPM.DATA -d 'rotatexy 45' -m all -o $OUT -l f4 -plopm -i $OUT/HELLO_WORLD_PYCOPM -v porv -o $OUT -s ,,1 -grid 'black,1e-2' -save submodel -save hello_world_3_submodel -plopm -i $OUT/HELLO_WORLD_PYCOPM_PYCOPM -v porv -o $OUT -s ,,1 -grid 'black,1e-2' -save hello_world_3_submodel_refined -plopm -i $OUT/HELLO_WORLD_PYCOPM_PYCOPM_PYCOPM -v porv -o $OUT -s ,,1 -grid 'black,1e-2' -save hello_world_3_submodel_refined_rotated +plopm -i $OUT/HELLO_WORLD_PYCOPM -v porv -o $OUT -s ,,1 -ge 'black,1e-2' -fn submodel -fn hello_world_3_submodel +plopm -i $OUT/HELLO_WORLD_PYCOPM_PYCOPM -v porv -o $OUT -s ,,1 -ge 'black,1e-2' -fn hello_world_3_submodel_refined +plopm -i $OUT/HELLO_WORLD_PYCOPM_PYCOPM_PYCOPM -v porv -o $OUT -s ,,1 -ge 'black,1e-2' -fn hello_world_3_submodel_refined_rotated diff --git a/tests/scripts/docs_via_deck_norne.sh b/tests/scripts/docs_via_deck_norne.sh index 3b01c76..ae17b90 100644 --- a/tests/scripts/docs_via_deck_norne.sh +++ b/tests/scripts/docs_via_deck_norne.sh @@ -5,4 +5,4 @@ OUT="test_outputs/docs_via_deck_norne" WHR="test_outputs/opm-data/norne" pycopm -i $WHR/NORNE_ATW2013.DATA -o $WHR -s pvmean -x 0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,2,0,2,2,2,2,0 -y 0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,0,2,2,2,2,2,2,2,2,2,0 -z 0,0,2,0,0,2,2,2,2,2,0,2,2,2,2,2,0,0,2,0,2,2,0 -a min -p 1 -q 1 -m all mpirun -np 8 flow $WHR/NORNE_ATW2013_PYCOPM -plopm -i "$WHR/NORNE_ATW2013 $WHR/NORNE_ATW2013_PYCOPM $WHR/NORNE_ATW2013 $WHR/NORNE_ATW2013_PYCOPM" -v sgas -o $OUT -s ,,: -rotate 65 -translate '[6456335.5,-3476500]' -x '[0,5600]' -y '[0,8800]' -d 30,10 -subfigs 1,4 -delax 1 -r 0,0,241,241 -suptitle 0 -cbsfax 0.15,0.95,0.7,0.02 -f 22 -t "Norne (intial time) Coarsened Norne (initial time) Norne (final time) Coarsened Norne (final time)" -cnum 5 -cformat .2f -save norne_plopm +plopm -i "$WHR/NORNE_ATW2013 $WHR/NORNE_ATW2013_PYCOPM $WHR/NORNE_ATW2013 $WHR/NORNE_ATW2013_PYCOPM" -v sgas -o $OUT -s ,,: -rot 65 -tr '[6456335.5,-3476500]' -x '[0,5600]' -y '[0,8800]' -fs 30,10 -sg 1,4 -rdl 1 -r 0,0,241,241 -st 0 -cbp 0.15,0.95,0.7,0.02 -fz 22 -t "Norne (intial time) Coarsened Norne (initial time) Norne (final time) Coarsened Norne (final time)" -cbn 5 -cbf .2f -fn norne_plopm diff --git a/tests/scripts/docs_via_deck_smeaheia.sh b/tests/scripts/docs_via_deck_smeaheia.sh index c1a7632..5c1c67d 100644 --- a/tests/scripts/docs_via_deck_smeaheia.sh +++ b/tests/scripts/docs_via_deck_smeaheia.sh @@ -9,4 +9,4 @@ if [ ! -f "$WHR" ]; then fi . tests/scripts/initialize_output_folders.sh $OUT pycopm -i $WHR -c 5,4,1 -a min -m all -o $OUT -plopm -i "$OUT/STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PREP_PYCOPM_DRYRUN $OUT/STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PYCOPM" -cnum 7 -s ,,1 -v poro -subfigs 1,2 -save smeaheia -t 'Smeaheia Coarsened Smeaheia' -delax 1 -xunits km -xformat .0f -yunits km -yformat .0f -d 10,7 -suptitle 0 -c cet_rainbow_bgyrm_35_85_c69 -cbsfax 0.1,0.95,0.8,0.02 -cformat .2f -o $OUT -save smeaheia +plopm -i "$OUT/STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PREP_PYCOPM_DRYRUN $OUT/STATOIL_FEASIBILITY_SIM_MODEL_WITH_DEPLETION_KROSS_INJ_SECTOR_20_PYCOPM" -cbn 7 -s ,,1 -v poro -sg 1,2 -fn smeaheia -t 'Smeaheia Coarsened Smeaheia' -rdl 1 -xu km -xf .0f -yu km -yf .0f -fs 10,7 -st 0 -c cet_rainbow_bgyrm_35_85_c69 -cbp 0.1,0.95,0.8,0.02 -cbf .2f -o $OUT -fn smeaheia diff --git a/tests/scripts/docs_via_deck_spe10.sh b/tests/scripts/docs_via_deck_spe10.sh index 21af5f4..f57bcf0 100644 --- a/tests/scripts/docs_via_deck_spe10.sh +++ b/tests/scripts/docs_via_deck_spe10.sh @@ -4,4 +4,4 @@ OUT="test_outputs/docs_via_deck_spe10" WHR="test_outputs/opm-data/spe10model2" pycopm -i $WHR/SPE10_MODEL2.DATA -o $WHR -s pvmean -c 4,8,2 -m all pycopm -i $WHR/SPE10_MODEL2_PYCOPM.DATA -o $WHR -p 0 -v 'INJ diamondxy 5' -m all -w vicinity -l sub -m all -plopm -i "$WHR/SPE10_MODEL2_PREP_PYCOPM_DRYRUN $WHR/SPE10_MODEL2_PYCOPM $WHR/VICINITY" -v poro -suptitle 0 -o $OUT -s ,,: -d 19.5,10 -cformat .2f -cnum 5 -subfigs 1,3 -delax 0 -cbsfax 0.20,0.001,0.6,0.02 -t "SPE10 MODEL2 COARSENED MODEL SECTOR MODEL (FROM COARSENED MODEL)" -save spe10_plopm -f 20 -cbsfax 0.15,0.95,0.7,0.02 +plopm -i "$WHR/SPE10_MODEL2_PREP_PYCOPM_DRYRUN $WHR/SPE10_MODEL2_PYCOPM $WHR/VICINITY" -v poro -st 0 -o $OUT -s ,,: -fs 19.5,10 -cbf .2f -cbn 5 -sg 1,3 -rdl 0 -cbp 0.20,0.001,0.6,0.02 -t "SPE10 MODEL2 COARSENED MODEL SECTOR MODEL (FROM COARSENED MODEL)" -fn spe10_plopm -fz 20 -cbp 0.15,0.95,0.7,0.02