Skip to content

Commit 3d13d91

Browse files
authored
[BUGFIX] Create reproducible version of random search layout optimizer for testing (#940)
* Rearrange optimize() method to call private methods. * Add _test_optimize() routine; remove unneded indent. * Enable fixed iteration number in individual optimization for debugging purposes. * Use _test_optimize() for reg test and update reg test results.
1 parent 1ac4de2 commit 3d13d91

File tree

2 files changed

+176
-123
lines changed

2 files changed

+176
-123
lines changed

floris/optimization/layout_optimization/layout_optimization_random_search.py

+171-118
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ def __init__(
328328
# Delete stored x and y to avoid confusion
329329
del self.x, self.y
330330

331+
# Set up to run in normal mode
332+
self.debug = False
333+
331334
def describe(self):
332335
print("Random Layout Optimization")
333336
print(f"Number of turbines to optimize = {self.N_turbines}")
@@ -491,74 +494,78 @@ def _get_initial_and_final_locs(self):
491494
y_opt = self.y_opt
492495
return x_initial, y_initial, x_opt, y_opt
493496

494-
495-
# Public methods
496-
497-
def optimize(self):
497+
def _initialize_optimization(self):
498498
"""
499-
Perform the optimization
499+
Set up logs etc
500500
"""
501501
print(f'Optimizing using {self.n_individuals} individuals.')
502-
opt_start_time = timerpc()
503-
opt_stop_time = opt_start_time + self.total_optimization_seconds
504-
sim_time = 0
502+
self._opt_start_time = timerpc()
503+
self._opt_stop_time = self._opt_start_time + self.total_optimization_seconds
505504

506505
self.objective_candidate_log = [self.objective_candidate.copy()]
507506
self.num_objective_calls_log = []
508507
self._num_objective_calls = [0]*self.n_individuals
509508

510-
while timerpc() < opt_stop_time:
511-
512-
# Set random seed for the main loop
513-
if self.random_seed is None:
514-
multi_random_seeds = [None]*self.n_individuals
515-
else:
516-
multi_random_seeds = [55 + self.iteration_step + i
517-
for i in range(self.n_individuals)]
518-
# 55 is just an arbitrary choice to ensure different random seeds
519-
# to the initialization code
520-
521-
# Update the optimization time
522-
sim_time = timerpc() - opt_start_time
523-
print(f'Optimization time: {sim_time:.1f} s / {self.total_optimization_seconds:.1f} s')
524-
525-
526-
# Generate the multiargs for parallel execution of single individual optimization
527-
multiargs = [
528-
(self.seconds_per_iteration,
529-
self.objective_candidate[i],
530-
self.x_candidate[i, :],
531-
self.y_candidate[i, :],
532-
self.fmodel_dict,
533-
self.fmodel.wind_data,
534-
self.min_dist,
535-
self._boundary_polygon,
536-
self.distance_pmf,
537-
self.enable_geometric_yaw,
538-
multi_random_seeds[i],
539-
self.use_value
540-
)
541-
for i in range(self.n_individuals)
542-
]
509+
def _run_optimization_generation(self):
510+
"""
511+
Run a generation of the outer genetic algorithm
512+
"""
513+
# Set random seed for the main loop
514+
if self.random_seed is None:
515+
multi_random_seeds = [None]*self.n_individuals
516+
else:
517+
multi_random_seeds = [55 + self.iteration_step + i
518+
for i in range(self.n_individuals)]
519+
# 55 is just an arbitrary choice to ensure different random seeds
520+
# to the initialization code
543521

544-
# Run the single individual optimization in parallel
545-
if self._PoolExecutor: # Parallelized
546-
with self._PoolExecutor(self.max_workers) as p:
547-
out = p.starmap(_single_individual_opt, multiargs)
548-
else: # Parallelization not activated
549-
out = [_single_individual_opt(*multiargs[0])]
522+
# Update the optimization time
523+
sim_time = timerpc() - self._opt_start_time
524+
print(f'Optimization time: {sim_time:.1f} s / {self.total_optimization_seconds:.1f} s')
550525

551-
# Unpack the results
552-
for i in range(self.n_individuals):
553-
self.objective_candidate[i] = out[i][0]
554-
self.x_candidate[i, :] = out[i][1]
555-
self.y_candidate[i, :] = out[i][2]
556-
self._num_objective_calls[i] = out[i][3]
557-
self.objective_candidate_log.append(self.objective_candidate)
558-
self.num_objective_calls_log.append(self._num_objective_calls)
559526

560-
# Evaluate the individuals for this step
561-
self._evaluate_opt_step()
527+
# Generate the multiargs for parallel execution of single individual optimization
528+
multiargs = [
529+
(self.seconds_per_iteration,
530+
self.objective_candidate[i],
531+
self.x_candidate[i, :],
532+
self.y_candidate[i, :],
533+
self.fmodel_dict,
534+
self.fmodel.wind_data,
535+
self.min_dist,
536+
self._boundary_polygon,
537+
self.distance_pmf,
538+
self.enable_geometric_yaw,
539+
multi_random_seeds[i],
540+
self.use_value,
541+
self.debug
542+
)
543+
for i in range(self.n_individuals)
544+
]
545+
546+
# Run the single individual optimization in parallel
547+
if self._PoolExecutor: # Parallelized
548+
with self._PoolExecutor(self.max_workers) as p:
549+
out = p.starmap(_single_individual_opt, multiargs)
550+
else: # Parallelization not activated
551+
out = [_single_individual_opt(*multiargs[0])]
552+
553+
# Unpack the results
554+
for i in range(self.n_individuals):
555+
self.objective_candidate[i] = out[i][0]
556+
self.x_candidate[i, :] = out[i][1]
557+
self.y_candidate[i, :] = out[i][2]
558+
self._num_objective_calls[i] = out[i][3]
559+
self.objective_candidate_log.append(self.objective_candidate)
560+
self.num_objective_calls_log.append(self._num_objective_calls)
561+
562+
# Evaluate the individuals for this step
563+
self._evaluate_opt_step()
564+
565+
def _finalize_optimization(self):
566+
"""
567+
Package and print final results.
568+
"""
562569

563570
# Finalize the result
564571
self.objective_final = self.objective_candidate[0]
@@ -572,8 +579,42 @@ def optimize(self):
572579
f" {self._obj_unit} ({increase:+.2f}%)"
573580
)
574581

582+
def _test_optimize(self):
583+
"""
584+
Perform a fixed number of iterations with a single worker for
585+
debugging and testing purposes.
586+
"""
587+
# Set up a minimal problem to run on a single worker
588+
print("Running test optimization on a single worker.")
589+
self._PoolExecutor = None
590+
self.max_workers = None
591+
self.n_individuals = 1
592+
self.debug = True
593+
594+
self._initialize_optimization()
595+
596+
# Run 2 generations
597+
for _ in range(2):
598+
self._run_optimization_generation()
599+
600+
self._finalize_optimization()
601+
575602
return self.objective_final, self.x_opt, self.y_opt
576603

604+
# Public methods
605+
def optimize(self):
606+
"""
607+
Perform the optimization
608+
"""
609+
self._initialize_optimization()
610+
611+
# Run generations until the overall stop time
612+
while timerpc() < self._opt_stop_time:
613+
self._run_optimization_generation()
614+
615+
self._finalize_optimization()
616+
617+
return self.objective_final, self.x_opt, self.y_opt
577618

578619
# Helpful visualizations
579620
def plot_distance_pmf(self, ax=None):
@@ -605,7 +646,8 @@ def _single_individual_opt(
605646
dist_pmf,
606647
enable_geometric_yaw,
607648
s,
608-
use_value
649+
use_value,
650+
debug
609651
):
610652
# Set random seed
611653
np.random.seed(s)
@@ -639,69 +681,80 @@ def _single_individual_opt(
639681
# disabled.
640682
use_momentum = False
641683

684+
# Special handling for debug mode
685+
if debug:
686+
debug_iterations = 100
687+
stop_time = np.inf
688+
dd = 0
689+
642690
# Loop as long as we've not hit the stop time
643691
while timerpc() < stop_time:
644692

645-
if not use_momentum:
646-
get_new_point = True
647-
648-
if get_new_point: #If the last test wasn't successful
649-
650-
# Randomly select a turbine to nudge
651-
tr = np.random.randint(0,num_turbines)
652-
653-
# Randomly select a direction to nudge in (uniform direction)
654-
rand_dir = np.random.uniform(low=0.0, high=2*np.pi)
655-
656-
# Randomly select a distance to travel according to pmf
657-
rand_dist = np.random.choice(dist_pmf["d"], p=dist_pmf["p"])
658-
659-
# Get a new test point
660-
test_x = layout_x[tr] + np.cos(rand_dir) * rand_dist
661-
test_y = layout_y[tr] + np.sin(rand_dir) * rand_dist
662-
663-
# In bounds?
664-
if not test_point_in_bounds(test_x, test_y, poly_outer):
665-
get_new_point = True
666-
continue
667-
668-
# Make a new layout
669-
original_x = layout_x[tr]
670-
original_y = layout_y[tr]
671-
layout_x[tr] = test_x
672-
layout_y[tr] = test_y
673-
674-
# Acceptable distances?
675-
if not test_min_dist(layout_x, layout_y,min_dist):
676-
# Revert and continue
677-
layout_x[tr] = original_x
678-
layout_y[tr] = original_y
679-
get_new_point = True
680-
continue
681-
682-
# Does it improve the objective?
683-
if enable_geometric_yaw: # Select appropriate yaw angles
684-
yaw_opt.fmodel_subset.set(layout_x=layout_x, layout_y=layout_y)
685-
df_opt = yaw_opt.optimize()
686-
yaw_angles = np.vstack(df_opt['yaw_angles_opt'])
687-
688-
num_objective_calls += 1
689-
test_objective = _get_objective(layout_x, layout_y, fmodel_, yaw_angles, use_value)
690-
691-
if test_objective > current_objective:
692-
# Accept the change
693-
current_objective = test_objective
694-
695-
# If not a random point this cycle and it did improve things
696-
# try not getting a new point
697-
# Feature is currently disabled by use_momentum flag
698-
get_new_point = False
699-
700-
else:
701-
# Revert the change
702-
layout_x[tr] = original_x
703-
layout_y[tr] = original_y
704-
get_new_point = True
693+
if debug and dd >= debug_iterations:
694+
break
695+
elif debug:
696+
dd += 1
697+
698+
if not use_momentum:
699+
get_new_point = True
700+
701+
if get_new_point: #If the last test wasn't successful
702+
703+
# Randomly select a turbine to nudge
704+
tr = np.random.randint(0,num_turbines)
705+
706+
# Randomly select a direction to nudge in (uniform direction)
707+
rand_dir = np.random.uniform(low=0.0, high=2*np.pi)
708+
709+
# Randomly select a distance to travel according to pmf
710+
rand_dist = np.random.choice(dist_pmf["d"], p=dist_pmf["p"])
711+
712+
# Get a new test point
713+
test_x = layout_x[tr] + np.cos(rand_dir) * rand_dist
714+
test_y = layout_y[tr] + np.sin(rand_dir) * rand_dist
715+
716+
# In bounds?
717+
if not test_point_in_bounds(test_x, test_y, poly_outer):
718+
get_new_point = True
719+
continue
720+
721+
# Make a new layout
722+
original_x = layout_x[tr]
723+
original_y = layout_y[tr]
724+
layout_x[tr] = test_x
725+
layout_y[tr] = test_y
726+
727+
# Acceptable distances?
728+
if not test_min_dist(layout_x, layout_y,min_dist):
729+
# Revert and continue
730+
layout_x[tr] = original_x
731+
layout_y[tr] = original_y
732+
get_new_point = True
733+
continue
734+
735+
# Does it improve the objective?
736+
if enable_geometric_yaw: # Select appropriate yaw angles
737+
yaw_opt.fmodel_subset.set(layout_x=layout_x, layout_y=layout_y)
738+
df_opt = yaw_opt.optimize()
739+
yaw_angles = np.vstack(df_opt['yaw_angles_opt'])
740+
741+
num_objective_calls += 1
742+
test_objective = _get_objective(layout_x, layout_y, fmodel_, yaw_angles, use_value)
743+
744+
if test_objective > current_objective:
745+
# Accept the change
746+
current_objective = test_objective
747+
748+
# If not a random point this cycle and it did improve things
749+
# try not getting a new point
750+
# Feature is currently disabled by use_momentum flag
751+
get_new_point = False
752+
753+
else:
754+
# Revert the change
755+
layout_x[tr] = original_x
756+
layout_y[tr] = original_y
757+
get_new_point = True
705758

706759
# Return the best result from this individual
707760
return current_objective, layout_x, layout_y, num_objective_calls

tests/reg_tests/random_search_layout_opt_regression_test.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717

1818
locations_baseline_aep = np.array(
1919
[
20-
[0.0, 619.07183266, 1260.0],
21-
[0.0, 499.88056089, 0.0]
20+
[0.0, 243.05304475, 1260.0],
21+
[0.0, 959.83979244, 0.0],
2222
]
2323
)
24-
baseline_aep = 44798828639.17205
24+
baseline_aep = 45226182795.34081
2525

2626
locations_baseline_value = np.array(
2727
[
@@ -68,7 +68,7 @@ def test_random_search_layout_opt(sample_inputs_fixture):
6868
use_dist_based_init=False,
6969
random_seed=0,
7070
)
71-
sol = layout_opt.optimize()
71+
sol = layout_opt._test_optimize()
7272
optimized_aep = sol[0]
7373
locations_opt = np.array([sol[1], sol[2]])
7474

@@ -130,7 +130,7 @@ def test_random_search_layout_opt_value(sample_inputs_fixture):
130130
random_seed=0,
131131
use_value=True,
132132
)
133-
sol = layout_opt.optimize()
133+
sol = layout_opt._test_optimize()
134134
optimized_value = sol[0]
135135
locations_opt = np.array([sol[1], sol[2]])
136136

0 commit comments

Comments
 (0)