diff --git a/epymorph/engine/mm_exec.py b/epymorph/engine/mm_exec.py index cb2187f9..587c4232 100644 --- a/epymorph/engine/mm_exec.py +++ b/epymorph/engine/mm_exec.py @@ -46,7 +46,8 @@ class StandardMovementExecutor(MovementEventsMixin, MovementExecutor): _predef_hash: int | None = None def __init__(self, ctx: RumeContext): - super().__init__() + MovementEventsMixin.__init__(self) + # If we were given a MovementSpec, we need to compile it to get its clauses. if isinstance(ctx.mm, MovementSpec): self._model = compile_spec(ctx.mm, ctx.rng) diff --git a/epymorph/event.py b/epymorph/event.py index 5680f459..4fd5a517 100644 --- a/epymorph/event.py +++ b/epymorph/event.py @@ -30,7 +30,7 @@ class SimulationEvents(Protocol): """ Protocol for Simulations that support lifecycle events. For correct operation, ensure that `on_start` is fired first, - then `on_tick` at least once, then finally `on_end`. + then `on_tick` any number of times, then finally `on_end`. """ on_start: Event[OnStart] diff --git a/epymorph/log/file.py b/epymorph/log/file.py index ea8451ef..0b819c17 100644 --- a/epymorph/log/file.py +++ b/epymorph/log/file.py @@ -19,6 +19,7 @@ def file_log( ) -> Generator[None, None, None]: """Attach file logging to a simulation.""" + # Initialize the logging system and create some Loggers for epymorph subsystems. log_handler = FileHandler(log_file, "w", "utf8") log_handler.setFormatter(Formatter(BASIC_FORMAT)) @@ -80,15 +81,14 @@ def on_movement_finish(e: OnMovementFinish) -> None: with subscriptions() as subs: # Simulation logging subs.subscribe(sim.on_start, on_start) - if sim.on_tick is not None: - subs.subscribe(sim.on_tick, on_tick) + subs.subscribe(sim.on_tick, on_tick) subs.subscribe(sim.on_end, on_end) # Geo logging will be attached if it makes sense. sim_geo = getattr(sim, 'geo', None) if isinstance(sim_geo, DynamicGeoEvents): - geo_log.info( - "Geo not loaded from cache; attributes will be lazily loaded during simulation run.") + geo_log.info("Geo not loaded from cache; " + "attributes will be lazily loaded during simulation run.") subs.subscribe(sim_geo.adrio_start, adrio_start) # Movement logging @@ -98,5 +98,9 @@ def on_movement_finish(e: OnMovementFinish) -> None: yield # to outer context + # Close out the log file. + # This isn't necessary if we're running on the CLI, but if we're in a Jupyter context, + # running the sim multiple times would keep appending to the file. + # For most use-cases, just having one sim run in the log file is preferable. epy_log.removeHandler(log_handler) epy_log.setLevel(NOTSET) diff --git a/epymorph/log/messaging.py b/epymorph/log/messaging.py index a242e4e6..ddea4fd8 100644 --- a/epymorph/log/messaging.py +++ b/epymorph/log/messaging.py @@ -1,3 +1,7 @@ +""" +Contexts which provide console messaging for epymorph processes like simulation runs and geo fetching. +It's nice to have some console output to show progress during long running tasks! +""" from contextlib import contextmanager from time import perf_counter from typing import Generator @@ -10,15 +14,12 @@ @contextmanager def sim_messaging(sim: SimulationEvents, geo_messaging=False) -> Generator[None, None, None]: """ - Attach fancy console messaging to a Simulation, e.g., a progress bar. - This creates subscriptions on `sim`'s events, so you only need to do it once - per sim. Returns `sim` as a convenience. + Attach fancy console messaging to a Simulation such as a progress bar. If `geo_messaging` is true, provide verbose messaging about geo operations (if applicable, e.g., when fetching external data). """ start_time: float | None = None - use_progress_bar = sim.on_tick is not None # If geo_messaging is true, the user has requested verbose messaging re: geo operations. # However we don't want to make a strong assertion that a sim has a geo, nor what type that geo is. @@ -30,9 +31,6 @@ def sim_messaging(sim: SimulationEvents, geo_messaging=False) -> Generator[None, if hasattr(sim, 'geo'): sim_geo = getattr(sim, 'geo') - if geo_messaging and isinstance(sim_geo, DynamicGeoEvents): - print("Geo not loaded from cache; attributes will be lazily loaded during simulation run.") - def on_start(ctx: OnStart) -> None: start_date = ctx.time_frame.start_date duration_days = ctx.time_frame.duration_days @@ -41,10 +39,7 @@ def on_start(ctx: OnStart) -> None: print(f"Running simulation ({sim.__class__.__name__}):") print(f"• {start_date} to {end_date} ({duration_days} days)") print(f"• {ctx.dim.nodes} geo nodes") - if use_progress_bar: - print(progress(0.0), end='\r') - else: - print('Running...') + print(progress(0.0), end='\r') nonlocal start_time start_time = perf_counter() @@ -52,27 +47,25 @@ def on_start(ctx: OnStart) -> None: def on_tick(tick: OnTick) -> None: print(progress(tick.percent_complete), end='\r') - def adrio_start(adrio: AdrioStart) -> None: - print(f"Uncached geo attribute requested: {adrio.attribute}. Retreiving now...") - def on_end(_: None) -> None: end_time = perf_counter() - if use_progress_bar: - print(progress(1.0)) - else: - print('Complete.') + print(progress(1.0)) if start_time is not None: print(f"Runtime: {(end_time - start_time):.3f}s") + def adrio_start(adrio: AdrioStart) -> None: + print(f"Uncached geo attribute requested: {adrio.attribute}. Retreiving now...") + # Set up a subscriptions context, subscribe our handlers, # then yield to the outer context (ostensibly where the sim will be run). with subscriptions() as subs: subs.subscribe(sim.on_start, on_start) - if sim.on_tick is not None: - subs.subscribe(sim.on_tick, on_tick) + subs.subscribe(sim.on_tick, on_tick) + subs.subscribe(sim.on_end, on_end) if geo_messaging and isinstance(sim_geo, DynamicGeoEvents): + print("Geo not loaded from cache; " + "attributes will be lazily loaded during simulation run.") subs.subscribe(sim_geo.adrio_start, adrio_start) - subs.subscribe(sim.on_end, on_end) yield # to outer context