From 5701eef56de1c99b5167648b887f998b714b5c0c Mon Sep 17 00:00:00 2001 From: Chris Hogan Date: Tue, 7 May 2019 09:19:57 -0500 Subject: [PATCH 1/4] Add stop_after_walltime and stop_on_interrupt --- doc/docs/Python_User_Interface.md | 10 +++++-- python/meep.i | 1 + python/simulation.py | 47 +++++++++++++++++++++++++++---- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/doc/docs/Python_User_Interface.md b/doc/docs/Python_User_Interface.md index 88b0050e6..afef56617 100644 --- a/doc/docs/Python_User_Interface.md +++ b/doc/docs/Python_User_Interface.md @@ -1528,11 +1528,11 @@ A common point of confusion is described in [The Run Function Is Not A Loop](The ### Run Functions -**`run(step_functions..., until=condition/time)`** +**`run(step_functions..., until=condition/time, stop_on_interrupt=False)`** — -Run the simulation until a certain time or condition, calling the given step functions (if any) at each timestep. The keyword argument `until` is *either* a number, in which case it is an additional time (in Meep units) to run for, *or* it is a function (of no arguments) which returns `True` when the simulation should stop. +Run the simulation until a certain time or condition, calling the given step functions (if any) at each timestep. The keyword argument `until` is *either* a number, in which case it is an additional time (in Meep units) to run for, *or* it is a function (of no arguments) which returns `True` when the simulation should stop. If `stop_on_interrupt` is set to `True`, instead of terminating when receiving a SIGINT or SIGTERM signal from the system, the simulation will abort time stepping and continue executing any code that follows the `run` function (e.g., outputting fields). -**`run(step_functions..., until_after_sources=condition/time)`** +**`run(step_functions..., until_after_sources=condition/time, stop_on_interrupt=False`)** — Run the simulation until all sources have turned off, calling the given step functions (if any) at each timestep. The keyword argument `until_after_sources` is either a number, in which case it is an *additional* time (in Meep units) to run for after the sources are off, *or* it is a function (of no arguments). In the latter case, the simulation runs until the sources are off *and* `condition` returns `True`. @@ -1544,6 +1544,10 @@ Return a `condition` function, suitable for passing to `until`/`until_after_sour Note that, if you make `decay_by` very small, you may need to increase the `cutoff` property of your source(s), to decrease the amplitude of the small high-frequency components that are excited when the source turns off. High frequencies near the [Nyquist frequency](https://en.wikipedia.org/wiki/Nyquist_frequency) of the grid have slow group velocities and are absorbed poorly by [PML](Perfectly_Matched_Layer.md). +**`stop_after_walltime(t)`** +— +Return a `condition` function, suitable for passing to `until`. Stops the simulation after `t` seconds of wall time have passed. + Finally, another run function, useful for computing ω(**k**) band diagrams, is: **`run_k_points(T, k_points)`** diff --git a/python/meep.i b/python/meep.i index 12a020f52..6c53bb746 100644 --- a/python/meep.i +++ b/python/meep.i @@ -1489,6 +1489,7 @@ PyObject *_get_array_slice_dimensions(meep::fields *f, const meep::volume &where scale_flux_fields, scale_force_fields, scale_near2far_fields, + stop_after_walltime, stop_when_fields_decayed, synchronized_magnetic, to_appended, diff --git a/python/simulation.py b/python/simulation.py index b051701c4..740e71858 100644 --- a/python/simulation.py +++ b/python/simulation.py @@ -5,6 +5,7 @@ import numbers import os import re +import signal import subprocess import sys import warnings @@ -1227,7 +1228,7 @@ def hook(): return dname - def _run_until(self, cond, step_funcs): + def _run_until(self, cond, step_funcs, stop_on_interrupt=False): self.interactive = False if self.fields is None: self.init_sim() @@ -1244,7 +1245,15 @@ def stop_cond(sim): step_funcs = list(step_funcs) step_funcs.append(display_progress(t0, t0 + stop_time, self.progress_interval)) - while not cond(self): + def _default_interrupt_func(): + return False + + if stop_on_interrupt: + interrupt_func = _stop_on_interrupt() + else: + interrupt_func = _default_interrupt_func + + while not cond(self) and not interrupt_func(): for func in step_funcs: _eval_step_func(self, func, 'step') self.fields.step() @@ -1262,7 +1271,7 @@ def stop_cond(sim): print("run {} finished at t = {} ({} timesteps)".format(self.run_index, self.meep_time(), self.fields.t)) self.run_index += 1 - def _run_sources_until(self, cond, step_funcs): + def _run_sources_until(self, cond, step_funcs, stop_on_interrupt=False): if self.fields is None: self.init_sim() @@ -1275,7 +1284,7 @@ def f(sim): return cond(sim) and sim.round_time() >= ts new_cond = f - self._run_until(new_cond, step_funcs) + self._run_until(new_cond, step_funcs, stop_on_interrupt) def _run_sources(self, step_funcs): self._run_sources_until(self, 0, step_funcs) @@ -1962,6 +1971,7 @@ def restart_fields(self): def run(self, *step_funcs, **kwargs): until = kwargs.pop('until', None) until_after_sources = kwargs.pop('until_after_sources', None) + stop_on_interrupt = kwargs.pop('stop_on_interrupt', False) if self.fields is None: self.init_sim() @@ -1973,9 +1983,9 @@ def run(self, *step_funcs, **kwargs): raise ValueError("Unrecognized keyword arguments: {}".format(kwargs.keys())) if until_after_sources is not None: - self._run_sources_until(until_after_sources, step_funcs) + self._run_sources_until(until_after_sources, step_funcs, stop_on_interrupt=stop_on_interrupt) elif until is not None: - self._run_until(until, step_funcs) + self._run_until(until, step_funcs, stop_on_interrupt=stop_on_interrupt) else: raise ValueError("Invalid run configuration") @@ -2425,6 +2435,31 @@ def _stop(sim): return _stop +def stop_after_walltime(t): + start = mp.wall_time() + def _stop_after_walltime(sim): + if mp.wall_time() - start > t: + return True + return False + return _stop_after_walltime + + +def _stop_on_interrupt(): + shutting_down = [False] + + def _signal_handler(sig, frame): + print("WARNING: System requested termination. Time stepping aborted.") + shutting_down[0] = True + + signal.signal(signal.SIGINT, _signal_handler) + signal.signal(signal.SIGTERM, _signal_handler) + + def _stop(): + return shutting_down[0] + + return _stop + + def synchronized_magnetic(*step_funcs): def _sync(sim, todo): sim.fields.synchronize_magnetic_fields() From a58c762b6469248f9201aeb15d5f42862de8082e Mon Sep 17 00:00:00 2001 From: Chris Hogan Date: Tue, 7 May 2019 11:59:40 -0500 Subject: [PATCH 2/4] Fix doc typo [ci skip] --- doc/docs/Python_User_Interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/docs/Python_User_Interface.md b/doc/docs/Python_User_Interface.md index afef56617..8b78e25e4 100644 --- a/doc/docs/Python_User_Interface.md +++ b/doc/docs/Python_User_Interface.md @@ -1532,7 +1532,7 @@ A common point of confusion is described in [The Run Function Is Not A Loop](The — Run the simulation until a certain time or condition, calling the given step functions (if any) at each timestep. The keyword argument `until` is *either* a number, in which case it is an additional time (in Meep units) to run for, *or* it is a function (of no arguments) which returns `True` when the simulation should stop. If `stop_on_interrupt` is set to `True`, instead of terminating when receiving a SIGINT or SIGTERM signal from the system, the simulation will abort time stepping and continue executing any code that follows the `run` function (e.g., outputting fields). -**`run(step_functions..., until_after_sources=condition/time, stop_on_interrupt=False`)** +**`run(step_functions..., until_after_sources=condition/time, stop_on_interrupt=False)`** — Run the simulation until all sources have turned off, calling the given step functions (if any) at each timestep. The keyword argument `until_after_sources` is either a number, in which case it is an *additional* time (in Meep units) to run for after the sources are off, *or* it is a function (of no arguments). In the latter case, the simulation runs until the sources are off *and* `condition` returns `True`. From bf32d75bdb2007481669d20567ef8156493f4dff Mon Sep 17 00:00:00 2001 From: Chris Hogan Date: Wed, 8 May 2019 09:53:30 -0500 Subject: [PATCH 3/4] Let until take a list of stopping conditions --- doc/docs/Python_User_Interface.md | 8 +++- python/meep.i | 1 + python/simulation.py | 63 ++++++++++++++++--------------- python/tests/simulation.py | 1 + 4 files changed, 40 insertions(+), 33 deletions(-) diff --git a/doc/docs/Python_User_Interface.md b/doc/docs/Python_User_Interface.md index 8b78e25e4..15cd7cc7b 100644 --- a/doc/docs/Python_User_Interface.md +++ b/doc/docs/Python_User_Interface.md @@ -1528,9 +1528,9 @@ A common point of confusion is described in [The Run Function Is Not A Loop](The ### Run Functions -**`run(step_functions..., until=condition/time, stop_on_interrupt=False)`** +**`run(step_functions..., until=condition/time)`** — -Run the simulation until a certain time or condition, calling the given step functions (if any) at each timestep. The keyword argument `until` is *either* a number, in which case it is an additional time (in Meep units) to run for, *or* it is a function (of no arguments) which returns `True` when the simulation should stop. If `stop_on_interrupt` is set to `True`, instead of terminating when receiving a SIGINT or SIGTERM signal from the system, the simulation will abort time stepping and continue executing any code that follows the `run` function (e.g., outputting fields). +Run the simulation until a certain time or condition, calling the given step functions (if any) at each timestep. The keyword argument `until` is *either* a number, in which case it is an additional time (in Meep units) to run for, *or* it is a function (of no arguments) which returns `True` when the simulation should stop. `until` can also be a list of stopping conditions which may include a number and additional functions. **`run(step_functions..., until_after_sources=condition/time, stop_on_interrupt=False)`** — @@ -1548,6 +1548,10 @@ Note that, if you make `decay_by` very small, you may need to increase the `cuto — Return a `condition` function, suitable for passing to `until`. Stops the simulation after `t` seconds of wall time have passed. +**`stop_on_interrupt()`** +— +Return a `condition` function, suitable for passing to `until`. Instead of terminating when receiving a SIGINT or SIGTERM signal from the system, the simulation will abort time stepping and continue executing any code that follows the `run` function (e.g., outputting fields). + Finally, another run function, useful for computing ω(**k**) band diagrams, is: **`run_k_points(T, k_points)`** diff --git a/python/meep.i b/python/meep.i index 6c53bb746..88240473f 100644 --- a/python/meep.i +++ b/python/meep.i @@ -1490,6 +1490,7 @@ PyObject *_get_array_slice_dimensions(meep::fields *f, const meep::volume &where scale_force_fields, scale_near2far_fields, stop_after_walltime, + stop_on_interrupt, stop_when_fields_decayed, synchronized_magnetic, to_appended, diff --git a/python/simulation.py b/python/simulation.py index 740e71858..fcd95b385 100644 --- a/python/simulation.py +++ b/python/simulation.py @@ -1228,32 +1228,30 @@ def hook(): return dname - def _run_until(self, cond, step_funcs, stop_on_interrupt=False): + def _run_until(self, cond, step_funcs): self.interactive = False if self.fields is None: self.init_sim() - if isinstance(cond, numbers.Number): - stop_time = cond - t0 = self.round_time() + if not isinstance(cond, list): + cond = [cond] - def stop_cond(sim): - return sim.round_time() >= t0 + stop_time + for i in range(len(cond)): + if isinstance(cond[i], numbers.Number): + stop_time = cond[i] + t0 = self.round_time() - cond = stop_cond + def stop_cond(sim): + return sim.round_time() >= t0 + stop_time - step_funcs = list(step_funcs) - step_funcs.append(display_progress(t0, t0 + stop_time, self.progress_interval)) + cond[i] = stop_cond - def _default_interrupt_func(): - return False - - if stop_on_interrupt: - interrupt_func = _stop_on_interrupt() - else: - interrupt_func = _default_interrupt_func + step_funcs = list(step_funcs) + step_funcs.append(display_progress(t0, t0 + stop_time, self.progress_interval)) + else: + assert callable(cond[i]), "Stopping condition {} is not an integer or a function".format(cond[i]) - while not cond(self) and not interrupt_func(): + while not any([x(self) for x in cond]): for func in step_funcs: _eval_step_func(self, func, 'step') self.fields.step() @@ -1271,20 +1269,24 @@ def _default_interrupt_func(): print("run {} finished at t = {} ({} timesteps)".format(self.run_index, self.meep_time(), self.fields.t)) self.run_index += 1 - def _run_sources_until(self, cond, step_funcs, stop_on_interrupt=False): + def _run_sources_until(self, cond, step_funcs): if self.fields is None: self.init_sim() - ts = self.fields.last_source_time() + if not isinstance(cond, list): + cond = [cond] - if isinstance(cond, numbers.Number): - new_cond = (ts - self.round_time()) + cond - else: - def f(sim): - return cond(sim) and sim.round_time() >= ts - new_cond = f + ts = self.fields.last_source_time() + new_conds = [] + for i in range(len(cond)): + if isinstance(cond[i], numbers.Number): + new_conds.append((ts - self.round_time()) + cond[i]) + else: + def f(sim): + return cond[i](sim) and sim.round_time() >= ts + new_conds.append(f) - self._run_until(new_cond, step_funcs, stop_on_interrupt) + self._run_until(new_conds, step_funcs) def _run_sources(self, step_funcs): self._run_sources_until(self, 0, step_funcs) @@ -1971,7 +1973,6 @@ def restart_fields(self): def run(self, *step_funcs, **kwargs): until = kwargs.pop('until', None) until_after_sources = kwargs.pop('until_after_sources', None) - stop_on_interrupt = kwargs.pop('stop_on_interrupt', False) if self.fields is None: self.init_sim() @@ -1983,9 +1984,9 @@ def run(self, *step_funcs, **kwargs): raise ValueError("Unrecognized keyword arguments: {}".format(kwargs.keys())) if until_after_sources is not None: - self._run_sources_until(until_after_sources, step_funcs, stop_on_interrupt=stop_on_interrupt) + self._run_sources_until(until_after_sources, step_funcs) elif until is not None: - self._run_until(until, step_funcs, stop_on_interrupt=stop_on_interrupt) + self._run_until(until, step_funcs) else: raise ValueError("Invalid run configuration") @@ -2444,7 +2445,7 @@ def _stop_after_walltime(sim): return _stop_after_walltime -def _stop_on_interrupt(): +def stop_on_interrupt(): shutting_down = [False] def _signal_handler(sig, frame): @@ -2454,7 +2455,7 @@ def _signal_handler(sig, frame): signal.signal(signal.SIGINT, _signal_handler) signal.signal(signal.SIGTERM, _signal_handler) - def _stop(): + def _stop(sim): return shutting_down[0] return _stop diff --git a/python/tests/simulation.py b/python/tests/simulation.py index d9d90b0c9..b468ed3fb 100644 --- a/python/tests/simulation.py +++ b/python/tests/simulation.py @@ -626,5 +626,6 @@ def mat_func(p): _check(mp.Medium(), False) _check(mat_func, False) + if __name__ == '__main__': unittest.main() From 6a4279a0a898609f322cca16003297a37706def6 Mon Sep 17 00:00:00 2001 From: Chris Hogan Date: Wed, 8 May 2019 12:00:35 -0500 Subject: [PATCH 4/4] Fix docs for run --- doc/docs/Python_User_Interface.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/docs/Python_User_Interface.md b/doc/docs/Python_User_Interface.md index 15cd7cc7b..61b215c2d 100644 --- a/doc/docs/Python_User_Interface.md +++ b/doc/docs/Python_User_Interface.md @@ -1532,9 +1532,9 @@ A common point of confusion is described in [The Run Function Is Not A Loop](The — Run the simulation until a certain time or condition, calling the given step functions (if any) at each timestep. The keyword argument `until` is *either* a number, in which case it is an additional time (in Meep units) to run for, *or* it is a function (of no arguments) which returns `True` when the simulation should stop. `until` can also be a list of stopping conditions which may include a number and additional functions. -**`run(step_functions..., until_after_sources=condition/time, stop_on_interrupt=False)`** +**`run(step_functions..., until_after_sources=condition/time)`** — -Run the simulation until all sources have turned off, calling the given step functions (if any) at each timestep. The keyword argument `until_after_sources` is either a number, in which case it is an *additional* time (in Meep units) to run for after the sources are off, *or* it is a function (of no arguments). In the latter case, the simulation runs until the sources are off *and* `condition` returns `True`. +Run the simulation until all sources have turned off, calling the given step functions (if any) at each timestep. The keyword argument `until_after_sources` is either a number, in which case it is an *additional* time (in Meep units) to run for after the sources are off, *or* it is a function (of no arguments). In the latter case, the simulation runs until the sources are off *and* `condition` returns `True`. Like `until` above, `until_after_sources` can take a list of stopping conditions. In particular, a useful value for `until_after_sources` or `until` is often `stop_when_field_decayed`, which is demonstrated in [Tutorial/Basics](Python_Tutorials/Basics.md#transmittance-spectrum-of-a-waveguide-bend):