Source code for tools.generate_configs

  1"""
  2Generate a bash script to run multiple experiments.
  3Make sure you generate configs from base BitstreamEvolution Directory.
  4
  5Origionally written by Allyn, improved and extended by Isaac.
  6
  7"""
  8
  9import os, stat
 10from typing import Generator, Any, Protocol
 11from dataclasses import dataclass
 12
 13# mkdir("data/SensitivityConfigs")
 14
 15# Set some defaults
 16base_config_path =              "data/config.ini"
 17generated_configs_dir =         "data/GeneratedConfigs"
 18generated_bash_script_path =    "data/runGeneratedConfigs.sh"
 19results_output_directory =      "data/GeneratedConfigsResults"
 20path_best_asc_in_workspace =    "./workspace/best.asc"
 21path_store_best_asc =           "data/previous_best.asc"
 22"""This variable is where the best.asc is copied to is set command to copy_best_target_path by default"""
 23
 24
 25# This will create the directory to put the generated configs in if it doesn't exist
 26if not os.path.isdir(generated_configs_dir):
 27    os.makedirs(generated_configs_dir)
 28
 29#Make the directory for the final results
 30if not os.path.isdir(results_output_directory):
 31    os.makedirs(results_output_directory)
 32
 33## Some Protocalls and Dataclasses to streamline operation & Provide good defaults
 34
[docs] 35class CommandInfo(Protocol): 36 """A protocol specifying all data needed to invoke and run evolve on the command line.""" 37 config_path:str 38 description:str 39 copy_best_asc_target_path:str|None 40 """This is the path we want to copy best.asc to once it has run successfully, None if don't want to copy it to a different location.""" 41 best_asc_workspace_path:str 42 skip_next_command_if_success:bool 43 skip_next_command_if_error:bool 44 skip_next_command_if_skipped:bool
45
[docs] 46@dataclass 47class CommandData: 48 """A class that stores all data needed to invoke and run evolve on the command line.""" 49 config_path:str 50 description:str 51 copy_best_asc_target_path:str|None = None 52 best_asc_workspace_path:str = path_best_asc_in_workspace 53 skip_next_command_if_success:bool = False 54 skip_next_command_if_error:bool = False 55 skip_next_command_if_skipped:bool = False
56 57 58# All {variables_in_brackets} need to be specified and formatted later as .format(variables_in_brackets="value") 59# It is fine to specify unused variables in .format(), but will error out if a value isn't included 60# You only need to include the values here that you don't want to inherit from your base config. 61 62######################### OLD SENSITIVITY SCRIPT ############################ 63old_sensitivity_config_base = \ 64"""[TOP-LEVEL PARAMETERS] 65base_config = {base_config_path} 66 67[FITNESS SENSITIVITY PARAMETERS] 68test_circuit = data/saved_bests/{circuit_id}.asc 69""" 70 71# return the path to the new config 72def old_sensitivity_config_generator()->Generator[CommandInfo,None,None]: 73 74 for circuit_id in range(10,510,10): 75 config_path = os.path.join(generated_configs_dir, f"{circuit_id}.ini") 76 # Create the config file using the config_base file 77 with open(config_path, "w") as config_file: 78 config_file.write(old_sensitivity_config_base.format( 79 base_config_path = base_config_path, 80 circuit_id = circuit_id 81 )) 82 83 yield CommandData(config_path=config_path,description=f"Runing sensitivity experiment on {circuit_id}.ini") 84 85 86################################## PULSE COUNT SCRIPTS ################################ 87 88pulse_count_config_base = \ 89"""[TOP-LEVEL PARAMETERS] 90base_config = {base_config_path} 91simulation_mode = FULLY_INTRINSIC 92 93[FITNESS PARAMETERS] 94fitness_func = {fitness_function} 95desired_freq = {desired_frequency} 96 97[GA PARAMETERS] 98population_size = {population_size} 99 100[STOPPING CONDITION PARAMETERS] 101generations = {generations} 102 103[LOGGING PARAMETERS] 104best_file = {best_asc_path} 105""" 106
[docs] 107def pulse_count_config_generator(target_pulses:list[int], 108 use_tolerant_ff:bool=True, 109 use_sensitive_ff:bool=True, 110 population_size:int=50, 111 max_generations:int=500, 112 store_best_circuit:bool=False, 113 skip_next_if_fail:bool=False, 114 skip_next_if_skipped:bool=False)->Generator[CommandInfo,None,None]: 115 """ 116 Generates configs for pulse_count experiments 117 118 The order experiments will run if all fitness functions are selected is: 119 tolerant -> sensitive 120 121 Parameters 122 ---------- 123 target_pulses : list[int] 124 A list of all of the target pulse counts you want to train for 125 use_tolerant_ff : bool, optional 126 If each pulse get a run using the tolerant fitness function, by default True 127 use_sensitive_ff: bool, optional 128 If each pulse get a run using the sinsitive fitness function, by default True 129 population_size: int, optional 130 This sets how many circuits are in each population, by default 50 131 max_generations: int, optional 132 Sets how many generations are allowed to run before the experiment ends, by default 500 133 store_best_circuit: bool, optional 134 If this is set the best.asc file will be copied to the data directory specified in path_store_best_asc, by default False 135 skip_next_if_fail: bool, optional 136 This will set the bash script to skip the next command it would run if this script fails, by default False 137 skip_next_if_skipped: bool, optional 138 Sets the bash script to skip the next command if this command is skipped, by default False 139 140 Yields 141 ------ 142 Generator[CommandInfo,None,None] 143 The path to the output file generated. 144 """ 145 def create_config(target_pulse_count:int,fitness_funciton:str,population_size:int,max_generations:int)->str: 146 # Generate the path for each pulse count 147 config_path=os.path.join(generated_configs_dir,f"{target_pulse_count}_with__{fitness_funciton}.ini") 148 149 with open(config_path, "w") as config_file: 150 config_file.write(pulse_count_config_base.format( 151 base_config_path = base_config_path, 152 fitness_function = fitness_funciton, 153 desired_frequency = target_pulse_count, 154 best_asc_path = path_best_asc_in_workspace, 155 population_size = population_size, 156 generations = max_generations 157 )) 158 return CommandData(config_path=config_path, 159 description=f"Pulse Count experiment targeting {target_pulse_count}Hz using fitness function {fitness_funciton}.", 160 copy_best_asc_target_path= path_store_best_asc if store_best_circuit else None, 161 skip_next_command_if_error=skip_next_if_fail, 162 skip_next_command_if_skipped=skip_next_if_skipped, 163 skip_next_command_if_success=False) 164 165 for target_pulse in target_pulses: 166 if use_sensitive_ff: 167 yield create_config(target_pulse,"SENSITIVE_PULSE_COUNT",population_size=population_size,max_generations=max_generations) 168 if use_tolerant_ff: 169 yield create_config(target_pulse,"TOLERANT_PULSE_COUNT",population_size=population_size,max_generations=max_generations)
170 171pulse_count_sensitivity_config_base = \ 172"""[TOP-LEVEL PARAMETERS] 173base_config = {base_config_path} 174simulation_mode = INTRINSIC_SENSITIVITY 175 176[FITNESS PARAMETERS] 177fitness_func = {fitness_function} 178desired_freq = {desired_frequency} 179 180[FITNESS SENSITIVITY PARAMETERS] 181test_circuit = {test_circuit_path} 182sensitivity_trials = IGNORE 183sensitivity_time = 001:00:00:00 184reading_temp_humidity = false 185""" 186
[docs] 187def pulse_count_then_sensitivity_config_generator(target_pulses:list[int], 188 use_tolerant_ff:bool=True, 189 use_sensitive_ff:bool=True, 190 population_size:int=50, 191 max_generations:int=500)->Generator[CommandInfo,None,None]: 192 """ 193 Generates configs for pulse_count experiments, 194 then follows them with the config for a sensitivity config. 195 196 The order experiments will run if all fitness functions are selected is: 197 tolerant -> sensitive 198 199 Parameters 200 ---------- 201 target_pulses : list[int] 202 A list of all of the target pulse counts you want to train for 203 use_tolerant_ff : bool, optional 204 If each pulse get a run using the tolerant fitness function, by default True 205 use_sensitive_ff: bool, optional 206 If each pulse get a run using the sinsitive fitness function, by default True 207 population_size: int, optional 208 The number of circuits in each generation for pulse count, by default 50 209 max_generations: int, optional 210 The number of generations run before ending the simulation for pulse count, by default 500 211 """ 212 def create_config_pair(target_pulse_count:int,fitness_funciton:str,pop_size:int,max_gens:int)->Generator[CommandInfo,None,None]: 213 # Generate a pulse_count_config_generator to get first config 214 use_sensitive = False 215 use_tolerant = False 216 match fitness_funciton: 217 case "SENSITIVE_PULSE_COUNT": 218 use_sensitive = True 219 case "TOLERANT_PULSE_COUNT": 220 use_tolerant = True 221 case bad_name: 222 raise ValueError(f"Fitness funciton name '{bad_name}' not recognised") 223 224 # This would be easier if we just pulled out the function the generator used, but I couldn't be bothered. 225 pc_gen = pulse_count_config_generator(target_pulses=[target_pulse_count], 226 use_sensitive_ff=use_sensitive, 227 use_tolerant_ff=use_tolerant, 228 population_size=pop_size, 229 max_generations=max_gens, 230 store_best_circuit=True, 231 skip_next_if_fail=True, 232 skip_next_if_skipped=True) 233 pulse_count_experiment = list(pc_gen)[0] # yield the first element only (there should only be 1) 234 yield pulse_count_experiment 235 236 # make sure we have a best.asc to act on. 237 if pulse_count_experiment.copy_best_asc_target_path is None: 238 raise ValueError("The pulse count must output to a target path for Sensitivity to test it.") 239 240 ## Generate the config for the Sensitivity run. 241 config_path=os.path.join(generated_configs_dir,f"Sensitivity_For_Pulse_Count_of_{target_pulse_count}_with_{fitness_funciton}.ini") 242 243 ## UPDATE SO TO GENERATE SENSITIVITY CONFIG 120 mins 244 with open(config_path, "w") as config_file: 245 config_file.write(pulse_count_sensitivity_config_base.format( 246 base_config_path = base_config_path, 247 fitness_function = fitness_funciton, 248 desired_frequency = target_pulse_count, 249 test_circuit_path = pulse_count_experiment.copy_best_asc_target_path # get the asc file stored by p_c_experiment 250 )) 251 yield CommandData(config_path=config_path, 252 description=f"Sensitivity Evaluation for Pulse Count experiment targeting {target_pulse_count}Hz with {fitness_funciton}.", 253 copy_best_asc_target_path= None, # No best_asc to copy out 254 skip_next_command_if_error=False, 255 skip_next_command_if_skipped=False, 256 skip_next_command_if_success=False) 257 258 for target_pulse in target_pulses: 259 if use_sensitive_ff: 260 configs = create_config_pair(target_pulse,"SENSITIVE_PULSE_COUNT",pop_size=population_size,max_gens=max_generations) 261 for config in configs: 262 yield config 263 if use_tolerant_ff: 264 configs = create_config_pair(target_pulse,"TOLERANT_PULSE_COUNT",pop_size=population_size,max_gens=max_generations) 265 for config in configs: 266 yield config
267 268 269############################ USEFUL UTILITIES ##############################################
[docs] 270def repeat(repeat_count:int, generator:Generator[Any,None,None])->Generator[Any,None,None]: 271 """ 272 Repeats the outputs of the instantiated generator it is passed. 273 274 Parameters 275 ---------- 276 repeat_count : int 277 number of times to duplicate the sequence 278 generator : Generator[Any,None,None] 279 Instantiated generator it duplicates 280 281 Yields 282 ------ 283 Generator[Any,None,None] 284 The repeated output of the input generator 285 """ 286 generator_results = list(generator) 287 for i in range(repeat_count): 288 for result in generator_results: 289 yield result
290 291 292## Select the config_generator you want to use and pass arguments 293## OPTIONS: 294# sensitivity_config_generator() 295# pulse_count_config_generator(target_pulses = [1000,10000], use_tolerant_ff = True, use_sensitive_ff = True) 296# repeat(2,pulse_count_config_generator(target_pulses = [1000, 10000], use_tolerant_ff = True, use_sensitive_ff = True)) 297# pulse_count_config_generator(target_pulses = [40000,20000,20000],use_tolerant_ff=False,use_sensitive_ff=True) 298config_generator: Generator[CommandInfo,None,None] = \ 299 pulse_count_then_sensitivity_config_generator(target_pulses= [1000,2000,4000],use_tolerant_ff=True, use_sensitive_ff=True,max_generations=12,population_size=53) 300## Bash File Configuration 301 302# Note that there are no spaces between variables, =, and value assigned. This is needed. 303#Bash will not recognize them as variables if there is a space in them. 304bash_head = \ 305"""#!/bin/bash 306# make sure this was generated from the BitstreamEvolution folder at the base of this directory. 307 308# This variables stores the number or errors that occour 309ErrorCounter=0 310SuccessCounter=0 311SkippedCounter=0 312UserInterruptTriggered=0 313SkipNextCommand=0 314FailedCommands='' 315SkippedCommands='' 316 317 318 319#Run Commands, log if they fail. 320""" 321 322evolve_command_base = "python3 src/evolve.py -c {config_path} -d {description} -o {output_directory}" 323 324# The || only runs 2nd if left fails, && only if left succeeds 325# parenthesis is to extend command over multiple lines 326# The not means it enters the then statement if there is an error 327# the UserInterruptTriggered=$((exitCode==130)) allows us to stop other code if user interrupt occoured (set to 1 if called) 328bash_command_wrapper_logic = \ 329""" 330Current_Command=$'{command}' 331if [ $UserInterruptTriggered -eq 0 ]; then 332if [ $SkipNextCommand -eq 0 ]; then 333{command} 334exitCode=$? 335if [ $exitCode -ne 0 ]; then #Error 336 UserInterruptTriggered=$((exitCode == 130)) 337 ((ErrorCounter=ErrorCounter+1)) && FailedCommands+="$Current_Command"+$'\\n' 338 {action_if_failure} 339else # Success 340 ((SuccessCounter=SuccessCounter+1)) 341 {action_if_success} 342fi 343else # Skipped 344 ((SkippedCounter=SkippedCounter+1)) && SkippedCommands+="$Current_Command"+$'\\n' 345 {action_if_skipped} 346fi #ctrl+c 347fi 348""" 349 350bash_tail = \ 351""" 352if [ $UserInterruptTriggered -ne 0 ]; then 353#print these results if the script had a user interrupt 354echo "=================================== USER INTERRUPT RESULTS ====================================" 355echo "Canceled Commands Due to Keyboard Interrupt while running:" 356echo $"$Current_Command" 357echo "" 358echo "Commands That Failed:" 359echo "$FailedCommands" 360echo "" 361echo "Commands Skipped:" 362echo "$SkippedCommands" 363 364echo "===============================================================================================" 365echo "$ErrorCounter of $((ErrorCounter+SuccessCounter+SkippedCounter)) commands Failed, including the one not completed." 366echo "$SkippedCounter of $((ErrorCounter+SuccessCounter+SkippedCounter)) commands were Skipped" 367echo "$(({num_commands}-(ErrorCounter+SuccessCounter+SkippedCounter))) of {num_commands} were not run at all." 368 369 370exit 1 371fi 372 373# Print out the results of the Tests 374echo "" 375echo "=================================== RESULTS ====================================" 376echo "" 377echo "Commands That Failed:" 378echo "$FailedCommands" 379echo "" 380echo "Commands Skipped:" 381echo "$SkippedCommands" 382 383echo "================================================================================" 384echo "$ErrorCounter of {num_commands} commands Failed" 385echo "$SkippedCounter of {num_commands} commands were Skipped" 386 387exit 0 388 389""" 390 391with open(generated_bash_script_path, 'w') as bash_file: 392 # Invoke the bash shell for bash script 393 bash_file.write(bash_head) 394 395 command_count = 0 396 for command_data in config_generator: 397 398 # Add a call to this 399 bash_file.write( 400 bash_command_wrapper_logic.format( 401 command = evolve_command_base.format( 402 config_path = command_data.config_path, 403 description = f'"{command_data.description}"', 404 # Note that it is important that outer string uses double quotes or this messes up wrapper logic 405 output_directory = results_output_directory 406 ), 407 action_if_failure = "SkipNextCommand=1" if command_data.skip_next_command_if_error else "SkipNextCommand=0", 408 action_if_success = ("SkipNextCommand=1" if command_data.skip_next_command_if_success else "SkipNextCommand=0") +\ 409 (f"\n\tcp {path_best_asc_in_workspace} {command_data.copy_best_asc_target_path}" if command_data.copy_best_asc_target_path is not None else ""), 410 action_if_skipped = "SkipNextCommand=1" if command_data.skip_next_command_if_skipped else "SkipNextCommand=0" 411 ) 412 ) 413 414 #count the number of commands we add 415 command_count += 1 416 417 bash_file.write(bash_tail.format(num_commands = command_count)) 418 419#ensure bash script is executable if possible, but don't require permisions to do so. 420try: 421 os.chmod(generated_bash_script_path,stat.S_IREAD 422 |stat.S_IWRITE 423 |stat.S_IRWXU 424 |stat.S_IRWXG 425 |stat.S_IRWXO) 426except PermissionError: 427 print( 428 f""" 429You may need to make the bash file executable. 430Alternatively, you could run this command with sudo privilages. (sudo python3 ...) 431To do so run the following command: 432 433chmod +x {bash_file.name} 434 435""" 436 ) 437 438completion_message=\ 439f""" 440-------------------------------------------------------------------------- 441Finished completing the bash file, and related folders. 442 443Folder containing generated partial configs: {generated_configs_dir} 444Reference base config: {base_config_path} 445Results are output to the following directory: {results_output_directory} 446 447Created the bash file at: {bash_file.name} 448--------------------------------------------------------------------------- 449 450Begin the experiment by running ./{bash_file.name} 451 452--------------------------------------------------------------------------- 453If you didn't run this script from the base of the BitstreamEvolution project, 454it is best you regenerate the file after you cd there and run the file from that folder, too. 455 456i.e. .../BitstreamEvolution$ python3 src/tools/generate_configs.py 457 .../BitstreamEvolution$ ./{bash_file.name} 458""" 459 460print(completion_message)