1"""Logging and monitoring for evolutionary experiments.
2
3Provides console and file-based logging with ANSI color formatting,
4live plot launching, and workspace archival for experiment results.
5"""
6from dataclasses import dataclass
7from pathlib import Path
8from sys import stdout
9from datetime import datetime
10from subprocess import CalledProcessError, run
11from os.path import exists
12from os.path import join
13from os import mkdir
14from shutil import copytree
15from shutil import rmtree
16from datetime import datetime
17
18# The window dimensions
19LINE_WIDTH = 112
20WIN_DIM="105x29"
21DOUBLE_HLINE = "=" * LINE_WIDTH
22
23MONITOR_FILE = None
24
25TERM_CMD=["gnome-terminal", "--geometry={}".format(WIN_DIM), "--"]
26
27# The time between animation frames in milliseconds
28FRAME_DELAY = 200
29
30# The spacing between matplotlib subplots as a percentage of the average
31# axis height
32SUBPLOT_SPACING = 0.5
33
34# Formatting constants
35HEADER = '\033[95m'
36OKBLUE = '\033[94m'
37OKGREEN = '\033[92m'
38WARNING = '\033[93m'
39FAIL = '\033[91m'
40ENDC = '\033[0m'
41BOLD = '\033[1m'
42UNDERLINE = '\033[4m'
43
44README_FILE_HEADER = "FPGA/MCU [1] \n"
45
[docs]
46@dataclass
47class LoggerConfig:
48 """Configuration for Logger output, plot launching, and file paths.
49
50 Consumed by ``Logger.__init__`` to control verbosity, plot behavior,
51 and workspace log destinations.
52 """
53
54 plots_dir: Path
55 launch_plots: bool
56 is_sensitivity: bool
57 frame_interval: int
58 log_file: Path
59 log_level: int
60 save_log: bool
61 datetime_format: str
62
63# TODO Utilize Python logging library
[docs]
64class Logger:
65 """Level-gated logger with monitor file output and live plot integration.
66
67 Writes timestamped entries to both stdout and a monitor file, supports
68 ANSI-colored severity prefixes, and can launch gnome-terminal-based
69 live plotting windows for experiment visualization.
70 """
71 def __init_monitor(self):
72 """Initialize the monitor file header, plots directory, and live plot windows.
73
74 Writes the experiment banner to the monitor file, resets the plots
75 directory, and optionally launches a live plotting terminal.
76 """
77 # Start the monitor
78 # self.log_event(1, "Creating the monitor file...")
79
80 self.log_monitor(1, "{}{}".format(
81 "Evolutionary Experiment Monitor".center(LINE_WIDTH),
82 "\n"
83 ))
84 self.log_monitor("", "{}".format(DOUBLE_HLINE))
85 # self.log_monitor(1, "Parameters and updates load during circuit evaluation")
86 # self.log_monitor(1, ".\n" * 23)
87 self.log_monitor("", str(self.__experiment_explanation))
88 self.log_monitor("", "{}".format(DOUBLE_HLINE))
89 # self.log_monitor("", self.__config.get_raw_data())
90 self.log_monitor("", "{}".format(DOUBLE_HLINE))
91 self.__monitor_file.flush()
92
93 # args = TERM_CMD + ["python3", "src/Monitor.py"]
94 # try:
95 # run(args, check=True, capture_output=True)
96 # except OSError as e:
97 # self.log_error(1, "An error occured while launching Monitor.py")
98 # except CalledProcessError as e:
99 # self.log_error(1, "An error occured in Monitor.py")
100
101 #set up directory for saving files
102 plots_dir = self.__config.plots_dir
103 try:
104 rmtree(plots_dir)
105 except OSError as error:
106 print(error)
107
108 if not plots_dir.exists():
109 plots_dir.mkdir()
110
111 if self.__config.launch_plots:
112 if self.__config.is_sensitivity:
113 args = TERM_CMD + ["python3", "src/PlotSensitivityLive.py"]
114 else:
115 args = TERM_CMD + ["python3", "src/PlotEvolutionLive.py", "--frame-interval", str(self.__config.frame_interval)]
116
117 try:
118 run(args, check=True, capture_output=True)
119 except OSError as e:
120 self.log_error(1, "An error occured while launching PlotEvolutionLive.py")
121 except CalledProcessError as e:
122 self.log_error(1, "An error occured in PlotEvolutionLive.py")
123 self.log_error(1, e)
124
[docs]
125 def __init__(self, explanation: str, logger_config: LoggerConfig):
126 """Set up log files, clear workspace data logs, and start the monitor.
127
128 Parameters
129 ----------
130 explanation : str
131 Human-readable description of the experiment.
132 logger_config : LoggerConfig
133 Configuration controlling log paths, verbosity, and plot behavior.
134 """
135 self.__monitor_file = open(logger_config.log_file, "w")
136 self.__log_file = stdout
137 self.__config = logger_config
138 self.__experiment_explanation = explanation
139
140 # Ensure the logs exists and have been cleared. Not happy with
141 # this method, but couldn't find a better way to do it.
142 open("workspace/alllivedata.log", "w").close()
143 open("workspace/bestlivedata.log", "w").close()
144 open("workspace/waveformlivedata.log", "w").close()
145 open("workspace/maplivedata.log", "w").close()
146 open("workspace/heatmaplivedata.log", "w").close()
147 open("workspace/pulselivedata.log", "w").close()
148 open("workspace/violinlivedata.log", "w").close()
149 open("workspace/poplivedata.log", "w").close()
150 open("workspace/randomizationdata.log", "w").close()
151 open("workspace/fitnesssensitivity.log", "w").close()
152 open("workspace/bitstream_avg.log", "w").close()
153 if not exists("workspace/template"):
154 mkdir("workspace/template")
155
156 if exists("workspace/plots"):
157 rmtree("workspace/plots")
158 if not exists("workspace/plots"):
159 mkdir("workspace/plots")
160 # Determine if we need to the to initialize the analysis and
161 # if so, do so.
162 # currently removed since we're not currently storing any data, so there's a bunch of empty files and directories
163 # if explanation != "test":
164 # self.__init_analysis()
165
166 # Determine whether we need to launch the monitor and launch it
167 # if so.
168 # if config.get_launch_monitor():
169 # self.__init_monitor()
170 self.__init_monitor()
171
[docs]
172 def log_generation(self, population, epoch_time):
173 """Log a generation summary including current and overall best fitness.
174
175 Parameters
176 ----------
177 population : Population
178 The population whose best circuits are reported.
179 epoch_time : float
180 Wall-clock seconds elapsed during the epoch.
181 """
182 self.log_event(2, DOUBLE_HLINE)
183 self.log_event(2, DOUBLE_HLINE)
184 self.log_event(2, DOUBLE_HLINE)
185
186 current_best_circuit = population.get_current_best_circuit()
187 overall_best_circuit = population.get_overall_best_circuit_info()
188
189 self.log_event(2, "CURRENT BEST: {} : EPOCH {} : FITNESS {}".format(
190 str(overall_best_circuit.name),
191 str(population.get_best_epoch()),
192 str(overall_best_circuit.fitness)
193 ))
194
195 self.log_event(2, "HIGHEST FITNESS OF EPOCH {} IS: {} = {} over {} seconds".format(
196 str(population.get_current_epoch()),
197 str(current_best_circuit),
198 str(current_best_circuit.get_fitness()),
199 str(epoch_time)
200 ))
201
202 self.log_event(2, DOUBLE_HLINE)
203 self.log_event(2, DOUBLE_HLINE)
204 self.log_event(2, DOUBLE_HLINE)
205
[docs]
206 def log_monitor(self, prefix, *msg):
207 """Write a timestamped message directly to the monitor log file."""
208 if self.__config.save_log:
209 now = datetime.now()
210 print(now, prefix, *msg, file=self.__monitor_file)
211
[docs]
212 def log_event(self, level, *msg):
213 """Log a general event to stdout and the monitor file if level permits."""
214 if self.__config.log_level >= level:
215 print(*msg, file=self.__log_file)
216 self.log_monitor("", *msg)
217
[docs]
218 def log_info(self, level, *msg):
219 """Log an informational message with blue ANSI formatting."""
220 if self.__config.log_level >= level:
221 print("INFO: ", OKBLUE, *msg, ENDC, file=self.__log_file)
222 self.log_monitor("INFO: ", *msg)
223
[docs]
224 def log_warning(self, level, *msg):
225 """Log a warning message with yellow ANSI formatting."""
226 if self.__config.log_level >= level:
227 print("WARNING: ", WARNING, *msg, ENDC, file=self.__log_file)
228 self.log_monitor("WARNING: ", *msg)
229
[docs]
230 def log_error(self, level, *msg):
231 """Log an error message with red ANSI formatting."""
232 if self.__config.log_level >= level:
233 print("ERROR: ", FAIL, *msg, ENDC, file=self.__log_file)
234 self.log_monitor("ERROR: ", *msg)
235
[docs]
236 def log_critical(self, level, *msg):
237 """Log a critical message with red ANSI formatting."""
238 if self.__config.log_level >= level:
239 print("CRITICAL: ", FAIL, *msg, ENDC, file=self.__log_file)
240 self.log_monitor("CRITICAL: ", *msg)
241
[docs]
242 def save_workspace(self, directory):
243 """Close the monitor file and archive the workspace to a timestamped directory."""
244 self.__monitor_file.close()
245 current_time = str(datetime.now().strftime(self.__config.datetime_format))
246 current_time = current_time.replace('/', '-')
247 copytree("./workspace", join(directory, current_time))