66import time
77import os
88from multiprocessing import Pool , cpu_count
9+ import matplotlib .pyplot as plt
910
1011from .climber_functions import perturb_vectors , calculate_objective
1112from .plotting_functions import plot_input_data , plot_results as plot_results_func
@@ -45,7 +46,8 @@ def __init__(
4546 mode = 'maximize' ,
4647 target_value = None ,
4748 checkpoint_file = None ,
48- save_interval = 60
49+ save_interval = 60 ,
50+ plot_progress = None
4951 ):
5052 """Initialize HillClimber.
5153
@@ -65,6 +67,8 @@ def __init__(
6567 target_value: Target objective value for target mode (default: None)
6668 checkpoint_file: Path to save/load checkpoints (default: None)
6769 save_interval: Seconds between checkpoint saves (default: 60)
70+ plot_progress: Plot results every N minutes during optimization.
71+ If None (default), no plots are drawn during optimization.
6872
6973 Raises:
7074 ValueError: If mode is invalid or target_value missing for target mode
@@ -94,6 +98,7 @@ def __init__(
9498 self .step_size = step_size
9599 self .perturb_fraction = perturb_fraction
96100 self .temperature = temperature
101+
97102 # Convert user-provided cooling_rate to multiplicative factor
98103 # User specifies 1 - multiplicative_rate, we store the multiplicative rate
99104 self .cooling_rate = 1 - cooling_rate
@@ -102,6 +107,7 @@ def __init__(
102107 self .target_value = target_value
103108 self .checkpoint_file = checkpoint_file
104109 self .save_interval = save_interval
110+ self .plot_progress = plot_progress
105111
106112 # These will be set during climb
107113 self .best_data = None
@@ -115,6 +121,7 @@ def __init__(
115121 self .temp = temperature
116122 self .start_time = None
117123 self .last_save_time = None
124+ self .last_plot_time = None
118125
119126
120127 def save_checkpoint (self , force = False ):
@@ -162,6 +169,7 @@ def save_checkpoint(self, force=False):
162169
163170 # Create checkpoint directory if needed
164171 checkpoint_dir = os .path .dirname (self .checkpoint_file )
172+
165173 if checkpoint_dir and not os .path .exists (checkpoint_dir ):
166174 os .makedirs (checkpoint_dir )
167175
@@ -172,6 +180,74 @@ def save_checkpoint(self, force=False):
172180 print (f"Checkpoint saved: { self .checkpoint_file } " )
173181
174182
183+ def plot_progress_check (self , force = False ):
184+ """Plot optimization progress if plot_progress interval has elapsed.
185+
186+ Args:
187+ force: Plot even if plot_progress interval hasn't elapsed (default: False)
188+ """
189+
190+ if self .plot_progress is None :
191+ return
192+
193+ if self .start_time is None :
194+ return
195+
196+ current_time = time .time ()
197+
198+ if not force and self .last_plot_time is not None :
199+ if (current_time - self .last_plot_time ) / 60 < self .plot_progress :
200+ return
201+
202+ # Clear any existing plots
203+ plt .close ('all' )
204+
205+ # Clear output in Jupyter notebooks to replace previous plot
206+ try :
207+ from IPython .display import clear_output
208+ clear_output (wait = True )
209+
210+ except ImportError :
211+ # Not in IPython/Jupyter environment
212+ pass
213+
214+ # Create a result structure for single climb
215+ best_data_output = (
216+ pd .DataFrame (self .best_data , columns = self .columns )
217+ if self .is_dataframe else self .best_data
218+ )
219+
220+ # Format as expected by plot_results (single replicate)
221+ results = {
222+ 'input_data' : self .data ,
223+ 'results' : [(self .data , best_data_output , pd .DataFrame (self .steps ))]
224+ }
225+
226+ # Plot current progress
227+ elapsed_min = (current_time - self .start_time ) / 60
228+ last_elapsed_min = (self .last_plot_time - self .start_time ) / 60 if self .last_plot_time else 0
229+
230+ # Format elapsed time based on duration
231+ def format_elapsed (minutes ):
232+ if minutes < 60 :
233+ return f"{ int (minutes )} minutes"
234+ else :
235+ hours = minutes / 60
236+ return f"{ hours :.1f} hours"
237+
238+ # Check if there are any steps to plot
239+ if len (self .steps ['Step' ]) == 0 :
240+ print (f"\n No accepted steps since last progress update" )
241+ print (f"Last progress update: { format_elapsed (last_elapsed_min )} " )
242+ print (f"Current time: { format_elapsed (elapsed_min )} " )
243+
244+ else :
245+ print (f"\n Plotting progress at { format_elapsed (elapsed_min )} ..." )
246+ plot_results_func (results , plot_type = 'scatter' )
247+
248+ self .last_plot_time = current_time
249+
250+
175251 def load_checkpoint (self , checkpoint_file ):
176252 """Load optimization state from checkpoint file.
177253
@@ -359,10 +435,16 @@ def climb(self):
359435
360436 # Save checkpoint periodically
361437 self .save_checkpoint ()
438+
439+ # Plot progress periodically
440+ self .plot_progress_check ()
362441
363442 # Save final checkpoint
364443 self .save_checkpoint (force = True )
365444
445+ # Plot final results
446+ self .plot_progress_check (force = True )
447+
366448 # Convert back to DataFrame if input was DataFrame
367449 best_data_output = (
368450 pd .DataFrame (self .best_data , columns = self .columns )
@@ -457,7 +539,7 @@ def climb_parallel(self, replicates=4, initial_noise=0.0, output_file=None,
457539 data_rep , self .objective_func , self .max_time , self .step_size ,
458540 self .perturb_fraction , self .temperature , self .cooling_rate ,
459541 self .mode , self .target_value , self .is_dataframe , self .columns ,
460- checkpoint_file , self .save_interval
542+ checkpoint_file , self .save_interval , None # Disable plot_progress for parallel
461543 ))
462544
463545 # Execute in parallel
@@ -509,7 +591,8 @@ def climb_parallel(self, replicates=4, initial_noise=0.0, output_file=None,
509591 print (f"Results saved to: { output_file } " )
510592
511593 return results
512-
594+
595+
513596 def plot_input (self , plot_type = 'scatter' ):
514597 """Plot the input data distribution.
515598
@@ -588,15 +671,15 @@ def _climb_wrapper(args):
588671 Args:
589672 args: Tuple of (data_numpy, objective_func, max_time, step_size,
590673 perturb_fraction, temperature, cooling_rate, mode, target_value,
591- is_dataframe, columns, checkpoint_file, save_interval)
674+ is_dataframe, columns, checkpoint_file, save_interval, plot_progress )
592675
593676 Returns:
594677 Result from climb(): (best_data, steps_df)
595678 """
596679
597680 (data_numpy , objective_func , max_time , step_size , perturb_fraction ,
598681 temperature , cooling_rate , mode , target_value , is_dataframe , columns ,
599- checkpoint_file , save_interval ) = args
682+ checkpoint_file , save_interval , plot_progress ) = args
600683
601684 # Reconstruct original data format for HillClimber
602685 data_input = (
@@ -615,7 +698,8 @@ def _climb_wrapper(args):
615698 mode = mode ,
616699 target_value = target_value ,
617700 checkpoint_file = checkpoint_file ,
618- save_interval = save_interval
701+ save_interval = save_interval ,
702+ plot_progress = plot_progress
619703 )
620704
621705 return climber .climb ()
0 commit comments