-
Notifications
You must be signed in to change notification settings - Fork 35
Refactor and cleanup restartthinner #847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,18 +2,22 @@ | |||||
|
|
||||||
| import argparse | ||||||
| import datetime | ||||||
| import glob | ||||||
| import logging | ||||||
| import os | ||||||
| import shutil | ||||||
| import sys | ||||||
| import subprocess | ||||||
| import tempfile | ||||||
| from collections.abc import Iterator | ||||||
| from contextlib import contextmanager | ||||||
| from pathlib import Path | ||||||
|
|
||||||
| import numpy | ||||||
| import pandas | ||||||
| import numpy as np | ||||||
| import pandas as pd | ||||||
| from resdata.resfile import ResdataFile | ||||||
|
|
||||||
| from subscript import __version__ | ||||||
| from subscript import __version__, getLogger | ||||||
|
|
||||||
| logger = getLogger(__name__) | ||||||
|
|
||||||
| DESCRIPTION = """ | ||||||
| Slice a subset of restart-dates from an E100 Restart file (UNRST) | ||||||
|
|
@@ -28,97 +32,110 @@ | |||||
|
|
||||||
|
|
||||||
| def find_resdata_app(toolname: str) -> str: | ||||||
| """Locate path of apps in resdata. | ||||||
|
|
||||||
| These have varying suffixes due through the history of resdata Makefiles. | ||||||
| """Locate path of resdata apps, trying common suffixes (.x, .c.x, .cpp.x). | ||||||
|
|
||||||
| Depending on resdata-version, it has the .x or the .c.x suffix | ||||||
| We prefer .x. | ||||||
| Args: | ||||||
| toolname: Base name of the tool (e.g., 'rd_unpack') | ||||||
|
|
||||||
| Returns: | ||||||
| String with path if found. | ||||||
| Full path to the executable. | ||||||
|
|
||||||
| Raises: | ||||||
| IOError: if tool can't be found | ||||||
| OSError: If tool cannot be found in PATH. | ||||||
| """ | ||||||
| extensions = [".x", ".c.x", ".cpp.x", ""] # Order matters. | ||||||
| candidates = [toolname + extension for extension in extensions] | ||||||
| for candidate in candidates: | ||||||
| for path in os.environ["PATH"].split(os.pathsep): | ||||||
| candidatepath = Path(path) / candidate | ||||||
| if candidatepath.exists(): | ||||||
| return str(candidatepath) | ||||||
| raise OSError(toolname + " not found in path, PATH=" + str(os.environ["PATH"])) | ||||||
|
|
||||||
|
|
||||||
| def date_slicer(slicedates: list, restartdates: list, restartindices: list) -> dict: | ||||||
| """Make a dict that maps a chosen restart date to a report index""" | ||||||
| slicedatemap = {} | ||||||
| for ext in [".x", ".c.x", ".cpp.x", ""]: # Order matters. | ||||||
| if path := shutil.which(toolname + ext): | ||||||
| return path | ||||||
| raise OSError(f"{toolname} not found in PATH") | ||||||
|
|
||||||
|
|
||||||
| def date_slicer( | ||||||
| slicedates: list[pd.Timestamp], | ||||||
| restartdates: list[datetime.datetime], | ||||||
| restartindices: list[int], | ||||||
| ) -> list[int]: | ||||||
| """Make a list of report indices that match the input slicedates.""" | ||||||
| slicedatelist = [] | ||||||
| for slicedate in slicedates: | ||||||
| daydistances = [ | ||||||
| abs((pandas.Timestamp(slicedate) - x).days) for x in restartdates | ||||||
| ] | ||||||
| slicedatemap[slicedate] = restartindices[daydistances.index(min(daydistances))] | ||||||
| return slicedatemap | ||||||
| daydistances = [abs((pd.Timestamp(slicedate) - x).days) for x in restartdates] | ||||||
| slicedatelist.append(restartindices[daydistances.index(min(daydistances))]) | ||||||
| return slicedatelist | ||||||
|
|
||||||
|
|
||||||
| def rd_repacker(rstfilename: str, slicerstindices: list, quiet: bool) -> None: | ||||||
| """ | ||||||
| Wrapper for ecl_unpack.x and ecl_pack.x utilities. These | ||||||
| utilities are from resdata. | ||||||
| @contextmanager | ||||||
| def _working_directory(path: Path) -> Iterator[None]: | ||||||
| original_cwd = Path.cwd() | ||||||
| try: | ||||||
| os.chdir(path) | ||||||
| yield | ||||||
| finally: | ||||||
| os.chdir(original_cwd) | ||||||
larsevj marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| First unpacking a UNRST file, then deleting dates the dont't want, then | ||||||
| pack the remainding files into a new UNRST file | ||||||
|
|
||||||
| This function will change working directory to the | ||||||
| location of the UNRST file, dump temporary files in there, and | ||||||
| modify the original filename. | ||||||
| """ | ||||||
| out = " >/dev/null" if quiet else "" | ||||||
| # Error early if resdata tools are not available | ||||||
| try: | ||||||
| find_resdata_app("rd_unpack") | ||||||
| find_resdata_app("rd_pack") | ||||||
| except OSError: | ||||||
| sys.exit( | ||||||
| "ERROR: rd_unpack.x and/or rd_pack.x not found.\n" | ||||||
| "These tools are required and must be installed separately" | ||||||
| ) | ||||||
|
|
||||||
| # Take special care if the UNRST file we get in is not in current directory | ||||||
| cwd = os.getcwd() | ||||||
| rstfilepath = Path(rstfilename).parent | ||||||
| tempdir = None | ||||||
| def rd_repacker(rstfilename: str, slicerstindices: list[int], quiet: bool) -> None: | ||||||
|
||||||
| """Repack a UNRST file keeping only selected restart indices. | ||||||
|
|
||||||
| try: | ||||||
| os.chdir(Path(rstfilename).parent) | ||||||
| tempdir = tempfile.mkdtemp(dir=".") | ||||||
| os.rename( | ||||||
| os.path.basename(rstfilename), | ||||||
| os.path.join(tempdir, os.path.basename(rstfilename)), | ||||||
| ) | ||||||
| os.chdir(tempdir) | ||||||
| os.system( | ||||||
| find_resdata_app("rd_unpack") + " " + os.path.basename(rstfilename) + out | ||||||
| ) | ||||||
| unpackedfiles = glob.glob("*.X*") | ||||||
| for file in unpackedfiles: | ||||||
| if int(file.split(".X")[1]) not in slicerstindices: | ||||||
| os.remove(file) | ||||||
| os.system(find_resdata_app("rd_pack") + " *.X*" + out) | ||||||
| # We are inside the tmp directory, move file one step up: | ||||||
| os.rename( | ||||||
| os.path.join(os.getcwd(), os.path.basename(rstfilename)), | ||||||
| os.path.join(os.getcwd(), "../", os.path.basename(rstfilename)), | ||||||
| ) | ||||||
| finally: | ||||||
| os.chdir(cwd) | ||||||
| if tempdir is not None: | ||||||
| shutil.rmtree(rstfilepath / tempdir) | ||||||
| Uses rd_unpack and rd_pack utilities from resdata to unpack the UNRST file, | ||||||
| remove unwanted dates, and repack into a new UNRST file. | ||||||
|
|
||||||
| Args: | ||||||
| rstfilename: Path to the UNRST file. | ||||||
| slicerstindices: List of restart indices to keep. | ||||||
| quiet: If True, suppress subprocess output. | ||||||
|
|
||||||
| Raises: | ||||||
| OSError: If rd_unpack or rd_pack tools are not found. | ||||||
larsevj marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| """ | ||||||
| rd_unpack = find_resdata_app("rd_unpack") | ||||||
| rd_pack = find_resdata_app("rd_pack") | ||||||
|
|
||||||
| rstpath = Path(rstfilename) | ||||||
| rstdir = rstpath.parent or Path(".") | ||||||
| rstname = rstpath.name | ||||||
|
|
||||||
| with _working_directory(rstdir): | ||||||
| tempdir = Path(tempfile.mkdtemp(dir=".")) | ||||||
| try: | ||||||
| # Move UNRST into temp directory and work there | ||||||
| shutil.move(rstname, tempdir / rstname) | ||||||
|
|
||||||
| with _working_directory(tempdir): | ||||||
| subprocess.run( | ||||||
| [rd_unpack, rstname], | ||||||
| capture_output=quiet, | ||||||
| check=True, | ||||||
| ) | ||||||
|
|
||||||
| for file in Path(".").glob("*.X*"): | ||||||
| index = int(file.suffix.lstrip(".X")) | ||||||
larsevj marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| if index not in slicerstindices: | ||||||
| file.unlink() | ||||||
|
|
||||||
| remaining_files = sorted(Path(".").glob("*.X*")) | ||||||
| subprocess.run( | ||||||
| [rd_pack, *[str(f) for f in remaining_files]], | ||||||
| capture_output=quiet, | ||||||
| check=True, | ||||||
| ) | ||||||
|
Comment on lines
+103
to
+119
|
||||||
|
|
||||||
| # Move result back up | ||||||
| shutil.move(rstname, f"../{rstname}") | ||||||
|
||||||
| shutil.move(rstname, f"../{rstname}") | |
| shutil.move(rstname, Path("..") / rstname) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an error occurs in rd_repacker after moving the original file into the temp directory but before successfully moving it back, the original file could be lost when the temp directory is cleaned up in the finally block. This is especially problematic when keep=False. Consider creating a temporary backup inside rd_repacker or ensuring the original file is only moved after successful processing, using a different approach like working on a copy.
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When numberofslices equals the number of existing restart points, the date_slicer logic may still try to calculate evenly-spaced dates which could theoretically select duplicates. The sorted(set(...)) on line 179 handles this, but it means the user might get fewer restart points than requested. Consider adding a check or warning when numberofslices >= len(restart_indices) to inform the user that all restart points are being kept.
Uh oh!
There was an error while loading. Please reload this page.