-
Notifications
You must be signed in to change notification settings - Fork 318
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
feat(MfSimulationList): add functionality to parse the mfsim.lst file #2005
Merged
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
95b39cd
feat(MfSimulationList): add functionality to parse the mfsim.lst file
jdhughes-usgs 014b214
* add testing for MfSimulationList
jdhughes-usgs 4136149
* isort imports
jdhughes-usgs e89a41d
* update MfSimulationList runtime function
jdhughes-usgs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import numpy as np | ||
import pytest | ||
from autotest.conftest import get_example_data_path | ||
from modflow_devtools.markers import requires_exe | ||
|
||
import flopy | ||
from flopy.mf6 import MFSimulation | ||
|
||
|
||
def base_model(sim_path): | ||
load_path = get_example_data_path() / "mf6-freyberg" | ||
|
||
sim = MFSimulation.load(sim_ws=load_path) | ||
sim.set_sim_path(sim_path) | ||
sim.write_simulation() | ||
sim.run_simulation() | ||
|
||
return sim | ||
|
||
|
||
@pytest.mark.xfail | ||
def test_mfsimlist_nofile(function_tmpdir): | ||
mfsimlst = flopy.mf6.utils.MfSimulationList(function_tmpdir / "fail.lst") | ||
|
||
|
||
@requires_exe("mf6") | ||
def test_mfsimlist_normal(function_tmpdir): | ||
sim = base_model(function_tmpdir) | ||
mfsimlst = flopy.mf6.utils.MfSimulationList(function_tmpdir / "mfsim.lst") | ||
assert mfsimlst.is_normal_termination, "model did not terminate normally" | ||
|
||
|
||
@pytest.mark.xfail | ||
def test_mfsimlist_runtime_fail(function_tmpdir): | ||
sim = base_model(function_tmpdir) | ||
mfsimlst = flopy.mf6.utils.MfSimulationList(function_tmpdir / "mfsim.lst") | ||
runtime_sec = mfsimlst.get_runtime(units="abc") | ||
|
||
|
||
@requires_exe("mf6") | ||
def test_mfsimlist_runtime(function_tmpdir): | ||
sim = base_model(function_tmpdir) | ||
mfsimlst = flopy.mf6.utils.MfSimulationList(function_tmpdir / "mfsim.lst") | ||
for sim_timer in ("elapsed", "formulate", "solution"): | ||
runtime_sec = mfsimlst.get_runtime(simulation_timer=sim_timer) | ||
if runtime_sec == np.nan: | ||
continue | ||
runtime_min = mfsimlst.get_runtime( | ||
units="minutes", simulation_timer=sim_timer | ||
) | ||
assert runtime_sec / 60.0 == runtime_min, ( | ||
f"model {sim_timer} time conversion from " | ||
+ "sec to minutes does not match" | ||
) | ||
|
||
runtime_hrs = mfsimlst.get_runtime( | ||
units="hours", simulation_timer=sim_timer | ||
) | ||
assert runtime_min / 60.0 == runtime_hrs, ( | ||
f"model {sim_timer} time conversion from " | ||
+ "minutes to hours does not match" | ||
) | ||
|
||
|
||
@requires_exe("mf6") | ||
def test_mfsimlist_iterations(function_tmpdir): | ||
it_outer_answer = 13 | ||
it_total_answer = 413 | ||
|
||
sim = base_model(function_tmpdir) | ||
mfsimlst = flopy.mf6.utils.MfSimulationList(function_tmpdir / "mfsim.lst") | ||
|
||
it_outer = mfsimlst.get_outer_iterations() | ||
assert it_outer == it_outer_answer, ( | ||
f"outer iterations is not equal to {it_outer_answer} " | ||
+ f"({it_outer})" | ||
) | ||
|
||
it_total = mfsimlst.get_total_iterations() | ||
assert it_total == it_total_answer, ( | ||
f"total iterations is not equal to {it_total_answer} " | ||
+ f"({it_total})" | ||
) | ||
|
||
|
||
@requires_exe("mf6") | ||
def test_mfsimlist_memory(function_tmpdir): | ||
total_answer = 0.000547557 | ||
virtual_answer = 0.0 | ||
|
||
sim = base_model(function_tmpdir) | ||
mfsimlst = flopy.mf6.utils.MfSimulationList(function_tmpdir / "mfsim.lst") | ||
|
||
total_memory = mfsimlst.get_memory_usage() | ||
assert total_memory == total_answer, ( | ||
f"total memory is not equal to {total_answer} " + f"({total_memory})" | ||
) | ||
|
||
virtual_memory = mfsimlst.get_memory_usage(virtual=True) | ||
assert virtual_memory == virtual_answer, ( | ||
f"virtual memory is not equal to {virtual_answer} " | ||
+ f"({virtual_memory})" | ||
) | ||
|
||
non_virtual_memory = mfsimlst.get_non_virtual_memory_usage() | ||
assert total_memory == non_virtual_memory, ( | ||
f"total memory ({total_memory}) " | ||
+ f"does not equal non-virtual memory ({non_virtual_memory})" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
import os | ||
import pathlib as pl | ||
import re | ||
|
||
import numpy as np | ||
|
||
|
||
class MfSimulationList: | ||
def __init__(self, file_name: os.PathLike): | ||
# Set up file reading | ||
if isinstance(file_name, str): | ||
file_name = pl.Path(file_name) | ||
if not file_name.is_file(): | ||
raise FileNotFoundError(f"file_name `{file_name}` not found") | ||
self.file_name = file_name | ||
self.f = open(file_name, "r", encoding="ascii", errors="replace") | ||
|
||
@property | ||
def is_normal_termination(self) -> bool: | ||
""" | ||
Determine if the simulation terminated normally | ||
|
||
Returns | ||
------- | ||
success: bool | ||
Boolean indicating if the simulation terminated normally | ||
|
||
""" | ||
# rewind the file | ||
self._rewind_file() | ||
|
||
seekpoint = self._seek_to_string("Normal termination of simulation.") | ||
self.f.seek(seekpoint) | ||
line = self.f.readline() | ||
if line == "": | ||
success = False | ||
else: | ||
success = True | ||
return success | ||
|
||
def get_runtime( | ||
self, units: str = "seconds", simulation_timer: str = "elapsed" | ||
) -> float: | ||
""" | ||
Get the elapsed runtime of the model from the list file. | ||
|
||
Parameters | ||
---------- | ||
units : str | ||
Units in which to return the timer. Acceptable values are | ||
'seconds', 'minutes', 'hours' (default is 'seconds') | ||
simulation_timer : str | ||
Timer to return. Acceptable values are 'elapsed', 'formulate', | ||
'solution' (default is 'elapsed') | ||
|
||
Returns | ||
------- | ||
out : float | ||
Floating point value with the runtime in requested units. Returns | ||
NaN if runtime not found in list file | ||
|
||
""" | ||
UNITS = ( | ||
"seconds", | ||
"minutes", | ||
"hours", | ||
) | ||
TIMERS = ( | ||
"elapsed", | ||
"formulate", | ||
"solution", | ||
) | ||
TIMERS_DICT = { | ||
"elapsed": "Elapsed run time:", | ||
"formulate": "Total formulate time:", | ||
"solution": "Total solution time:", | ||
} | ||
|
||
simulation_timer = simulation_timer.lower() | ||
if simulation_timer not in TIMERS: | ||
msg = ( | ||
"simulation_timers input variable must be " | ||
+ " ,".join(TIMERS) | ||
+ f": {simulation_timer} was specified." | ||
) | ||
raise ValueError(msg) | ||
|
||
units = units.lower() | ||
if units not in UNITS: | ||
msg = ( | ||
"units input variable must be " | ||
+ " ,".join(UNITS) | ||
+ f": {units} was specified." | ||
) | ||
raise ValueError(msg) | ||
|
||
# rewind the file | ||
self._rewind_file() | ||
|
||
seekpoint = self._seek_to_string(TIMERS_DICT[simulation_timer]) | ||
self.f.seek(seekpoint) | ||
line = self.f.readline() | ||
if line == "": | ||
return np.nan | ||
|
||
# yank out the floating point values from the Elapsed run time string | ||
times = list(map(float, re.findall(r"[+-]?[0-9.]+", line))) | ||
# pad an array with zeros and times with | ||
# [days, hours, minutes, seconds] | ||
times = np.array([0 for _ in range(4 - len(times))] + times) | ||
# convert all to seconds | ||
time2sec = np.array([24 * 60 * 60, 60 * 60, 60, 1]) | ||
times_sec = np.sum(times * time2sec) | ||
# return in the requested units | ||
if units == "seconds": | ||
return times_sec | ||
elif units == "minutes": | ||
return times_sec / 60.0 | ||
elif units == "hours": | ||
return times_sec / 60.0 / 60.0 | ||
|
||
def get_outer_iterations(self) -> int: | ||
""" | ||
Get the total outer iterations from the list file. | ||
|
||
Parameters | ||
---------- | ||
|
||
Returns | ||
------- | ||
outer_iterations : float | ||
Sum of all TOTAL ITERATIONS found in the list file | ||
|
||
""" | ||
# initialize total_iterations | ||
outer_iterations = 0 | ||
|
||
# rewind the file | ||
self._rewind_file() | ||
|
||
while True: | ||
seekpoint = self._seek_to_string("CALLS TO NUMERICAL SOLUTION IN") | ||
self.f.seek(seekpoint) | ||
line = self.f.readline() | ||
if line == "": | ||
break | ||
outer_iterations += int(line.split()[0]) | ||
|
||
return outer_iterations | ||
|
||
def get_total_iterations(self) -> int: | ||
""" | ||
Get the total number of iterations from the list file. | ||
|
||
Parameters | ||
---------- | ||
|
||
Returns | ||
------- | ||
total_iterations : float | ||
Sum of all TOTAL ITERATIONS found in the list file | ||
|
||
""" | ||
# initialize total_iterations | ||
total_iterations = 0 | ||
|
||
# rewind the file | ||
self._rewind_file() | ||
|
||
while True: | ||
seekpoint = self._seek_to_string("TOTAL ITERATIONS") | ||
self.f.seek(seekpoint) | ||
line = self.f.readline() | ||
if line == "": | ||
break | ||
total_iterations += int(line.split()[0]) | ||
|
||
return total_iterations | ||
|
||
def get_memory_usage(self, virtual=False) -> float: | ||
""" | ||
Get the simulation memory usage from the simulation list file. | ||
|
||
Parameters | ||
---------- | ||
virtual : bool | ||
Return total or virtual memory usage (default is total) | ||
|
||
Returns | ||
------- | ||
memory_usage : float | ||
Total memory usage for a simulation (in Gigabytes) | ||
|
||
""" | ||
# initialize total_iterations | ||
memory_usage = 0.0 | ||
|
||
# rewind the file | ||
self._rewind_file() | ||
|
||
tags = ( | ||
"MEMORY MANAGER TOTAL STORAGE BY DATA TYPE", | ||
"Total", | ||
"Virtual", | ||
) | ||
|
||
while True: | ||
seekpoint = self._seek_to_string(tags[0]) | ||
self.f.seek(seekpoint) | ||
line = self.f.readline() | ||
if line == "": | ||
break | ||
units = line.split()[-1] | ||
if units == "GIGABYTES": | ||
conversion = 1.0 | ||
elif units == "MEGABYTES": | ||
conversion = 1e-3 | ||
elif units == "KILOBYTES": | ||
conversion = 1e-6 | ||
elif units == "BYTES": | ||
conversion = 1e-9 | ||
else: | ||
raise ValueError(f"Unknown memory unit '{units}'") | ||
|
||
if virtual: | ||
tag = tags[2] | ||
else: | ||
tag = tags[1] | ||
seekpoint = self._seek_to_string(tag) | ||
self.f.seek(seekpoint) | ||
line = self.f.readline() | ||
if line == "": | ||
break | ||
memory_usage = float(line.split()[-1]) * conversion | ||
|
||
return memory_usage | ||
|
||
def get_non_virtual_memory_usage(self): | ||
""" | ||
|
||
Returns | ||
------- | ||
non_virtual: float | ||
Non-virtual memory usage, which is the difference between the | ||
total and virtual memory usage | ||
|
||
""" | ||
return self.get_memory_usage() - self.get_memory_usage(virtual=True) | ||
|
||
def _seek_to_string(self, s): | ||
""" | ||
Parameters | ||
---------- | ||
s : str | ||
Seek through the file to the next occurrence of s. Return the | ||
seek location when found. | ||
|
||
Returns | ||
------- | ||
seekpoint : int | ||
Next location of the string | ||
|
||
""" | ||
while True: | ||
seekpoint = self.f.tell() | ||
line = self.f.readline() | ||
if line == "": | ||
break | ||
if s in line: | ||
break | ||
return seekpoint | ||
|
||
def _rewind_file(self): | ||
""" | ||
Rewind the mfsim.lst file | ||
|
||
Returns | ||
------- | ||
|
||
""" | ||
self.f.seek(0) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
guess this is because virtual memory is only used for mf6 parallel at the moment — once flopy has more parallel support a parallel test case could be added to check virtual memory usage I imagine