Skip to content
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

Update(ModelTime): refactor ModelTime and add new features #2367

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from

Conversation

jlarsen-usgs
Copy link
Contributor

  • refactor call signature to accept perlen, nstp, and tsmult as parameters

  • added methods:

    • set_start_datetime(): allows user to set/reset the start_datetime
    • set_time_units(): allows user to set/reset the time_units
    • timeunits_from_user_input(): uses fuzzy string logic to parse user supplied time units
    • datetime_from_user_input(): converts many types of datetime representation to a standard datetime object
    • get_totim(): allows user to get the totim value at the end of a stress period or stress period/time step combination
    • get_datetime(): allows user to get the datetime value at the end of a stress period or stress period/time step combination
    • intersect(): method that allows for time intersection. Method accepts datetime representations or totim and returns either the zero based stress period or the stress period and time step combination
  • added testing for ModelTime

  • updated FloPy model classes for ModelTime changes

* refactor call signature to accept perlen, nstp, and tsmult as parameters
* added methods:
   - `set_start_datetime()`: allows user to set/reset the start_datetime
   - `set_time_units()`: allows user to set/reset the time_units
   - `timeunits_from_user_input()`: uses fuzzy string logic to parse user supplied time units
   - `datetime_from_user_input()`: converts many types of datetime representation to a standard datetime object
   - `get_totim()`: allows user to get the totim value at the end of a stress period or stress period/time step combination
   - `get_datetime()`: allows user to get the datetime value at the end of a stress period or stress period/time step combination
   - `intersect()`: method that allows for time intersection. Method accepts datetime representations or totim and returns either the zero based stress period or the stress period and time step combination

* added testing for ModelTime
* updated FloPy model classes for ModelTime changes
* add get_datetime_string() method to ModelTime to format ISO 8601 compliant date times
@wpbonelli
Copy link
Member

will this close #1915?

@jlarsen-usgs
Copy link
Contributor Author

@wpbonelli

This will close #1915. The TemporalReference class had very few uses and it's functionality has been 1) replaced and 2) improved on in this PR.

@langevin-usgs langevin-usgs marked this pull request as draft November 14, 2024 17:21
Copy link
Contributor

@langevin-usgs langevin-usgs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Josh, I had a bunch of little comments, many just about terminology. Just thought I'd bring them up for discussion before they get burned in. I think what you've done here will be widely used.

One thing to consider: we have adaptive time stepping now, which doesn't fit the mold of nstp/tsmult. Not sure how we should handle it, but I think we should leave space for it somehow. Unfortunately, we don't know the time stepping information until after the simulation runs, but we could have ModelTime load the information. We can modify ATS to write time step information, which might be helpful.

Is there a way to get a list of datetimes that correspond to the end of each time step? Seems like this would be great for making plots?

return str_rep

@staticmethod
def datetime_from_user_input(datetime_obj):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider dateutil.parser.parse here instead of rolling our own? If so, we could include dateutil as an optional dependency and benefit from any improvements made there. If so, should we use similar terminology and call this method parse instead?

Copy link
Contributor Author

@jlarsen-usgs jlarsen-usgs Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider it, but dateutil would need to be a core dependency because ModelTime is constructed much like the Grid classes on modflow model objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed this to parse_datetime


return dt

def intersect(self, datetime_obj=None, totim=None, kper_kstp=False, forgrive=False):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def intersect(self, datetime_obj=None, totim=None, kper_kstp=False, forgrive=False):
def intersect(self, datetime_obj=None, totim=None, kper_kstp=False, forgive=False):

Note that forgive is misspelled in a few places.

Also, I wonder if intersect is the best name for this? Would this be better explained with find?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the forgive flag.

We should think about the naming here, because I think that this is going to be one of the more used methods of the class. I'm not sure that find is quite right for the method name

flopy/discretization/modeltime.py Outdated Show resolved Hide resolved
flopy/discretization/modeltime.py Outdated Show resolved Hide resolved
flopy/discretization/modeltime.py Outdated Show resolved Hide resolved
flopy/discretization/modeltime.py Outdated Show resolved Hide resolved
flopy/discretization/modeltime.py Outdated Show resolved Hide resolved

return datetime_obj

def get_totim(self, kper, kstp=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a method here called get_totim which means something different from the totim method. I think people might find this confusing. We should probably try and settle on a terminology, such as start/stop or begin/end. We might also need to think about a way to distinguish between datetime and elapsed time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about get_model_time or get_end_time?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about get_kstp_kper_totim(), since I think that's what it's doing? Also I think it's important to note that simulation time might be different from elapsed time, for example if the simulation starts with a steady-state stress period of some length.

flopy/discretization/modeltime.py Outdated Show resolved Hide resolved
flopy/discretization/modeltime.py Outdated Show resolved Hide resolved
"""

def __init__(
self,
period_data=None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know period data will be computed dynamically from perlen/nstp/tsmult now, but maybe still convenient to pass in a dataframe/recarray/list of tuples.. would it be hard to keep support for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible to keep support for that, but if we added that too it would create multiple ways to instantiate the class. I personally think we should have a single standard way (either period_data or by each variable that would be in a period_data array). But I'm open to having multiple ways, it just complicates the code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd personally vote to keep the initializer parameter a table, for backwards compatibility and because it is consistent with what we do for packages. We could have a from_arrays(perlen, nstp, tsmult) class method?

In future we could maybe also consider e.g. a from_times(times) class method doing what imod-python does

@jlarsen-usgs jlarsen-usgs requested a review from aleaf December 4, 2024 16:22
raise AssertionError("datetime not properly determined from user input")


def test_timeunits_userinput_parsing():
Copy link
Contributor

@aleaf aleaf Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider refactoring test cases like this (here and in the functions below) to use @pytest.mark.parametrize. For example, this test could be refactored to:

@pytest.mark.parametrize('unit_name,user_inputs', (
    ("years", ["years", "YeaR", "yaEr", "ayer", "y", "yr", 5]),
    ("days", ["days", "Day", "dyAs", "dysa", "d", 4]),
    ("hours", ["hours", "Hour", "huors", "h", "hrs", 3]),
    ("minutes", ["minutes", "MinUte", "minte", "m", "min", 2]),
    ("seconds", ["seconds", "Second", "sedcon", "s", "sec", 1]),
    ("unknown", ["unkonwn", "undefined", "u", 0])
    ))  
def test_timeunits_userinput_parsing(unit_name,user_inputs):
    for user_input in user_inputs:
        mt_unit = ModelTime.timeunits_from_user_input(user_input)
        assert mt_unit == unit_name

This carries some advantages, in that each case is treated as a separate test (meaning all of the cases will be run unless "fail fast" is set), and the tests can be debugged individually in VS Code.

"11/12/2024",
]

valid = datetime.datetime(2024, 11, 12)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for simplicity and readability, this could be shortened to simply

assert ModelTime.parse_datetime(dt_rep) == datetime.datetime(2024, 11, 12)

same for other checks with AssertionErrors

if modeltime.start_datetime != start_datetime:
raise AssertionError("start_datetime improperly stored")

result = modeltime.intersect("3/06/2024 23:59:59")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know intersect is used elsewhere, but consider renaming this method to say what it does, for example get_kstpkper() (if I'm understanding this correctly)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants