diff --git a/febid/Statistics.py b/febid/Statistics.py index 4e728ed..2d431a0 100644 --- a/febid/Statistics.py +++ b/febid/Statistics.py @@ -6,7 +6,7 @@ import time import timeit from math import floor, log -from threading import Thread +from threading import Thread, Condition from dataclasses import dataclass import numpy as np @@ -20,8 +20,19 @@ class SynchronizationHelper: """ Secures a flag that serves as a signal to the threads that have it weather to stop execution or not. True to stop execution, False to continue. + Also contains timer that counts intrinsic simulation time. """ run_flag: bool + loop_tick: Condition = Condition() # this allows the thread to pause instead of constantly looping + _current_time: float = 0 + + @property + def timer(self): + return self._current_time + + @timer.setter + def timer(self, value): + self._current_time = value def __repr__(self): return str(self.run_flag) @@ -37,13 +48,21 @@ def __init__(self, run_flag: SynchronizationHelper, refresh_rate, purpose='Unide self.refresh_rate = refresh_rate self.purpose = purpose self.start_time = timeit.default_timer() + self.start_time_sim = run_flag.timer + self.passed_time = run_flag.timer def run(self): print(f'Starting {self.purpose} daemon.') + next_record_time = self.passed_time + self.refresh_rate + self.run_flag.loop_tick.acquire() while not self.run_flag: - self.looped_func() - time.sleep(self.refresh_rate) + self.run_flag.loop_tick.wait() + if next_record_time < self.run_flag.timer: + self.passed_time = self.run_flag.timer + next_record_time = self.passed_time + self.refresh_rate + self.looped_func() self.looped_func(end=True) + self.run_flag.loop_tick.release() print(f'Closing {self.purpose} daemon.') def looped_func(self, end=False): @@ -182,6 +201,13 @@ def save_to_file(self, force=False): The gathered statistics are appended to the end of the table every couple of seconds Caution: the session keeps the file open until it finishes. """ + + def write_to_file(data, header, last_row): + args, kwargs = self.__get_writer_args_and_kwargs() + with pd.ExcelWriter(*args, **kwargs) as writer: + data.to_excel(writer, startrow=last_row, sheet_name=self.sheet_name, header=header) + self.last_row = writer.sheets[self.sheet_name].max_row + if timeit.default_timer() - self.time <= self.save_freq and not force: return else: @@ -193,14 +219,13 @@ def save_to_file(self, force=False): last_row = self.last_row header = False data = self.data.iloc[last_row:] - try: - args, kwargs = self.__get_writer_args_and_kwargs() - with pd.ExcelWriter(*args, **kwargs) as writer: - data.to_excel(writer, startrow=last_row, sheet_name=self.sheet_name, header=header) - self.last_row = writer.sheets[self.sheet_name].max_row - except Exception as e: - print(f'Was unable to save statistics to file, the following error occurred: {e.args}') - sys.exit() + while True: + try: + write_to_file(data, header, last_row) + break + except PermissionError as e: + print(f'Was unable to save statistics to file, the following error occurred: {e.args}') + input('Please close the file and press Enter to continue recording.') def get_growth_rate(self): delta = 4 diff --git a/febid/febid_core.py b/febid/febid_core.py index f5b279e..4520488 100644 --- a/febid/febid_core.py +++ b/febid/febid_core.py @@ -144,25 +144,28 @@ def run_febid(structure, precursor_params, settings, sim_params, path, temperatu stats.get_params(precursor_params, 'Precursor parameters') stats.get_params(settings, 'Beam parameters and settings') stats.get_params(sim_params, 'Simulation volume parameters') - process_obj.stats_frequency = min(saving_params['gather_stats_interval'], saving_params['save_snapshot_interval'], - rendering.get('frame_rate', 1)) + process_obj.stats_frequency = min(saving_params.get('gather_stats_interval', 1), + saving_params.get('save_snapshot_interval', 1), + rendering.get('frame_rate', 1)) stats.start() if saving_params['save_snapshot']: - struc = StructureSaver(process_obj, flag, saving_params['save_snapshot'], saving_params['filename']) + struc = StructureSaver(process_obj, flag, saving_params['save_snapshot_interval'], saving_params['filename']) struc.start() printing.start() - if rendering['show_process']: - visualize_process(process_obj, flag, **rendering) + if rendering['show_process']: # running visualization in the main loop + total_time = visualize_process(process_obj, flag, **rendering) printing.join() if saving_params['gather_stats']: stats.join() if saving_params['save_snapshot']: struc.join() print('Finished path.') + if rendering['show_process']: + visualize_result(process_obj, total_time, **rendering) return process_obj, sim -def print_all(path, pr, sim, run_flag): +def print_all(path, pr: Process, sim: MC_Simulation, run_flag: SynchronizationHelper): """ Main event loop, that iterates through consequent points in a stream-file. @@ -172,6 +175,7 @@ def print_all(path, pr, sim, run_flag): :param run_flag: :return: """ + pr.start_time = datetime.datetime.now() pr.x0, pr.y0 = path[0, 0:2] start = 0 total_time = int(path[:, 2].sum() * pr.deposition_scaling * 1e6) @@ -189,11 +193,11 @@ def print_all(path, pr, sim, run_flag): if pr.temperature_tracking: pr.heat_transfer(sim.beam_heating) pr.request_temp_recalc = False - print_step(y, x, step, pr, sim, t) + print_step(y, x, step, pr, sim, t, run_flag) run_flag.run_flag = True -def print_step(y, x, dwell_time, pr: Process, sim, t): +def print_step(y, x, dwell_time, pr: Process, sim: MC_Simulation, t, run_flag: SynchronizationHelper): """ Sub-loop, that iterates through the dwell time by a time step @@ -203,6 +207,7 @@ def print_step(y, x, dwell_time, pr: Process, sim, t): :param pr: Process object :param sim: MC simulation object :param t: tqdm progress bar + :param run_flag: Thread synchronization object :return: """ @@ -240,47 +245,59 @@ def print_step(y, x, dwell_time, pr: Process, sim, t): pr.precursor_density() # recalculate precursor coverage pr.t += pr.dt * pr.deposition_scaling time_passed += pr.dt + run_flag.timer = pr.t t.update(pr.dt * pr.deposition_scaling * 1e6) if time_passed % pr.stats_frequency < pr.dt * 1.5: pr.min_precursor_coverage = pr.precursor_min pr.dep_vol = pr.deposited_vol pr.reset_dt() + # Allow only one tick of the loop for daemons per one tick of simulation + run_flag.loop_tick.acquire() + run_flag.loop_tick.notify_all() + run_flag.loop_tick.release() -def visualize_process(pr: Process, run_flag, show_process=False, frame_rate=1, displayed_data='precursor'): +def visualize_process(pr: Process, run_flag, frame_rate=1, displayed_data='precursor', **kwargs): """ A daemon process function to manage statistics gathering and graphics update. :param pr: object of the core deposition process - :param run_flag: - :param show_process: True will enable graphical monitoring of the process + :param run_flag: thread synchronization object, allows to stop visualization when simulation concludes :param frame_rate: redrawing delay - :param displayed_data: name of the displayed data + :param displayed_data: name of the displayed data. Options: 'precursor', 'deposit', 'temperature', 'surface_temperature' :return: """ - - # When deposition process thread finishes, it sets flag to False which will finish current thread - - pr.start_time = datetime.datetime.now() start_time = timeit.default_timer() # Initializing graphical monitoring - if show_process: - rn = vr.Render(pr.structure.cell_size) - rn.p.clear - pr.redraw = True - # Event loop - while not run_flag: - now = timeit.default_timer() - update_graphical(rn, pr, now - start_time, displayed_data) - time.sleep(frame_rate) - print('Rendering last frame interactively.') + + rn = vr.Render(pr.structure.cell_size) + rn.p.clear() + pr.redraw = True + now = 0 + # Event loop + while not run_flag: now = timeit.default_timer() - rn.p.close() - rn = vr.Render(pr.structure.cell_size) - pr.redraw = True - update_graphical(rn, pr, now - start_time, displayed_data, False) - rn.show(interactive_update=False) - print('Closing rendering.') + update_graphical(rn, pr, now - start_time, displayed_data) + time.sleep(frame_rate) + rn.p.close() + print('Closing rendering.') + return now - start_time + + +def visualize_result(pr, total_time, displayed_data='precursor', **kwargs): + """ + Rendering the final state of the process. + + :param pr: object of the core deposition process + :param displayed_data: name of the displayed data + :param total_time: total time of the simulation + :return: + """ + print('Rendering last frame interactively.') + rn = vr.Render(pr.structure.cell_size) + pr.redraw = True + update_graphical(rn, pr, total_time, displayed_data, False) + rn.show(interactive_update=False) def update_graphical(rn: vr.Render, pr: Process, time_spent, displayed_data='precursor', update=True): @@ -375,7 +392,7 @@ def update_graphical(rn: vr.Render, pr: Process, time_spent, displayed_data='pre f'Sim. time: {(pr.t):.8f} s \n' # showing simulation time passed f'Speed: {speed:.8f} \n' f'Av. growth rate: {growth_rate} nm^3/s \n' - f'Max. temperature: {max_T:.3f} K') + f'Max. temperature: {max_T:.1f} K') rn.p.actors['stats'].SetText(3, f'Cells: {pr.n_filled_cells[i]} \n' # showing total number of deposited cells f'Height: {height} nm \n' @@ -397,10 +414,6 @@ def update_graphical(rn: vr.Render, pr: Process, time_spent, displayed_data='pre return 0 -def dump_structure(structure: Structure, sim_t=None, t=None, beam_position=None, filename='FEBID_result'): - vr.save_deposited_structure(structure, sim_t, t, beam_position, filename) - - if __name__ == '__main__': print('##################### FEBID Simulator ###################### \n') print('Please use `python -m febid` for launching') diff --git a/requirements.txt b/requirements.txt index ab3cf5c..2809046 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ openpyxl tqdm pyqt5 pyaml +vtk==9.3.1 numexpr_mod \ No newline at end of file