Skip to content

Commit 22cbf0f

Browse files
Add runtime option to particleset execute
1 parent d3786da commit 22cbf0f

2 files changed

Lines changed: 54 additions & 50 deletions

File tree

parcels/particleset.py

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -709,9 +709,10 @@ def set_variable_write_status(self, var, write_status):
709709

710710
def execute(
711711
self,
712-
endtime: timedelta | datetime,
713-
dt: np.float64 | np.float32 | timedelta,
714712
pyfunc=AdvectionRK4,
713+
endtime: timedelta | datetime | None = None,
714+
runtime=None,
715+
dt: np.float64 | np.float32 | timedelta | None = None,
715716
output_file=None,
716717
verbose_progress=True,
717718
):
@@ -726,9 +727,13 @@ def execute(
726727
Kernel function to execute. This can be the name of a
727728
defined Python function or a :class:`parcels.kernel.Kernel` object.
728729
Kernels can be concatenated using the + operator (Default value = AdvectionRK4)
729-
endtime (datetime.datetime or np.timedelta64): :
730-
End time for the timestepping loop. If a timedelta is provided, it is interpreted as the total simulation time.
731-
If a datetime is provided, it is interpreted as the end time of the simulation.
730+
endtime (datetime or timedelta): :
731+
End time for the timestepping loop. If a timedelta is provided, it is interpreted as the total simulation time. In this case,
732+
the absolute end time is the start of the fieldset's time interval plus the timedelta.
733+
If a datetime is provided, it is interpreted as the absolute end time of the simulation.
734+
runtime (timedelta):
735+
The duration of the simuulation execution. Must be a timedelta object and is required to be set when the `fieldset.time_interval` is not defined.
736+
If the `fieldset.time_interval` is defined and the runtime is provided, the end time will be the start of the fieldset's time interval plus the runtime.
732737
dt (timedelta):
733738
Timestep interval (in seconds) to be passed to the kernel.
734739
It is either a timedelta object or a double.
@@ -757,78 +762,77 @@ def execute(
757762
if output_file:
758763
output_file.metadata["parcels_kernels"] = self._kernel.name
759764

760-
# The fieldset time intervale defines the extent of time that is allowed to be
761-
# simulated. If `fieldset.time_interval` is not None, it will be used to determine the endtime (the min of endtime or fieldset.time_interval[1]).
762-
# If `fieldset.time_interval` is None, the endtime will be determined by the
763-
# `endtime` parameter or the fieldset's time dimension.
764-
# Time parameters for the main for loop are converted to floats, since the interpolation kernels expect float objects for time
765-
# The initial time (in float point) representation is t0=0.0 and time is interpreted as relative to the start of the time interval
766-
fieldset_timeinterval = self.fieldset.time_interval
767-
768-
if fieldset_timeinterval is None:
769-
if isinstance(endtime, datetime):
770-
raise NotImplementedError(
771-
"If fieldset.time_interval is None, endtime must be a timedelta not a datetime"
772-
)
773-
duration = endtime.total_seconds() # converts timedelta to seconds as float64
765+
if self.fieldset.time_interval is None:
766+
start_time = timedelta(seconds=0) # For the execution loop, we need a start time as a timedelta object
767+
if runtime is None:
768+
raise ValueError("The runtime must be provided when the time_interval is not defined for a fieldset.")
769+
770+
else:
771+
if isinstance(runtime, timedelta):
772+
end_time = runtime
773+
else:
774+
raise ValueError("The runtime must be a timedelta object")
774775

775776
else:
776-
# Get the particle time interval
777-
if isinstance(endtime, datetime):
778-
simulation_endtime = min(fieldset_timeinterval[1], endtime)
779-
if simulation_endtime < fieldset_timeinterval[1]:
780-
print(
781-
f"Simulation endtime is limited by fieldset.time_interval. End time adjusted to {simulation_endtime}"
782-
)
783-
duration = (simulation_endtime - fieldset_timeinterval[0]).total_seconds()
777+
valid_start, valid_end = self.fieldset.time_interval
778+
start_time = valid_start
784779

780+
if runtime is None:
781+
if endtime is None:
782+
raise ValueError(
783+
"Must provide either runtime or endtime when time_interval is defined for a fieldset."
784+
)
785+
# Ensure that the endtime uses the same type as the start_time
786+
if isinstance(endtime, valid_start.__class__):
787+
if endtime < valid_start:
788+
raise ValueError("The endtime must be after the start time of the fieldset.time_interval")
789+
end_time = min(endtime, valid_end)
790+
else:
791+
raise ValueError("The endtime must be of the same type as the fieldset.time_interval start time.")
785792
else:
786-
duration = endtime.total_seconds()
787-
788-
if isinstance(dt, timedelta):
789-
dt = dt.total_seconds() # convert to seconds as float64
793+
end_time = start_time + runtime
790794

791795
outputdt = output_file.outputdt if output_file else None
792796

793-
self.particledata._data["dt"][:] = dt
797+
# dt must be converted to float to avoid "TypeError: float() argument must be a string or a real number, not 'datetime.timedelta'"
798+
self.particledata._data["dt"][:] = dt.total_seconds()
794799

795800
# Set up pbar
796801
if output_file:
797802
logger.info(f"Output files are stored in {output_file.fname}.")
798803

799804
if verbose_progress:
800-
pbar = tqdm(total=abs(duration), file=sys.stdout)
805+
pbar = tqdm(total=(start_time - end_time).total_seconds(), file=sys.stdout)
801806

802807
if output_file:
803808
next_output = outputdt
804809
else:
805-
next_output = np.inf * np.sign(dt)
810+
next_output = np.inf
806811

807812
tol = 1e-12
808-
time = 0.0
809-
while time < duration and dt > 0: # Forward in time only for now
810-
# Check if we can fast-forward to the next time needed for the particles
811-
# if dt > 0:
812-
# skip_kernel = True if duration > (time + dt) else False
813-
# else:
814-
# skip_kernel = True if max(self.time) < (time + dt) else False
813+
814+
time = start_time
815+
while time <= end_time:
815816
t0 = time
816817
next_time = t0 + dt
817-
res = self._kernel.execute(self, endtime=next_time, dt=dt)
818+
# Kernel and particledata currently expect all time objects to be numpy floats.
819+
# When converting absolute times to floats, we do them all relative to the start time.
820+
# TODO: To completely support datetime or timedelta objects, this really needs to be addressed in the kernels and particledata
821+
next_time_float = (next_time - start_time).total_seconds()
822+
res = self._kernel.execute(self, endtime=next_time_float, dt=dt.total_seconds())
818823
if res == StatusCode.StopAllExecution:
819824
return StatusCode.StopAllExecution
820825

821826
# End of interaction specific code
822-
time = next_time
823-
824-
if abs(time - next_output) < tol:
827+
# TODO: Handle IO timing based of timedelta or datetime objects
828+
if abs(next_time_float - next_output) < tol:
825829
if output_file:
826-
output_file.write(self, t0)
830+
output_file.write(self, next_output)
827831
if np.isfinite(outputdt):
828-
next_output += outputdt * np.sign(dt)
832+
next_output += outputdt
829833

830834
if verbose_progress:
831-
pbar.update(abs(dt))
835+
pbar.update(dt.total_seconds())
832836

833837
if verbose_progress:
834838
pbar.close()

tests/v4/test_particleset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def test_uxstommel_gyre():
4444
lon=[30.0],
4545
lat=[5.0],
4646
depth=[50.0],
47-
time=[0.0], # important otherwise initialization appears to take forever?
47+
time=[0.0],
4848
pclass=Particle,
4949
)
50-
pset.execute(endtime=timedelta(minutes=10), dt=timedelta(seconds=60), pyfunc=AdvectionEE, verbose_progress=False)
50+
pset.execute(runtime=timedelta(minutes=10), dt=timedelta(seconds=60), pyfunc=AdvectionEE, verbose_progress=False)

0 commit comments

Comments
 (0)